// 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 `.`, 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 }