package grants import ( "encoding/json" "fmt" "git.novox.be/novox/mesh-contracts/go/seats/meshbroker" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "slices" "sort" "strings" "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" "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 = meshconditions.Seat + ".condition-raised" EventKeeperCleared = meshconditions.Seat + ".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, meshconditions.Seat+".") || strings.HasPrefix(event, "mesh.seat."+meshconditions.Seat+".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. // The seat mesh-broker's created, recreated and dropped are heard as every seat's event, `.` on EVENTS (the // lead's ruling, registry milestone 13:0x), never on the bus module's own stream. var Consumes = append(append(slices.Clone(seatEvents), seatListEvents...), viewsEvents()...) // seatListEvents are the two events of the seat mesh-catalog the seat list reader follows (mesh-sdk seatlist), heard by it on the // module's event hub, so no handler of the hub takes them besides. var seatListEvents = []string{meshcatalog.Seat + "." + meshcatalog.EventSeatDefinitionChanged, meshcatalog.Seat + "." + meshcatalog.EventSeatDefinitionRemoved} // seatEvents are the events of seats the part follows its passes by, as `.`. var seatEvents = []string{EventKeeperRaised, EventKeeperCleared, EventAccountIssued, meshbroker.Seat + "." + BrokerCreated, meshbroker.Seat + "." + BrokerDropped, meshbroker.Seat + "." + BrokerRecreated} // SeatEventSubjects are the subjects of the seats' events the part follows its passes by besides the verdicts' and the // views': the keeper's condition events, the vault's account-issued and the seat mesh-broker's, each `.` of // Consumes on the seat's own subject. func SeatEventSubjects() []string { var out []string for _, c := range seatEvents { seat, event, _ := strings.Cut(c, ".") out = append(out, "mesh.seat."+seat+".event."+event) } return out } // viewsEvents are the owners' events the part follows its first views by: `.views.`, but the verdicts by // the indexer's manifest-accepted and manifest-refused (VerdictEvents, ADR 0622 §2). func viewsEvents() []string { var out []string for _, fv := range FirstViews { if fv.Read == ReadVerdicts { out = append(out, VerdictEvents...) continue } out = append(out, fv.Owner+".views."+fv.State) } return out } // 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 }