messenger: ask each machine's own engine whether it is root-free, the node seat node-engine's root-free, not the controller's (issue 774, ADR 0563 §6)

This commit is contained in:
2026-10-28 21:54:36 +01:00
parent ef618e8199
commit 2043200201
8 changed files with 87 additions and 58 deletions
+1 -1
View File
@@ -133,7 +133,7 @@ func pruneGrants(grants map[string]Grant, kind string, now time.Time, keep int)
// stands, closes the ask with a warrant for its asker (asks.go); Details are shown where they were asked.
// Anything else is refused, and said there. The same answer is acted on once.
func (h *Holder) Chosen(kind string, c asks.Choice) (string, error) {
// The controller's root-free word is asked before the work is taken (the confirmation review of 2026-10-09):
// The machines' engines' root-free word is asked before the work is taken (the confirmation review of 2026-10-09):
// up to RootFreeWithin, which no ask, answer or link should wait behind. Asked fresh here, for this answer,
// and judged with in it.
root := h.rootWordFor(kind)
@@ -600,6 +600,16 @@ func TestAnAskIsRefusedInWordsToItsAsker(t *testing.T) {
if _, kept := store.asks["mesh-delivery.d-1"]; kept {
t.Error("a refused ask was recorded")
}
// A node's engine is an asker under node-engine-<node> (novox/hq ADR 0563 §3, changing ADR 0259 §2): its ask is
// recorded under that name, with its own bound of open asks; a name that is no asker's is dropped.
submit(t, h, "node-engine-ace", heldAsk(c, "hand-over-1"))
submit(t, h, "node.ace", heldAsk(c, "hand-over-2"))
if rec, kept := store.asks["node-engine-ace.hand-over-1"]; !kept || rec.Asker != "node-engine-ace" {
t.Errorf("a node's engine's ask was not recorded under its name: %+v", rec)
}
if _, kept := store.asks["node.ace.hand-over-2"]; kept {
t.Error("an ask under a name that is no asker's was recorded")
}
// No channel can carry anything: held with the reason (ADR 0397), never queued in silence.
h2, _, _, c2, store2, w2 := newRouter(t)
h2.Bench.Stand("telegram", Standing{Ready: false, Why: "no bot token", At: c2.now()})
+51 -37
View File
@@ -242,8 +242,9 @@ func (h *Holder) carriers(level asks.Level) []string {
return out
}
// RootVerdict is the controller's word on one machine (its verb root-free, novox/hq ADR 0259 §8): free only
// when it measured, recently, that no agent can become root there without a person.
// RootVerdict is a machine's engine's word on that machine (the node seat node-engine's root-free, novox/hq ADR 0259
// §8 as ADR 0563 §6 changes it): free only when it measured, recently, that no agent can become root there without a
// person.
type RootVerdict struct {
Machine string `json:"machine"`
Free bool `json:"free"`
@@ -251,51 +252,64 @@ type RootVerdict struct {
Judged string `json:"judged,omitempty"`
}
// RootFreeVerb is the controller's verb the router asks, as this module, which machines are root-free.
const RootFreeVerb = "seat:" + ControllerSeat + ".root-free"
// EngineSeat is the node seat each machine's engine holds, and RootFreeVerb its verb the router asks of a machine, as
// this module: `seat:node-engine.root-free@<machine>`, which that machine's engine alone answers (ADR 0563 §6).
const (
EngineSeat = "node-engine"
RootFreeVerb = "seat:" + EngineSeat + ".root-free"
)
// RootFreeWithin is how long the router waits for the controller's word; RootFreeKept how long a word is
// used to render a message's buttons. An answer is always judged on a word asked then.
// RootFreeWithin is how long the router waits for the machines' words; RootFreeKept how long a word is used to render a
// message's buttons. An answer is always judged on a word asked then.
const (
RootFreeWithin = 10 * time.Second
RootFreeKept = 30 * time.Second
)
// askRootFree asks the controller about machines, failing on no answer in time.
// askRootFree asks each machine's own engine about that machine, at once, failing on no answer in time. A machine's
// word is the verdict its engine gives on it; a verdict an engine gives on another machine is not taken.
func (h *Holder) askRootFree(machines []string) (map[string]RootVerdict, error) {
if h.RootFree != nil {
return h.RootFree(machines)
}
if h.Ask == nil {
return nil, errors.New("this router cannot ask the controller")
return nil, errors.New("this router cannot ask a machine's engine")
}
type answer struct {
raw json.RawMessage
err error
machine string
raw json.RawMessage
err error
}
done := make(chan answer, 1)
go func() {
raw, err := h.Ask(RootFreeVerb, map[string]any{"machines": machines})
done <- answer{raw, err}
}()
var a answer
select {
case a = <-done:
case <-time.After(RootFreeWithin):
return nil, fmt.Errorf("the controller did not answer within %s", RootFreeWithin)
}
if a.err != nil {
return nil, a.err
}
var body struct {
Machines []RootVerdict `json:"machines"`
}
if err := json.Unmarshal(a.raw, &body); err != nil {
return nil, fmt.Errorf("the controller's answer could not be read: %v", err)
done := make(chan answer, len(machines))
for _, m := range machines {
go func(m string) {
raw, err := h.Ask(RootFreeVerb+"@"+m, map[string]any{})
done <- answer{m, raw, err}
}(m)
}
out := map[string]RootVerdict{}
for _, v := range body.Machines {
out[v.Machine] = v
deadline := time.After(RootFreeWithin)
for range machines {
var a answer
select {
case a = <-done:
case <-deadline:
return nil, fmt.Errorf("a machine's engine did not answer within %s", RootFreeWithin)
}
if a.err != nil {
return nil, fmt.Errorf("%s's engine: %w", a.machine, a.err)
}
var body struct {
Machines []RootVerdict `json:"machines"`
}
if err := json.Unmarshal(a.raw, &body); err != nil {
return nil, fmt.Errorf("%s's engine's answer could not be read: %v", a.machine, err)
}
for _, v := range body.Machines {
if v.Machine == a.machine {
out[v.Machine] = v
}
}
}
return out, nil
}
@@ -329,7 +343,7 @@ func (h *Holder) rootFree(k KindInfo, cached bool) string {
case cached:
// Read inside the work, which never waits on the controller: no recent word is no pass. warmRootFree
// asks outside it.
return "the controller's word on whether an agent can become root there is not recent"
return "the machines' engines' word on whether an agent can become root there is not recent"
default:
got, err := h.askRootFree(machines)
if err != nil {
@@ -351,7 +365,7 @@ func (h *Holder) rootFree(k KindInfo, cached bool) string {
v, said := verdicts[m]
switch {
case !said:
return "the controller said nothing of whether an agent can become root on " + m
return "its engine said nothing of whether an agent can become root on " + m
case !v.Free:
why := v.Why
if why == "" {
@@ -364,7 +378,7 @@ func (h *Holder) rootFree(k KindInfo, cached bool) string {
return ""
}
// warmRootFree asks the controller's root-free word for every kind that proves its sender, outside the work,
// warmRootFree asks the machines' engines' root-free word for every kind that proves its sender, outside the work,
// so what is rendered and offered inside it reads a recent word.
// force asks even a recent word again: taking an ask, whose refusal its asker keeps until something changes.
func (h *Holder) warmRootFree(force bool) {
@@ -390,7 +404,7 @@ func (h *Holder) warmRootFree(force bool) {
}
}
// rootWordFor asks the controller's root-free word for a kind fresh, before the work is taken, and answers
// rootWordFor asks the machines' engines' root-free word for a kind fresh, before the work is taken, and answers
// what judging an answer of that kind then reads: the word for that very kind's placement, or why there is none.
// A kind that does not prove its sender is asked nothing: its answers never approve.
func (h *Holder) rootWordFor(kind string) func(KindInfo) string {
@@ -453,7 +467,7 @@ func (h *Holder) TakeAsk(verb, asker string, body json.RawMessage) error {
if verb == ConditionVerb {
// Before the work lock: Condition takes it, and decides as for the controller's conditions.
if !asks.UsableID(strings.ReplaceAll(asker, "-", "_")) {
h.Logf("[messenger] a condition from %q was taken and dropped: not a module's name", asker)
h.Logf("[messenger] a condition from %q was taken and dropped: not an asker's name (a module's, or a node's engine's)", asker)
return nil
}
h.takeCondition(asker, body)
@@ -466,7 +480,7 @@ func (h *Holder) TakeAsk(verb, asker string, body json.RawMessage) error {
h.init()
h.mu.Unlock()
if !asks.UsableID(strings.ReplaceAll(asker, "-", "_")) {
h.Logf("[messenger] an ask from %q was taken and dropped: not a module's name", asker)
h.Logf("[messenger] an ask from %q was taken and dropped: not an asker's name (a module's, or a node's engine's)", asker)
return nil
}
if verb == "cancel" {
+2 -10
View File
@@ -256,16 +256,8 @@ func (r *busRuntime) runtime(id json.RawMessage, method string, params json.RawM
case "mesh/seat.publish":
return map[string]any{}, r.nc.Publish(p.Subject, p.Body)
case "mesh/ask":
if p.Key == RootFreeVerb {
var a struct {
Machines []string `json:"machines"`
}
_ = json.Unmarshal(p.Body, &a)
var out []RootVerdict
for _, m := range a.Machines {
out = append(out, RootVerdict{Machine: m, Free: true})
}
return map[string]any{"machines": out}, nil
if m, ok := strings.CutPrefix(p.Key, RootFreeVerb+"@"); ok {
return map[string]any{"machines": []RootVerdict{{Machine: m, Free: true}}}, nil
}
return nil, fmt.Errorf("the test runtime answers nothing for %s", p.Key)
}
+2 -1
View File
@@ -315,7 +315,8 @@ type Holder struct {
Emit func(event string, body any) error
// Ask calls a seat verb through the mesh, as this module: the answers the operator chooses.
Ask Asker
// RootFree asks the controller's verb root-free about machines; nil asks it through Ask (askRootFree).
// RootFree asks each machine's engine's root-free (the node seat node-engine's, ADR 0563 §6); nil asks it through Ask
// (askRootFree).
RootFree func(machines []string) (map[string]RootVerdict, error)
// Zone is the operator's time zone, which every time in a message is said in; nil is this machine's.
Zone func() *time.Location
@@ -87,7 +87,7 @@ func TestItDeclaresTheSeatsOfAnAskAndHoldsTheOperatorChannel(t *testing.T) { //
// It performs nothing: the verbs it calls read the open conditions, whether root is free, and which issues
// track which conditions.
if !reflect.DeepEqual(m.Emits, []string{"refused"}) || !reflect.DeepEqual(m.Invokes, []string{"seat:" + ControllerSeat + ".conditions",
"seat:" + ControllerSeat + ".root-free", TrackingVerb}) {
"seat:" + EngineSeat + ".root-free", TrackingVerb}) {
t.Fatalf("emits %v invokes %v", m.Emits, m.Invokes)
}
var state []string
+19 -7
View File
@@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"sync"
"testing"
@@ -66,26 +67,37 @@ func TestApprovalNeedsTheControllersWordThatRootIsFree(t *testing.T) {
}
}
// H3: the word is asked of the controller's verb, as this module, and read from its answer.
func TestTheRootWordIsAskedOfTheControllersVerb(t *testing.T) {
// H3, as ADR 0563 §6 changes it: the word on each machine is asked of that machine's own engine, the node seat
// node-engine's root-free there, as this module, and read from its answer; a machine's word is that machine's alone.
func TestTheRootWordIsAskedOfEachMachinesEngine(t *testing.T) {
h, _, _, _, _, _ := newRouter(t)
h.RootFree = nil
var asked []string
h.Ask = func(key string, body any) (json.RawMessage, error) {
asked = append(asked, key)
raw, _ := json.Marshal(body)
if !strings.Contains(string(raw), `"machines":["anchor"]`) {
t.Errorf("asked about %s", raw)
machine := strings.TrimPrefix(key, "seat:node-engine.root-free@")
if machine == "elsewhere" {
machine = "anchor" // an engine that answers for another machine is not believed
}
return json.RawMessage(`{"machines":[{"machine":"anchor","free":true,"why":"judged","judged":"2026-10-09T10:00:00Z"}]}`), nil
return json.RawMessage(`{"machines":[{"machine":"` + machine + `","free":true,"why":"judged","judged":"2026-10-09T10:00:00Z"}]}`), nil
}
info, _ := h.Bench.Kind("telegram")
if why := h.rootFree(info, false); why != "" {
t.Fatalf("free not read: %s", why)
}
if len(asked) != 1 || asked[0] != "seat:mesh-controller.root-free" {
if len(asked) != 1 || asked[0] != "seat:node-engine.root-free@anchor" {
t.Errorf("asked %v", asked)
}
asked = nil
h.Node = "elsewhere"
if why := h.rootFree(info, false); !strings.Contains(why, "said nothing of whether an agent can become root on elsewhere") {
t.Errorf("one machine's word was taken for another's: %q", why)
}
sort.Strings(asked)
if len(asked) != 2 || asked[0] != "seat:node-engine.root-free@anchor" || asked[1] != "seat:node-engine.root-free@elsewhere" {
t.Errorf("the router's machine and the channel's were asked as %v", asked)
}
h.Node = "anchor"
h.Ask = func(string, any) (json.RawMessage, error) { return json.RawMessage(`not json`), nil }
if why := h.rootFree(info, false); !strings.Contains(why, "could not be read") {
t.Errorf("an unreadable answer was a pass: %q", why)
+1 -1
View File
@@ -90,7 +90,7 @@
],
"invokes": [
"seat:mesh-controller.conditions",
"seat:mesh-controller.root-free",
"seat:node-engine.root-free",
"seat:issue-tracker.tracking"
],
"state": [