Files
mesh-tools/node-tools/internal/approval/gate.go
T
jschoubben 809288da69 E-5 marked waits: a wait on a marked event runs only on the operator's warrant for that one wait
mesh_wait goes through the runner's approval gate (node-tools/internal/approval, Gate.Waited): a wait whose
subjects overlap a marked subject of any module the catalog half lists (the newest entry of every followed
branch, with the served entry of each module this runner serves) is an act of the runner's own module and
build (MESH_FINGERPRINT at its start), asked once on operator-channel as "Wait on <names> on <node>?", and
held, subscribing nothing but its ask's decided, until its timeout, its caller's going or the choice. On a
warrant it subscribes, its timeout running from then, and answers {answer, approval}. Before the marks are
read nothing is asked; a wait from a seat is never marked. The state fixture moves to meshtest so the
console's table can hold a gate.

Builds: design 34 every-event §34, §35, §36, §37; design 47 G lead answer A-168

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 05:41:53 +01:00

180 lines
7.3 KiB
Go

package approval
import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
"sync"
"time"
condseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// 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 is the runner's held finding writer (views.HeldWriter over the contract's Finding).
type FindingWriter interface {
Write(ctx context.Context, finding condseat.Finding) error
Clear(ctx context.Context, scope, id, kind, 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
// Publish says an accept on a seat under the module's name, on the runner's own account (§7).
Publish func(module, subject string, body any, id string) error
// Record reads the router's record of an ask, `<module>.<id>` in the seat's state asks, on the runner's own account (§14).
Record func(module, id string) (*stdio.StateEntry, error)
// 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); 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())
}
// Gate is the runner's approval gate: every call of a module it serves goes through it before the bundle (§20).
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
// 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: scope machine, 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 := keys.EscapeName(g.d.Node + "." + what)
if g.d.Findings == nil {
g.d.Logf("[mesh-tools] %s %s: %s", kind, id, summary)
return
}
var err error
if raised {
err = g.d.Findings.Write(context.Background(), condseat.Finding{Kind: kind, Scope: scopeMachine, ID: id,
Severity: condseat.FindingSeverityWarning, Source: "approval", Observer: g.d.Runner,
Subject: condseat.Subject{Scope: "module", ID: g.d.Runner, Machine: g.d.Node}, Summary: summary, Headline: headline,
At: g.d.Now().UTC()})
} else {
err = g.d.Findings.Clear(context.Background(), scopeMachine, id, kind, summary)
}
if err != nil {
g.d.Logf("[mesh-tools] the finding %s of %s is not kept: %v", kind, id, err)
}
}
const scopeMachine = "machine"