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
173 lines
6.7 KiB
Go
173 lines
6.7 KiB
Go
// Package approval is the one gate on a call that needs the operator's approval (design 46 7-approval-mark; design 47 G
|
|
// §677, lead answer A-224): the tool runner's and a module's process's seat server's. It holds 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"
|
|
|
|
"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) (Entry, bool, string)
|
|
// Newest is the module's entry at the newest revision; false when the catalog half lists none.
|
|
Newest(module string) (Entry, 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() []Entry
|
|
}
|
|
|
|
// 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 []Entry
|
|
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.Fingerprint != entries[0].Fingerprint) {
|
|
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 Entry, defs map[string]seatdef.Definition) {
|
|
source := "entry " + e.Module + " " + orNone(e.Fingerprint)
|
|
var said marked
|
|
_ = json.Unmarshal(e.Manifest, &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
|
|
}
|