From e3ce4b095ce9ecaf842bd024ba3dcab07d8d9507 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 21:36:26 +0100 Subject: [PATCH] Count mesh-sdk's kept RuntimeModule as the tool runner while node-tools' trunk manifest leaves tool-runner out The running indexer refuses the tool-runner word, so node-tools' trunk manifest takes it out again (mesh-tools fix/l3-words-after-judge); trunk's judge then refuses node-tools twice (its waits, its runtime bundle) and mesh-grants would compose its node users as plain modules. One helper, sdk/toolrunner.Is, counts a manifest as the tool runner when it says tool-runner or its module is the SDK's deprecated catalogue.RuntimeModule; the judge's waits and undelivered-bundle rules and mesh-grants' declared records ask it. It goes once tool-runner is back on node-tools' trunk manifest (issue 774, issue 1585 note 42). Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn --- modules/mesh-delivery-indexer/judge/rules.go | 5 ++-- .../mesh-delivery-indexer/judge/rules_test.go | 20 +++++++++++--- modules/mesh-grants/go.mod | 4 +++ modules/mesh-grants/grants_test.go | 27 +++++++++++++------ .../mesh-grants/internal/grants/records.go | 3 ++- sdk/toolrunner/toolrunner.go | 16 +++++++++++ 6 files changed, 60 insertions(+), 15 deletions(-) create mode 100644 sdk/toolrunner/toolrunner.go diff --git a/modules/mesh-delivery-indexer/judge/rules.go b/modules/mesh-delivery-indexer/judge/rules.go index 734c5730..b780e482 100644 --- a/modules/mesh-delivery-indexer/judge/rules.go +++ b/modules/mesh-delivery-indexer/judge/rules.go @@ -10,6 +10,7 @@ import ( json "encoding/json" errors "errors" fmt "fmt" + toolrunner "git.novox.be/novox/mesh-delivery/sdk/toolrunner" catalogue "git.novox.be/novox/mesh-sdk/go/catalogue" math "math" net "net" @@ -136,7 +137,7 @@ func spokenSystems() string { // and was simply absent — seven modules' tools went missing that way on 2026-10-03. Refused here, // naming the field that would deliver it. func undeliveredBundles(m catalogue.Manifest) []string { - if m.Build == nil || catalogue.IsToolRunner(m) { + if m.Build == nil || toolrunner.Is(m) { return nil } named := runByAResource(m) @@ -3306,7 +3307,7 @@ func waitProblems(m catalogue.Manifest) []string { if len(m.Waits) == 0 { return nil } - if !catalogue.IsToolRunner(m) { + if !toolrunner.Is(m) { return []string{fmt.Sprintf("%s says what a session waits on; only the tool runner's module (it says "+ "\"tool-runner\": true) declares waits (novox/hq ADR 0367 §2)", m.Module)} } diff --git a/modules/mesh-delivery-indexer/judge/rules_test.go b/modules/mesh-delivery-indexer/judge/rules_test.go index 5e3c4325..32d37b9c 100644 --- a/modules/mesh-delivery-indexer/judge/rules_test.go +++ b/modules/mesh-delivery-indexer/judge/rules_test.go @@ -163,11 +163,18 @@ func TestABundleNothingDeliversIsRefused(t *testing.T) { t.Errorf("a bundle delivered by %s was refused: %v", name, p) } } - // The tool runner is what says it is one (issue 774): its name alone exempts nothing. + // The tool runner is what says it is one (issue 774), and, while node-tools' trunk manifest leaves `tool-runner` out + // for the running indexer, mesh-sdk's deprecated catalogue.RuntimeModule too (issue 1585 note 42): any other name alone + // exempts nothing. + kept := base() + kept.Module = catalogue.RuntimeModule + if p := undeliveredBundles(kept); len(p) != 0 { + t.Errorf("a bundle of the kept runtime module, which does not say it is the tool runner, was refused: %v", p) + } named := base() - named.Module = "node-tools" + named.Module = "agent-tools" if p := undeliveredBundles(named); len(p) != 1 { - t.Errorf("a bundle of a module named as the tool runner, which does not say it is one: %v", p) + t.Errorf("a bundle of a module that does not say it is the tool runner: %v", p) } } @@ -3115,7 +3122,7 @@ func TestAVerbWithAnUnknownKeyIsRefused(t *testing.T) { func TestAWaitIsRefusedUnlessItNamesOneFamilyOfTheToolRunner(t *testing.T) { for manifest, want := range map[string]string{ `{"module":"shop","version":"1","waits":["mesh-knowledge.reviewed"]}`: "only the tool runner's module", - `{"module":"node-tools","version":"1","waits":["mesh-knowledge.reviewed"]}`: "only the tool runner's module", // its name says nothing (issue 774) + `{"module":"agent-tools","version":"1","waits":["mesh-knowledge.reviewed"]}`: "only the tool runner's module", // its name says nothing (issue 774) `{"module":"node-tools","version":"1","tool-runner":true,"waits":["mesh-knowledge.*"]}`: "never a wildcard", `{"module":"node-tools","version":"1","tool-runner":true,"waits":["**"]}`: "never a wildcard", `{"module":"node-tools","version":"1","tool-runner":true,"waits":["mesh-knowledge.>"]}`: "never a wildcard", @@ -3134,6 +3141,11 @@ func TestAWaitIsRefusedUnlessItNamesOneFamilyOfTheToolRunner(t *testing.T) { t.Errorf("%s refused for the wrong reason (want %q): %v", manifest, want, err) } } + // mesh-sdk's deprecated catalogue.RuntimeModule waits without saying `tool-runner`, while node-tools' trunk manifest + // leaves the word out for the running indexer (issue 774, issue 1585 note 42). + if _, err := ParseManifest([]byte(`{"module":"` + catalogue.RuntimeModule + `","version":"1","waits":["mesh-knowledge.reviewed"]}`)); err != nil { + t.Errorf("the kept runtime module's waits were refused: %v", err) + } } // 1178/AC2: a work-queue stream is read by exactly one process of its module and by no other module's consumes: one diff --git a/modules/mesh-grants/go.mod b/modules/mesh-grants/go.mod index a90cee61..bba38e07 100644 --- a/modules/mesh-grants/go.mod +++ b/modules/mesh-grants/go.mod @@ -20,3 +20,7 @@ require ( golang.org/x/sys v0.48.0 // indirect golang.org/x/time v0.15.0 // indirect ) + +require git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000 + +replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 88deec86..d639fee7 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -2842,27 +2842,38 @@ func TestGrants(t *testing.T) { // ADR 0601 §4: a tool runner whose manifest does not say so yet (an older build's) keeps its user as the list in force // holds it, known by what it runs (it says the runner is alive), and the held condition says so; never composed as a // plain module. - const runner = `{"module":"node-tools","version":"1","tool-runner":true,"own-secrets":{"broker":"/var/lib/nt/broker"}}` - b.manifestOf("node-tools", runner) - b.assign("a", "node-tools") - b.put(grants.ReadAccounts, "a.node-tools", map[string]any{"user": "a.node-tools", "hash": "$2a$10$first", "generation": 1, "fingerprint": "sha256:1"}) + const runner = `{"module":"agent-runtime","version":"1","tool-runner":true,"own-secrets":{"broker":"/var/lib/nt/broker"}}` + b.manifestOf("agent-runtime", runner) + b.assign("a", "agent-runtime") + b.put(grants.ReadAccounts, "a.agent-runtime", map[string]any{"user": "a.agent-runtime", "hash": "$2a$10$first", "generation": 1, "fingerprint": "sha256:1"}) p.Compose() inForce, _, _ = p.Sealed() - ran, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(inForce.Digest, "a.node-tools")) + ran, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(inForce.Digest, "a.agent-runtime")) if !ok || !strings.Contains(string(ran), "mesh.control.a.tools-alive") { t.Fatalf("the list in force holds no tool runner on a: %s", ran) } - b.manifestOf("node-tools", strings.Replace(runner, `"tool-runner":true,`, "", 1)) + b.manifestOf("agent-runtime", strings.Replace(runner, `"tool-runner":true,`, "", 1)) b.put(grants.ReadAccounts, "a.mod-x", map[string]any{"user": "a.mod-x", "hash": "$2a$10$gen10", "generation": 10, "fingerprint": "sha256:10"}) p.Compose() now, _, _ := p.Sealed() - kept, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(now.Digest, "a.node-tools")) + kept, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(now.Digest, "a.agent-runtime")) if !ok || string(kept) != string(ran) { t.Errorf("the tool runner of a was composed without what it runs: %s, was %s", kept, ran) } - if last := said[len(said)-1]; last.State != asks.StateRaised || !strings.Contains(last.Summary, "a.node-tools (stays in the list") { + if last := said[len(said)-1]; last.State != asks.StateRaised || !strings.Contains(last.Summary, "a.agent-runtime (stays in the list") { t.Errorf("the kept tool runner was not said: %+v", last) } + // While node-tools' trunk manifest leaves `tool-runner` out for the running indexer, mesh-sdk's deprecated + // catalogue.RuntimeModule is the tool runner without saying so (issue 774, issue 1585 note 42): composed as one. + b.manifestOf(catalogue.RuntimeModule, strings.Replace(strings.Replace(runner, `"tool-runner":true,`, "", 1), "agent-runtime", catalogue.RuntimeModule, 1)) + b.assign("c", catalogue.RuntimeModule) + b.put(grants.ReadAccounts, "c."+catalogue.RuntimeModule, map[string]any{"user": "c." + catalogue.RuntimeModule, "hash": "$2a$10$c", "generation": 1, "fingerprint": "sha256:c"}) + p.Compose() + now, _, _ = p.Sealed() + if composed, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(now.Digest, "c."+catalogue.RuntimeModule)); !ok || + !strings.Contains(string(composed), "mesh.control.c.tools-alive") { + t.Errorf("the kept runtime module on c was not composed as the tool runner: %s", composed) + } }) } diff --git a/modules/mesh-grants/internal/grants/records.go b/modules/mesh-grants/internal/grants/records.go index c93bf116..1213cb9e 100644 --- a/modules/mesh-grants/internal/grants/records.go +++ b/modules/mesh-grants/internal/grants/records.go @@ -6,6 +6,7 @@ import ( "slices" "strings" + "git.novox.be/novox/mesh-delivery/sdk/toolrunner" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" "mesh-grants/internal/buscompose" @@ -132,7 +133,7 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio fromModules = append(fromModules, c) } d := buscompose.Declared{ - Module: m.Module, ToolRunner: catalogue.IsToolRunner(m), Emits: m.EmitsAll(), Consumes: fromModules, Watches: watches, Serves: m.Tools, + Module: m.Module, ToolRunner: toolrunner.Is(m), Emits: m.EmitsAll(), Consumes: fromModules, Watches: watches, Serves: m.Tools, Invokes: m.Invokes, Waits: waitsOf(m, seats), State: bucketsOf(m), Reads: m.Reads, Logs: logsOf(m), Streams: streamsOf(m), Queues: queueOwnersOf(m), Processes: processesOf(m), Checks: catalogue.HealthChecks(m), RunsAs: m.RunsAs, Issued: issuedTools(m), diff --git a/sdk/toolrunner/toolrunner.go b/sdk/toolrunner/toolrunner.go new file mode 100644 index 00000000..b42c5f35 --- /dev/null +++ b/sdk/toolrunner/toolrunner.go @@ -0,0 +1,16 @@ +// Package toolrunner says whether a manifest is the node's tool runner, for the parts of mesh-delivery that judge or +// compose one (the indexer's judge, mesh-grants). +package toolrunner + +import "git.novox.be/novox/mesh-sdk/go/catalogue" + +// Is says whether a manifest is the node's tool runner: it says so (`tool-runner`, catalogue.IsToolRunner), or its module +// is mesh-sdk's deprecated catalogue.RuntimeModule. +// +// The second clause is kept only while node-tools' manifest on mesh-tools' trunk does not say `tool-runner`: the running +// indexer refuses that word, so node-tools' trunk manifest leaves it out until the indexer that knows it is delivered +// (issue 774, issue 1585 note 42). It goes, with this helper's use of catalogue.RuntimeModule, once `tool-runner` is +// back on node-tools' trunk manifest; then every caller asks catalogue.IsToolRunner alone. +func Is(m catalogue.Manifest) bool { + return catalogue.IsToolRunner(m) || m.Module == catalogue.RuntimeModule +} -- 2.54.0