The gate (node-tools/internal/approval) derives a module's marked tools and verbs, and the subjects of its marked events and restricted streams, from its served entry (the build its composed environment's fingerprint names) and the newest, unioned while they differ, and the seat definitions' verb marks, which no holder unmarks. A bundle whose marks name a tool it does not register is not loaded, its finding mark-unserved raised through the runner's held writer and cleared when a bundle serves it. The first views now start whether or not the console listens, since the gate reads the catalog half too. Builds: design 46 7-approval-mark §5, §4 (at load), §3; design 34 every-event §33; lead answer A-154 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
177 lines
6.6 KiB
Go
177 lines
6.6 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
|
|
}
|
|
|
|
// 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 {
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
// 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
|
|
}
|