Keep carrying a module whose bundle the tool runtime loads, as route-proxy's helper writes its state on the runner's account (issue 1329)

This commit is contained in:
2026-10-19 23:14:51 +00:00
parent 1d0df24a0d
commit b893cc829c
3 changed files with 67 additions and 2 deletions
@@ -636,3 +636,58 @@ func TestTheToolRunnerStillCarriesAModuleWithToolsAndItsOwnCredential(t *testing
}
}
}
// routeProxyExcerpt is the part of mesh-control's modules/route-proxy/module.json at 70cb217601f9 (read 2026-10-20)
// that decides its grants: its state, what it reads, its own broker secret, and the bundle `provider` that the machine's
// runtime loads (`loads`) though the module declares no tools.
const routeProxyExcerpt = `{
"module": "route-proxy",
"state": ["bound"],
"reads": ["mesh-controller.assignments"],
"own-secrets": { "broker": "${dir:mesh-state}/broker" },
"build": { "artifacts": [
{ "name": "provider", "kind": "bundle", "language": "go", "from": "cmd/route-provider", "binary": "route-provider", "loads": ["route-provider"] }
] }
}`
// 1329 (review): a module with no tools whose bundle the runtime loads stays carried, because its helper writes its state
// and reads on the runner's account; the delivery's five parts are still dropped.
func TestTheToolRunnerStillCarriesAModuleWhoseBundleItLoads(t *testing.T) {
var m struct {
Module string `json:"module"`
State []string `json:"state"`
Reads []string `json:"reads"`
Build struct {
Artifacts []struct {
Loads []string `json:"loads"`
} `json:"artifacts"`
} `json:"build"`
}
if err := json.Unmarshal([]byte(routeProxyExcerpt), &m); err != nil {
t.Fatal(err)
}
proxy := Declared{Module: m.Module, Reads: m.Reads}
for _, s := range m.State {
proxy.State = append(proxy.State, Bucket{Module: m.Module, Name: s, History: 1})
}
for _, a := range m.Build.Artifacts {
proxy.Loads = proxy.Loads || len(a.Loads) > 0
}
streams := map[string]ModuleStream{"mesh-delivery-forge-bridge": {Module: "mesh-delivery-forge-bridge", Name: "FORGE"}}
all := composed(t, Records{Nodes: []string{"novox"}, Assigned: map[string][]Declared{"novox": {
{Module: RuntimeModule}, proxy, declaredOf(t, bridgeManifest, streams), declaredOf(t, indexerManifest, streams),
declaredOf(t, tellerManifest, streams), declaredOf(t, watchmanManifest, streams),
}}})
runner := all["runner@novox"]
if !canWrite(runner, "route-proxy_bound", "k") {
t.Error("novox's tool runner does not carry route-proxy, whose bundle it loads")
}
if !canWrite(all["route-proxy@novox"], "route-proxy_bound", "k") {
t.Error("route-proxy lost its own credential")
}
for _, b := range []string{"mesh-delivery-teller_answers", "mesh-delivery-forge-bridge_heads", "mesh-delivery-indexer_modules", "mesh-delivery-watchman_stuck"} {
if canWrite(runner, b, "k") {
t.Errorf("novox's tool runner may write %s", b)
}
}
}
+6 -2
View File
@@ -69,6 +69,10 @@ type Declared struct {
// Checks are the module's own tools its health asks, each `<module>.<tool>` (novox/hq ADR 0240, to-be
// 48 §3): the machine's node-engine asks them of its own node tools, and is granted that and no more.
Checks []string
// Loads says the module has a compiled bundle whose entrypoints the machine's tool runtime loads and runs
// (novox/hq ADR 0175; issue 1329): a helper that reaches the bus on the runner's account, so the runner
// must carry the module although it declares no tools.
Loads bool
// RunsAs is the account the module runs as in a runtime of its own (novox/hq ADR 0259 §8): it is never
// carried by the machine's runtime, and reaches the bus on its own account.
RunsAs string
@@ -394,11 +398,11 @@ func boundBucketsOf(r Records) []string {
// runnerServes says the machine's tool runner has something of this module to serve (novox/hq ADR 0436 §1,
// ADR 0437 §4, issue 1329): tools or loads it answers, a seat it holds, event families a session waits on,
// or a check its health asks. A module with none of these is a process of its own on its own credential, and
// a bundle the runtime loads (Loads), or a check its health asks. A module with none of these is a process of its own on its own credential, and
// carrying it would hand every session on the machine the runner's account with that module's state, seat
// traffic and streams. A module with no account of its own (NoAccount) is spoken for by the runner, so it
// stays carried, and so does the runtime module itself. Whether the module has its own broker secret is not
// the test: a module with tools and its own credential is still carried (ADR 0175).
func runnerServes(d Declared) bool {
return d.Module == RuntimeModule || d.NoAccount || len(d.Serves) > 0 || len(d.Holds) > 0 || len(d.Waits) > 0 || len(d.Checks) > 0
return d.Module == RuntimeModule || d.NoAccount || d.Loads || len(d.Serves) > 0 || len(d.Holds) > 0 || len(d.Waits) > 0 || len(d.Checks) > 0
}
+6
View File
@@ -201,6 +201,12 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
// name (novox/hq ADR 0305).
Issued: issuedTools(m),
}
// Whether the node's runtime loads a bundle of it: a helper that runs on the runner's account (issue 1329).
for _, b := range m.Bundles {
if len(b.Loads) > 0 {
d.Loads = true
}
}
// Whether it can be given an account at all: delivered as its own secret named broker, so one
// that declares none has nowhere to read it (novox/hq issue 195).
if _, reads := m.OwnSecrets["broker"]; !reads {