Files
mesh-tools/node-tools/internal/approval/marks.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

186 lines
7.0 KiB
Go

// Package approval is the tool runner's gate on a call that needs the operator's approval (design 46 7-approval-mark): the
// marks it reads from the catalog half, the act it digests, the ask it raises on the seat operator-channel under the
// marked module's name, the judging of the same call again on the router's record, the claim, the recording in the
// hand-act log, and the refusals when a part of the check is missing (§5, §8 to §20). No module's name is written here.
package approval
import (
"encoding/json"
"fmt"
"sort"
"strings"
catalogseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/seatdef"
)
// Catalog is what the marks are read from: the catalog half the runner follows (design 46 7-approval-mark §5, design 47 G
// §196, §443) and the seat definitions it carries (§2).
type Catalog interface {
// Ready is nil once the catalog half's first view is read, else says what it waits for.
Ready() error
// Revision is the catalog revision the marks are read at, as the runner names it.
Revision() string
// Served is the entry of the build a bundle serves, by the fingerprint its composed environment carries; false when
// none is read (not yet, or not found), with why.
Served(fingerprint string) (catalogseat.CatalogEntry, bool, string)
// Newest is the module's entry at the newest revision; false when the catalog half lists none.
Newest(module string) (catalogseat.CatalogEntry, bool)
// Definitions are the seat definitions the catalog half carries, by seat.
Definitions() map[string]seatdef.Definition
// Entries are the newest entry of every module in every followed branch's half, a marked wait's marks (lead answer
// A-168).
Entries() []catalogseat.CatalogEntry
}
// Marks is what one module's calls are gated by: its marked tools and seat verbs, and the subjects of its marked events and
// restricted streams, each with its source (§18: its module's entry in the catalog half, or its seat's definition).
type Marks struct {
Module string
// Tools are its marked tools by name, Verbs its marked seat verbs as `<seat>.<verb>`, Subjects the bus subjects of its
// marked events and restricted streams (design 34 every-event §33); each to its source.
Tools, Verbs, Subjects map[string]string
// Revision is the catalog revision read; Served the served entry's fingerprint, empty when it could not be read.
Revision, Served string
// Unread says why the served entry could not be read: every marked call is then refused (§5, §19).
Unread string
}
// Marked says whether a tool or a seat verb of the module needs the operator's approval, and its source.
func (m Marks) Marked(seat, name string) (bool, string) {
if seat == "" {
src, ok := m.Tools[name]
return ok, src
}
src, ok := m.Verbs[seat+"."+name]
return ok, src
}
// Any says the module marks anything at all.
func (m Marks) Any() bool { return len(m.Tools)+len(m.Verbs)+len(m.Subjects) > 0 }
// marked is what an entry's manifest says of its marks: `needs-approval`, its events and its streams.
type marked struct {
NeedsApproval []string `json:"needs-approval"`
Emits []string `json:"emits"`
// Streams are read for their events and whether each carries a `grant`, and no other word, so a word added to a
// stream's declaration later never hides its marks.
Streams []struct {
Events []string `json:"events"`
Grant json.RawMessage `json:"grant"`
} `json:"streams"`
}
// Derive is a module's marks from its served entry and its newest one, unioned while they differ so a mark is never lost
// before or after the module moves (§5, ADR 0286 §3), and the seat definitions' verb marks for every verb an entry serves,
// which no holder unmarks (§2, §3). fingerprint is the build its bundle serves; empty, or an entry not read, leaves the
// marks of the newest entry and the seats, with Unread saying why.
func Derive(c Catalog, module, fingerprint string) Marks {
m := Marks{Module: module, Tools: map[string]string{}, Verbs: map[string]string{}, Subjects: map[string]string{},
Revision: c.Revision()}
defs := c.Definitions()
var entries []catalogseat.CatalogEntry
if fingerprint == "" {
m.Unread = "its bundle's environment carries no fingerprint, so the build it serves cannot be read"
} else if e, ok, why := c.Served(fingerprint); ok {
entries, m.Served = append(entries, e), fingerprint
} else {
m.Unread = fmt.Sprintf("the catalog half's entry of %s cannot be read: %s", fingerprint, why)
}
if e, ok := c.Newest(module); ok && (len(entries) == 0 || e.Fp != entries[0].Fp) {
entries = append(entries, e)
}
for _, e := range entries {
m.add(e, defs)
}
return m
}
// add takes one entry's marks into m: its `needs-approval` (a tool, a verb it serves or an event it declares), the seat
// definitions' marks of every verb it serves, and every stream it declares with a `grant`, each with its source.
func (m *Marks) add(e catalogseat.CatalogEntry, defs map[string]seatdef.Definition) {
source := "entry " + e.Module + " " + orNone(e.Fp)
var said marked
if raw, err := json.Marshal(e.Manifest); err == nil {
_ = json.Unmarshal(raw, &said)
}
serves := map[string]bool{}
for _, sv := range e.Serves {
serves[sv] = true
seat, verb, _ := strings.Cut(sv, ".")
for _, v := range defs[seat].Verbs {
if v.Name == verb && v.NeedsApproval {
m.Verbs[sv] = "seat " + seat
}
}
}
events := map[string]bool{}
for _, ev := range said.Emits {
events[ev] = true
}
for _, s := range said.Streams {
for _, ev := range s.Events {
events[ev] = true
}
}
for _, name := range said.NeedsApproval {
switch {
case serves[name]:
if _, bySeat := m.Verbs[name]; !bySeat {
m.Verbs[name] = source
}
case events[name]:
for _, s := range eventSubjects(e.Module, name) {
m.Subjects[s] = source
}
default:
m.Tools[name] = source
}
}
for _, s := range said.Streams {
if len(s.Grant) == 0 || string(s.Grant) == "null" {
continue
}
wire := make([]string, 0, len(s.Events))
for _, ev := range s.Events {
wire = append(wire, buscompose.WirePattern(ev))
}
for _, sub := range (buscompose.ModuleStream{Module: e.Module, Events: wire}).Subjects() {
m.Subjects[sub] = source
if !strings.HasSuffix(sub, ">") {
m.Subjects[sub+".>"] = source
}
}
}
}
// eventSubjects are a marked event's subjects on the bus (design 34 every-event §33): said on the module's events and on its
// own stream, each also with every subject below it.
func eventSubjects(module, event string) []string {
ev := buscompose.WirePattern(event)
out := []string{"mesh.mod." + module + ".event." + ev, buscompose.ModuleStreamSubject(module, ev)}
if !strings.HasSuffix(ev, ">") {
out = append(out, out[0]+".>", out[1]+".>")
}
sort.Strings(out)
return out
}
func orNone(s string) string {
if s == "" {
return "none"
}
return s
}
// keysOf is a set's names, sorted; nil when empty.
func keysOf(set map[string]string) []string {
var out []string
for k := range set {
out = append(out, k)
}
sort.Strings(out)
return out
}