ADR 0590 §3 ends the choice of a condition source by the keeper's take-over key: the controller is unassigned and the keeper is the one source. The part still read the take-over key, fell back to the controller's condition events when it was absent, and read and watched the controller's lease, which nothing writes any more. It now hears the keeper's events alone and reads neither. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
147 lines
5.3 KiB
Go
147 lines
5.3 KiB
Go
package grants
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
|
"git.novox.be/novox/mesh-sdk/go/health"
|
|
)
|
|
|
|
// 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 = 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 (`<seat>.<event>`) or its bus subject
|
|
// (`mesh.seat.<seat>.event.<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`, `<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
|
|
}
|