Main reverted G's half merge (#189, reverted by8afc3ebin #191) and then took the reader release's S1-S5, which copied G's code. G's form holds where they differ (process_bus.go, seats.go, state.go and their tests); the release's kept names stay so main's callers keep building: SeatViewPrefix beside SeatReadPrefix, and QueueInvoke's empty strings on not-ok (with its test TestQueueInvoke). The tree differs from G's headf30dee54only by those and the release's two tests (objects cap option, seat states and stream kinds). Builds: design 47 G (the milestone's sdk part, as atf30dee54) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
179 lines
7.4 KiB
Go
179 lines
7.4 KiB
Go
package approval
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// The runner's findings of the gate (design 46 7-approval-mark §4, §16, §19), written through its held writer.
|
|
const (
|
|
KindMarkUnserved = "mark-unserved"
|
|
KindCallUnanswered = "call-unanswered"
|
|
KindApprovalsUnreadable = "approvals-unreadable"
|
|
)
|
|
|
|
// Kinds are the finding kinds the gate writes through the runner's writer.
|
|
var Kinds = []string{KindMarkUnserved, KindCallUnanswered, KindApprovalsUnreadable}
|
|
|
|
// FindingWriter raises and clears the gate's findings by kind and id (A-228): its caller writes each as the contract's
|
|
// finding, about its own module on its machine, through its held writer.
|
|
type FindingWriter interface {
|
|
Raise(ctx context.Context, kind, id, summary, headline string) error
|
|
Clear(ctx context.Context, kind, id, why string) error
|
|
}
|
|
|
|
// Deps is what the gate reaches the mesh through: the runner's connection in its one place, fakes in a test.
|
|
type Deps struct {
|
|
// Node is the machine the runner serves; Runner its own module's name, the subject of its findings.
|
|
Node, Runner string
|
|
// Catalog is the catalog half the marks are read from.
|
|
Catalog Catalog
|
|
// Wait waits, within the bound, for the catalog half's first view, and is nil once it is read or why not.
|
|
Wait func(within time.Duration) error
|
|
// Findings is the runner's held writer; nil says a finding in the log only.
|
|
Findings FindingWriter
|
|
Logf func(string, ...any)
|
|
Now func() time.Time
|
|
// After is the clock's pause; nil is time.After.
|
|
After func(time.Duration) <-chan time.Time
|
|
// State is the runner's state approvals; nil when its membership binds none, so every marked call is refused (§13).
|
|
State Store
|
|
// Channel is the operator channel, its asks kept in State under this machine's keys (A-219, A-228).
|
|
Channel Channel
|
|
// Warranted asks `mesh-hand-acts.warranted` {asker, ask}, each try a kept call open while open says so (§16, A-157).
|
|
Warranted func(asker, ask string, open func() (bool, error)) error
|
|
// Bounds is the runner's key-ttl state bound (A-157, A-158), or a process holder's state approvals itself (A-255); nil
|
|
// keeps no bound.
|
|
Bounds Bounds
|
|
// Holder says whether a module holds a seat, from the assigner's view of the seats' holders the runner follows (§19).
|
|
Holder func(seat string) (bool, error)
|
|
// Build is the tool runner's own fingerprint, read from its MESH_FINGERPRINT at its start: the build a marked wait's
|
|
// act binds; empty while unknown, when every marked wait is refused (design 34 every-event §35).
|
|
Build string
|
|
// Hear waits within the bound for the first event on the subjects whose body match takes, or the caller's going: a
|
|
// marked wait's ask's `decided` (every-event §36); false when none came.
|
|
Hear func(ctx context.Context, subjects []string, within time.Duration, match func(json.RawMessage) bool) (bool, error)
|
|
// Go runs what goes on beside the call (a run's recording); nil is a goroutine.
|
|
Go func(func())
|
|
// Holding says the gate is a module's process's seat server's (design 47 G §677): its keys are its module's own, its
|
|
// calls held as waiting calls and their ends said through Say; Runner is then the holder's module.
|
|
Holding bool
|
|
// Say publishes one gated-ended on its subject with the message id given (§677).
|
|
Say func(subject string, body any, id string) error
|
|
}
|
|
|
|
// Gate is the approval gate: every call of a module the tool runner serves goes through it before the bundle (§20), and a
|
|
// marked verb a module's process serves before its handler (design 47 G §677; lead answer A-224).
|
|
type Gate struct {
|
|
d Deps
|
|
mu sync.Mutex
|
|
// registered is each module's tools as its bundle registered them at its last load.
|
|
registered map[string]map[string]bool
|
|
// unserved is each module whose marks name a tool its bundle does not register, with those tools.
|
|
unserved map[string][]string
|
|
// unmarkedAt is each caller's address it last called unmarked, so a mark added since is named (§12).
|
|
unmarkedAt map[string]bool
|
|
// inFlight is each ask whose recording runs now.
|
|
inFlight map[string]bool
|
|
// failingSince is when the state approvals began to fail; zero while it is read.
|
|
failingSince time.Time
|
|
// handlers are a process holder's marked verbs' handlers, by `<seat>.<verb>` (design 47 G §677).
|
|
handlers map[string]func(string, json.RawMessage) (any, error)
|
|
// served is each module this runner serves, with the build its bundle serves, as its last load said (§18).
|
|
served map[string]string
|
|
}
|
|
|
|
// New is a gate over d.
|
|
func New(d Deps) *Gate {
|
|
if d.Logf == nil {
|
|
d.Logf = func(string, ...any) {}
|
|
}
|
|
if d.Now == nil {
|
|
d.Now = time.Now
|
|
}
|
|
if d.After == nil {
|
|
d.After = time.After
|
|
}
|
|
if d.Go == nil {
|
|
d.Go = func(f func()) { go f() }
|
|
}
|
|
return &Gate{d: d, registered: map[string]map[string]bool{}, unserved: map[string][]string{}}
|
|
}
|
|
|
|
// Unserved is, of the module's marked tools, those its bundle does not register (§4): such a bundle is not loaded, its
|
|
// finding `mark-unserved` raised; a bundle that serves them, or marks that no longer name them, clear it. Before the marks
|
|
// are read nothing is known, and every marked call is refused until they are (§19).
|
|
func (g *Gate) Unserved(module, fingerprint string, registered []string) []string {
|
|
set := map[string]bool{}
|
|
for _, t := range registered {
|
|
set[t] = true
|
|
}
|
|
g.mu.Lock()
|
|
g.registered[module] = set
|
|
if g.served == nil {
|
|
g.served = map[string]string{}
|
|
}
|
|
g.served[module] = fingerprint
|
|
g.mu.Unlock()
|
|
if g.d.Catalog == nil || g.d.Catalog.Ready() != nil {
|
|
return nil
|
|
}
|
|
return g.checkServed(Derive(g.d.Catalog, module, fingerprint))
|
|
}
|
|
|
|
// checkServed raises or clears the module's `mark-unserved` as its marks and its bundle's registrations stand now, and is
|
|
// the tools it marks and does not register.
|
|
func (g *Gate) checkServed(m Marks) []string {
|
|
g.mu.Lock()
|
|
set := g.registered[m.Module]
|
|
var missing []string
|
|
for t := range m.Tools {
|
|
if !set[t] {
|
|
missing = append(missing, t)
|
|
}
|
|
}
|
|
sort.Strings(missing)
|
|
was := g.unserved[m.Module]
|
|
if len(missing) > 0 {
|
|
g.unserved[m.Module] = missing
|
|
} else {
|
|
delete(g.unserved, m.Module)
|
|
}
|
|
g.mu.Unlock()
|
|
switch {
|
|
case len(missing) > 0 && strings.Join(was, ",") != strings.Join(missing, ","):
|
|
g.finding(m.Module, KindMarkUnserved, true, fmt.Sprintf("%s's marks (%s, catalog revision %s) name %s, which its bundle on %s does not "+
|
|
"register: no tool of %s is served here", m.Module, sourceOf(m, missing[0]), m.Revision, strings.Join(missing, ", "), g.d.Node, m.Module),
|
|
fmt.Sprintf("%s is not served on %s", m.Module, g.d.Node))
|
|
case len(missing) == 0 && len(was) > 0:
|
|
g.finding(m.Module, KindMarkUnserved, false, "a bundle that serves every marked tool, or marks without the tool, is loaded", "")
|
|
}
|
|
return missing
|
|
}
|
|
|
|
func sourceOf(m Marks, tool string) string { return m.Tools[tool] }
|
|
|
|
// finding writes or clears one of the gate's findings, its id `<node>.<what>` escaped as a key's name is,
|
|
// about the runner's module on this machine (lead answer A-154).
|
|
func (g *Gate) finding(what, kind string, raised bool, summary, headline string) {
|
|
id := g.findingID(what)
|
|
if g.d.Findings == nil {
|
|
g.d.Logf("[approval] %s %s: %s", kind, id, summary)
|
|
return
|
|
}
|
|
var err error
|
|
if raised {
|
|
err = g.d.Findings.Raise(context.Background(), kind, id, summary, headline)
|
|
} else {
|
|
err = g.d.Findings.Clear(context.Background(), kind, id, summary)
|
|
}
|
|
if err != nil {
|
|
g.d.Logf("[approval] the finding %s of %s is not kept: %v", kind, id, err)
|
|
}
|
|
}
|