package grants import ( "encoding/json" "fmt" "slices" "sort" "strings" "git.novox.be/novox/mesh-sdk/go/health" "mesh-grants/internal/buscompose" ) // CallRootFree is the node seat's verb that judges one machine root-free (novox/hq ADR 0259 §8, ADR 0563 §6), asked of // that machine's own engine as `seat:node-engine.root-free@`: the judgement the composer needs and the part // cannot make from the bus (issue 1945). Only that machine's engine answers it, and the controller takes no part in it // (ADR 0571 §11, issue 1955). The messenger asks the same verb the same way. const CallRootFree = "seat:node-engine.root-free" // The condition events the part hears (never a timer): a machine's root-free word changes with the raising and the // clearing of its `root-not-free` condition. They are the condition keeper's (mesh-conditions) alone, the one keeper // since the controller ended by its unassignment (novox/hq ADR 0590 §3, which ends ADR 0564 §9's selection by the // take-over key). const ( EventKeeperRaised = health.KeeperModule + ".condition-raised" EventKeeperCleared = health.KeeperModule + ".condition-cleared" // KindRootNotFree is the condition kind whose events wake a pass. KindRootNotFree = "root-not-free" ) // FromKeeper says a condition event heard is the keeper's, by its name (`.`) or its bus subject // (`mesh.seat..event.`). func FromKeeper(event string) bool { return strings.HasPrefix(event, health.KeeperModule+".") || strings.HasPrefix(event, "mesh.seat."+health.KeeperModule+".event.") } // EventAccountIssued is the vault's word that it minted an account (novox/hq ADR 0561 §1): its hash is in the vault's // bus-accounts, so a pass composes the list with it. const EventAccountIssued = "mesh-vault." + buscompose.AccountIssuedEvent // Consumes is every event the part hears, as its manifest's consumes name them. var Consumes = []string{EventKeeperRaised, EventKeeperCleared, EventAccountIssued} // IsAccountIssued says an event heard is the vault's account-issued, by its name or its bus subject. func IsAccountIssued(event string) bool { return event == EventAccountIssued || event == buscompose.AccountIssuedSubject } // IsRootNotFree says a condition event's body names the `root-not-free` kind (its `kind`, or the last part of its // `key`, `..`). An unreadable body wakes a pass too: a pass is cheap, a missed change is not. func IsRootNotFree(body []byte) bool { var c struct { Kind string `json:"kind"` Key string `json:"key"` } if json.Unmarshal(body, &c) != nil { return true } return c.Kind == KindRootNotFree || (c.Kind == "" && strings.HasSuffix(c.Key, "."+KindRootNotFree)) || (c.Kind == "" && c.Key == "") } // RootFreeNodes are the machines a composition needs a root-free word for: where the router's seat is held and where a // kinded seat's holder declares `verified-sender`. None when no holder declares it. Sorted. Pure. func RootFreeNodes(r buscompose.Records) []string { var nodes []string need := false for node, declared := range r.Assigned { for _, d := range declared { for _, s := range d.Holds { if s.Name == "operator-channel" && !slices.Contains(nodes, node) { nodes = append(nodes, node) } if s.Kinded && s.Kind != "" && slices.Contains(s.Capabilities, "verified-sender") { need = true if !slices.Contains(nodes, node) { nodes = append(nodes, node) } } } } } if !need { return nil } sort.Strings(nodes) return nodes } // verdict is one machine's answer of its engine's root-free verb. type verdict struct { Machine string `json:"machine"` Free bool `json:"free"` Why string `json:"why"` } // WithRootFree fills r.RootFree for the machines the composition needs by asking each machine's own engine about that // machine, and answers a line for each machine that is not free, `: `. It fails closed: an error, no // answer or a verdict an engine gives on another machine is not free, and says so. Pure but for the calls; the answers // are kept for this pass alone. func WithRootFree(r *buscompose.Records, call Caller) []string { nodes := RootFreeNodes(*r) if len(nodes) == 0 { return nil } free := map[string]verdict{} failures := map[string]string{} for _, n := range nodes { if call == nil { failures[n] = "no way to ask the machine's engine here" continue } raw, err := call.Call(CallRootFree+"@"+n, map[string]any{}) if err != nil { failures[n] = "the engine's root-free word could not be asked: " + err.Error() continue } var body struct { Machines []verdict `json:"machines"` } if err := json.Unmarshal(raw, &body); err != nil { failures[n] = "the engine's root-free answer could not be read: " + err.Error() continue } for _, v := range body.Machines { if v.Machine == n { free[n] = v } } } r.RootFree = map[string]bool{} var notes []string for _, n := range nodes { v, answered := free[n] switch { case answered && v.Free: r.RootFree[n] = true case failures[n] != "": notes = append(notes, fmt.Sprintf("%s: not free, %s", n, failures[n])) case !answered: notes = append(notes, fmt.Sprintf("%s: not free, its engine named no verdict for it", n)) default: notes = append(notes, fmt.Sprintf("%s: not free, %s", n, v.Why)) } } return notes }