Let a bundle ask the runtime who answers a verb, and the self-check's own account ask the bus's discovery, so D-root and D3 find holders without the controller (issue 1955)

This commit is contained in:
2026-10-29 23:58:21 +01:00
parent 31e90aaa0f
commit fa5a6aeff5
5 changed files with 89 additions and 2 deletions
+8 -2
View File
@@ -41,6 +41,12 @@ var createdAdvisory = regexp.MustCompile(`"\$JS\.EVENT\.ADVISORY\.STREAM\.CREATE
// write.
func beyondTheControllers(user string) map[string]bool {
out := map[string]bool{}
// The self-check's runtime asks who answers (D-root, D3).
if strings.HasSuffix(user, "."+SelfCheckModule) {
for _, s := range discovering() {
out[s] = true
}
}
if strings.HasSuffix(user, "."+WatchmanModule) {
for _, s := range append(WatchmanDeadLetterGrants(nil), WatchmanHears()...) {
out[s] = true
@@ -659,7 +665,7 @@ func healthChecks(t *testing.T) {
"provisioner.a": {"$JS.API.DIRECT.GET.KV_mesh-provisioner_plans.$KV.mesh-provisioner_plans.a.keycloak",
"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_plans.c1.$KV.mesh-provisioner_plans.a.keycloak"},
// ADR 0571 clause 3: restic's bundle writes each item's last good night as its own state; the self-check reads it.
"a.mesh-self-check": {"$JS.API.DIRECT.GET.KV_restic_good.$KV.restic_good.a.pg.data",
"a.mesh-self-check": {"$JS.API.DIRECT.GET.KV_restic_good.$KV.restic_good.a.pg.data", "$SRV.INFO.node-tools", "$SRV.PING",
"$JS.API.DIRECT.GET.KV_mesh-grants_withheld.$KV.mesh-grants_withheld." + WithheldKey(Withheld{Node: "a", Module: "reader", Stream: "MAIL"})},
"a.mesh-grants": {"$KV.mesh-grants_withheld.reader.mail.a"},
"a.restic": {"$JS.API.DIRECT.GET.KV_disk-load_readings.$KV.disk-load_readings.a.pg.data", "$KV.restic_good.a.pg.data",
@@ -688,7 +694,7 @@ func healthChecks(t *testing.T) {
"a.mesh-vault": {"$KV.mesh-provisioner_provisions.a.keycloak.postgres-database", "$KV.mesh-provisioner_plans.a.keycloak",
"mesh.seat.mesh-artifact-sweep.event.artifacts-gone"},
// Each writes its own state alone, and these users are composed.
"a.disk-load": {"$KV.restic_good.a.pg.data"},
"a.disk-load": {"$KV.restic_good.a.pg.data", "$SRV.INFO", "$SRV.PING.node-tools"},
"a.mesh-delivery-builder": {"$KV.disk-load_readings.a.pg.data"},
// Another machine's plans are not its to read; no client writes or reads the needs any more.
"provisioner.a": {"$JS.API.DIRECT.GET.KV_mesh-provisioner_plans.$KV.mesh-provisioner_plans.b.keycloak",
+3
View File
@@ -170,3 +170,6 @@ func keepsDeadLetters(p Principal, pattern string) bool {
return isController(p, pattern) ||
(p.Kind == KindModule && p.Module == WatchmanModule && pattern == deadLetterPrefix+">")
}
// SelfCheckModule is the module holding the seat mesh-self-check, whose runtime asks the bus who answers (D-root, D3).
const SelfCheckModule = health.SelfCheckModule
+5
View File
@@ -1014,6 +1014,11 @@ func PermissionsFor(p Principal) (Permissions, error) {
// And the words its watchdogs hear (ADR 0564 clause 4, Limits 3, 11).
sub = append(sub, WatchmanHears()...)
}
// The self-check asks who answers a verb (D-root, D3): its runtime, on the self-check's own account (runs-as),
// asks the bus's discovery, its replies to its own inbox; no other module's account may.
if p.Module == SelfCheckModule {
pub = append(pub, discovering()...)
}
// The grants part alone publishes the modules' memberships into the assignments stream (novox/hq ADR 0547 §5;
// the operator, 2026-10-25 22:31): one per subject, `mesh.assignment.<node>.<module>`, reading the last one of each
// first so it publishes only what changed, and no stream it defines.
+48
View File
@@ -0,0 +1,48 @@
package stdio
import (
"encoding/json"
"fmt"
"time"
)
// **Who answers a verb, asked by the bus's discovery** (D-root and D3 of the seat mesh-self-check; issue 1955): the
// runtime asks the services protocol on the account it runs as (ADR 0197) and answers each machine and module that
// says it serves the verb. The bundle never asks the bus itself.
// Answerer is one module on one machine that answers a verb.
type Answerer struct {
Machine string `json:"machine"`
Module string `json:"module"`
}
const discoverTimeout = 10 * time.Second
// Discover is every module that answers verb (`<seat>.<verb>` or `<module>.<tool>`), on node when one is named: runtime
// verb `mesh/discover {verb, node?}`, answered as a list. None answering is an empty list and no error; an error is a
// question that could not be asked.
func Discover(verb, node string) ([]Answerer, error) {
if verb == "" {
return nil, fmt.Errorf("discover names the verb it asks about")
}
c, err := live()
if err != nil {
return nil, err
}
params := map[string]any{"verb": verb}
if node != "" {
params["node"] = node
}
raw, err := c.ask("mesh/discover", params, discoverTimeout)
if err != nil {
return nil, err
}
out := []Answerer{}
if len(raw) == 0 || string(raw) == "null" {
return out, nil
}
if err := json.Unmarshal(raw, &out); err != nil {
return nil, fmt.Errorf("the runtime's answer to discover is not readable: %w", err)
}
return out, nil
}
+25
View File
@@ -154,6 +154,17 @@ func logging(t *testing.T, r *logRuntime, run func() (any, error)) map[string]an
if json.Unmarshal(lines.Bytes(), &m) != nil {
continue
}
if m.Method == "mesh/discover" {
// Who answers: one module on ace, none on any other machine named.
var p struct{ Verb, Node string }
_ = json.Unmarshal(m.Params, &p)
var result any = []map[string]string{{"machine": "ace", "module": "node-tools"}}
if p.Node != "" && p.Node != "ace" {
result = nil
}
send(map[string]any{"id": m.ID, "result": result})
continue
}
if strings.HasPrefix(m.Method, "mesh/log.") {
go func(m message) {
a := r.answer(m)
@@ -214,6 +225,7 @@ func TestABundleKeepsALogThroughTheRuntime(t *testing.T) {
narrowed, since *LogPage
newest []LogEntry
watched *LogEntry
found, nobody []Answerer
)
got := logging(t, r, func() (any, error) {
changes := Log("changes")
@@ -272,6 +284,16 @@ func TestABundleKeepsALogThroughTheRuntime(t *testing.T) {
watched = &e
case <-time.After(5 * time.Second):
}
// Who answers a verb, through the runtime (D-root, D3): one, then nobody on a machine none answers on.
if found, err = Discover("node-engine.root-free", ""); err != nil {
return nil, err
}
if nobody, err = Discover("node-engine.root-free", "g14"); err != nil {
return nil, err
}
if _, err := Discover("", ""); err == nil {
return nil, fmt.Errorf("a discover naming no verb was asked")
}
return "done", nil
})
if out, isError := text(t, got); isError || out != `"done"` {
@@ -342,6 +364,9 @@ func TestABundleKeepsALogThroughTheRuntime(t *testing.T) {
p["from"] != "since" || p["since"] != "2026-10-11T18:00:00Z" || watched == nil || watched.Sequence != 6 {
t.Fatalf("a watch asked %s %v and heard %+v", method, p, watched)
}
if len(found) != 1 || found[0] != (Answerer{Machine: "ace", Module: "node-tools"}) || nobody == nil || len(nobody) != 0 {
t.Fatalf("discover found %+v, and on g14 %+v", found, nobody)
}
if info == nil || info.Entries != 6 || info.MaxBytes != 1<<30 || info.First != 1 || info.Last != 6 || info.Bytes == 0 {
t.Fatalf("the log's info is %+v", info)
}