The lead's ruling of 13:0x (registry milestone): mesh-broker's events are mesh.seat.mesh-broker.event.<event> on the shared events stream like every seat's. The grants part and the watchman consume mesh-broker.<event> and no longer hear the bus module's own stream, which is its history for its tool. The keeper's process account is composed the seat's event subjects, and the acknowledgement of a snapshot's chunks of each entry it keeps, which ADR 0626 §1 grants and the composition lacked; a row holds it to no subscription of a restricted stream's subjects and no consumer of one. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
165 lines
6.1 KiB
Go
165 lines
6.1 KiB
Go
package grants
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"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@<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.
|
|
// 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([]string{EventKeeperRaised, EventKeeperCleared, EventAccountIssued, catalogue.BrokerSeat + "." + BrokerCreated,
|
|
catalogue.BrokerSeat + "." + BrokerDropped, catalogue.BrokerSeat + "." + BrokerRecreated}, viewsEvents()...)
|
|
|
|
// 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
|
|
}
|