Count mesh-sdk's kept RuntimeModule as the tool runner while node-tools' trunk manifest leaves tool-runner out
mesh/delivery merged: source.main moved to novox/mesh-delivery@007c982ae670
mesh/delivery-group group fix/l3-words-after-judge merged: every member is merged
mesh/check being carried to e3ce4b095ce9 on 007c982ae670 | from 4154c3040cee, mesh-catalog e6de42630355, mesh-tools 01ee2ef39488

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
This commit is contained in:
2026-11-03 21:36:26 +01:00
parent 4154c3040c
commit e3ce4b095c
6 changed files with 60 additions and 15 deletions
+3 -2
View File
@@ -10,6 +10,7 @@ import (
json "encoding/json" json "encoding/json"
errors "errors" errors "errors"
fmt "fmt" fmt "fmt"
toolrunner "git.novox.be/novox/mesh-delivery/sdk/toolrunner"
catalogue "git.novox.be/novox/mesh-sdk/go/catalogue" catalogue "git.novox.be/novox/mesh-sdk/go/catalogue"
math "math" math "math"
net "net" 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, // and was simply absent — seven modules' tools went missing that way on 2026-10-03. Refused here,
// naming the field that would deliver it. // naming the field that would deliver it.
func undeliveredBundles(m catalogue.Manifest) []string { func undeliveredBundles(m catalogue.Manifest) []string {
if m.Build == nil || catalogue.IsToolRunner(m) { if m.Build == nil || toolrunner.Is(m) {
return nil return nil
} }
named := runByAResource(m) named := runByAResource(m)
@@ -3306,7 +3307,7 @@ func waitProblems(m catalogue.Manifest) []string {
if len(m.Waits) == 0 { if len(m.Waits) == 0 {
return nil 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 "+ 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)} "\"tool-runner\": true) declares waits (novox/hq ADR 0367 §2)", m.Module)}
} }
@@ -163,11 +163,18 @@ func TestABundleNothingDeliversIsRefused(t *testing.T) {
t.Errorf("a bundle delivered by %s was refused: %v", name, p) 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 := base()
named.Module = "node-tools" named.Module = "agent-tools"
if p := undeliveredBundles(named); len(p) != 1 { 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) { func TestAWaitIsRefusedUnlessItNamesOneFamilyOfTheToolRunner(t *testing.T) {
for manifest, want := range map[string]string{ for manifest, want := range map[string]string{
`{"module":"shop","version":"1","waits":["mesh-knowledge.reviewed"]}`: "only the tool runner's module", `{"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":["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":["**"]}`: "never a wildcard",
`{"module":"node-tools","version":"1","tool-runner":true,"waits":["mesh-knowledge.>"]}`: "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) 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 // 1178/AC2: a work-queue stream is read by exactly one process of its module and by no other module's consumes: one
+4
View File
@@ -20,3 +20,7 @@ require (
golang.org/x/sys v0.48.0 // indirect golang.org/x/sys v0.48.0 // indirect
golang.org/x/time v0.15.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
+19 -8
View File
@@ -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 // 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 // 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. // plain module.
const runner = `{"module":"node-tools","version":"1","tool-runner":true,"own-secrets":{"broker":"/var/lib/nt/broker"}}` const runner = `{"module":"agent-runtime","version":"1","tool-runner":true,"own-secrets":{"broker":"/var/lib/nt/broker"}}`
b.manifestOf("node-tools", runner) b.manifestOf("agent-runtime", runner)
b.assign("a", "node-tools") b.assign("a", "agent-runtime")
b.put(grants.ReadAccounts, "a.node-tools", map[string]any{"user": "a.node-tools", "hash": "$2a$10$first", "generation": 1, "fingerprint": "sha256:1"}) 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() p.Compose()
inForce, _, _ = p.Sealed() 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") { 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) 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"}) 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() p.Compose()
now, _, _ := p.Sealed() 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) { if !ok || string(kept) != string(ran) {
t.Errorf("the tool runner of a was composed without what it runs: %s, was %s", kept, 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) 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)
}
}) })
} }
@@ -6,6 +6,7 @@ import (
"slices" "slices"
"strings" "strings"
"git.novox.be/novox/mesh-delivery/sdk/toolrunner"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-grants/internal/buscompose" "mesh-grants/internal/buscompose"
@@ -132,7 +133,7 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
fromModules = append(fromModules, c) fromModules = append(fromModules, c)
} }
d := buscompose.Declared{ 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), 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), Streams: streamsOf(m), Queues: queueOwnersOf(m), Processes: processesOf(m), Checks: catalogue.HealthChecks(m),
RunsAs: m.RunsAs, Issued: issuedTools(m), RunsAs: m.RunsAs, Issued: issuedTools(m),
+16
View File
@@ -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
}