Files
mesh-delivery/modules/mesh-grants/internal/grants/rootfree.go
T

185 lines
7.2 KiB
Go

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@<machine>`: 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 (`<seat>.<event>`) or its bus subject
// (`mesh.seat.<seat>.event.<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, `<seat>.<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 `<seat>.<event>`.
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 `<seat>.<event>` 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: `<owner>.views.<state>`, 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`, `<scope>.<id>.<kind>`). 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, `<machine>: <why>`. 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
}