Carry a module on the tool runner only when the runner has something of it to serve, so a part on its own credential has one writer (issue 1329)

This commit is contained in:
2026-10-19 23:10:48 +00:00
parent 40350d82fa
commit 1d0df24a0d
4 changed files with 165 additions and 6 deletions
+2 -2
View File
@@ -67,8 +67,8 @@ func TestTheGatesBucketsAreWritableByItsMachinesToolRunner(t *testing.T) {
if !canRead(runner, read, "k") {
t.Errorf("novox's tool runner may not read %s, which the gate declares reading", read)
}
// (The runner may write the teller's answers: the teller declares no runs-as, so the runner carries it too. See the report of issue 1326.)
if canWrite(runner, read, "k") && read != "mesh-delivery-teller_answers" {
// 1329/AC3: the runner no longer carries the teller, so it may not write the teller's answers either.
if canWrite(runner, read, "k") {
t.Errorf("novox's tool runner may write %s, which the gate only reads", read)
}
}
+150 -2
View File
@@ -13,8 +13,9 @@ import (
// and streams, `reads` watching another module's bucket and never writing it, `consumes` hearing the named events,
// `invokes` calling the named tools, `uses` the seat operator-channel's by-caller subjects under the module's own name.
//
// The three manifests are copied from mesh-delivery's main at 6a23627426c6 (modules/<module>/module.json, read
// 2026-10-19), whole, so a change to one of them there that changes its grant is a change to read here.
// The four manifests (the watchman's is the fourth, issue 1329) are copied from mesh-delivery's main at 9c15112d935a
// (modules/<module>/module.json, read 2026-10-20; the first three are unchanged since 6a23627426c6), whole, so a change
// to one of them there that changes its grant is a change to read here.
const bridgeManifest = `{
"module": "mesh-delivery-forge-bridge",
"version": "1",
@@ -160,6 +161,69 @@ const indexerManifest = `{
}
}`
const watchmanManifest = `{
"module": "mesh-delivery-watchman",
"version": "1",
"slug": "watchman",
"reads": [
"mesh-controller.checks-running"
],
"state": [
{ "name": "presence", "history": 20 },
{ "name": "pulse", "key-ttl": true },
{ "name": "stuck", "history": 20 }
],
"own-secrets": {
"broker": "${dir:state}/broker"
},
"secrets-owner": "mesh-delivery-watchman",
"resources": [
{
"id": "account",
"type": "user",
"name": "mesh-delivery-watchman",
"shell": "/usr/bin/nologin",
"home": "/var/lib/mesh-delivery-watchman"
},
{
"id": "state",
"type": "directory",
"mode": "0700",
"owner": "mesh-delivery-watchman",
"place": ".",
"unknown": "program"
},
{
"id": "watchman",
"type": "process",
"name": "mesh-delivery-watchman",
"artifact": "program",
"run": [
"./mesh-delivery-watchman"
],
"env": {
"MESH_BROKER_FILE": "${dir:state}/broker"
},
"user": "mesh-delivery-watchman",
"health": {
"kind": "unit"
}
}
],
"build": {
"artifacts": [
{
"name": "program",
"kind": "bundle",
"language": "go",
"system": "arch",
"from": "cmd/mesh-delivery-watchman",
"binary": "mesh-delivery-watchman"
}
]
}
}`
const tellerManifest = `{
"module": "mesh-delivery-teller",
"version": "1",
@@ -488,3 +552,87 @@ func TestNoPartsCredentialReachesBeyondItsDeclaration(t *testing.T) {
}
}
}
// toolServer is a module that serves a tool on its own broker credential: the runner carries it (ADR 0175).
func toolServer() Declared {
return Declared{Module: "some-tools", Serves: []string{"some_tool"},
State: []Bucket{{Module: "some-tools", Name: "kept", History: 1}}}
}
// 1329/AC1: composed from the manifests of mesh-delivery's main, the machine's tool runner may write no bucket of the
// forge bridge, the indexer, the teller or the watchman, publish none of their stream events nor the teller's
// operator-channel accept subjects, and pull none of their consumers; each part's own credential still may.
func TestTheToolRunnerCarriesNoneOfTheDeliverysParts(t *testing.T) {
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}, declaredOf(t, bridgeManifest, streams), declaredOf(t, indexerManifest, streams),
declaredOf(t, tellerManifest, streams), declaredOf(t, watchmanManifest, streams),
}}})
runner := all["runner@novox"]
buckets := map[string][]string{
"mesh-delivery-forge-bridge": {"heads"}, "mesh-delivery-indexer": {"candidates", "modules"},
"mesh-delivery-teller": {"answers"}, "mesh-delivery-watchman": {"presence", "pulse", "stuck"},
}
for part, names := range buckets {
own, there := all[part+"@novox"]
if !there {
t.Errorf("%s has no credential of its own", part)
continue
}
for _, name := range names {
if canWrite(runner, part+"_"+name, "some.key") {
t.Errorf("novox's tool runner may write %s_%s", part, name)
}
if !canWrite(own, part+"_"+name, "some.key") {
t.Errorf("%s may not write its own bucket %s", part, name)
}
}
}
for _, subject := range []string{
"mesh.stream.mesh-delivery-forge-bridge.event.forge.merged",
"mesh.seat.operator-channel.accept.ask.mesh-delivery-teller",
"mesh.seat.operator-channel.accept.cancel.mesh-delivery-teller",
"mesh.seat.operator-channel.accept.condition.mesh-delivery-teller",
"$JS.API.CONSUMER.MSG.NEXT.FORGE.novox_mesh-delivery-indexer",
} {
if allowed(runner.Publish, subject) {
t.Errorf("novox's tool runner may publish %s", subject)
}
}
if !allowed(all["mesh-delivery-forge-bridge@novox"].Publish, "mesh.stream.mesh-delivery-forge-bridge.event.forge.merged") ||
!allowed(all["mesh-delivery-teller@novox"].Publish, "mesh.seat.operator-channel.accept.ask.mesh-delivery-teller") ||
!allowed(all["mesh-delivery-indexer@novox"].Publish, "$JS.API.CONSUMER.MSG.NEXT.FORGE.novox_mesh-delivery-indexer") {
t.Error("a part's own credential lost what its manifest grants")
}
}
// 1329/AC2: a module with tools and a broker secret of its own stays carried by the tool runner, beside its own
// credential (ADR 0175); a module with neither tools, a seat, waits nor a check is not.
func TestTheToolRunnerStillCarriesAModuleWithToolsAndItsOwnCredential(t *testing.T) {
process := Declared{Module: "only-process", State: []Bucket{{Module: "only-process", Name: "mine", History: 1}}}
all := composed(t, Records{Nodes: []string{"novox"}, Assigned: map[string][]Declared{"novox": {
{Module: RuntimeModule}, toolServer(), process,
}}})
runner := all["runner@novox"]
if !canWrite(runner, "some-tools_kept", "k") {
t.Error("novox's tool runner does not carry a module with tools")
}
if !canWrite(all["some-tools@novox"], "some-tools_kept", "k") {
t.Error("the module with tools lost its own credential")
}
if canWrite(runner, "only-process_mine", "k") {
t.Error("novox's tool runner carries a module that has nothing to serve")
}
if !canWrite(all["only-process@novox"], "only-process_mine", "k") {
t.Error("the process module lost its own credential")
}
for name, d := range map[string]Declared{
"a held seat": {Module: "m", Holds: []Seat{{Name: "some-seat"}}},
"a wait": {Module: "m", Waits: []Wait{{}}},
"a check": {Module: "m", Checks: []string{"m.check"}},
} {
if !runnerServes(d) {
t.Errorf("the tool runner does not serve a module with %s", name)
}
}
}
+1 -1
View File
@@ -154,7 +154,7 @@ func TestTheToolRunnerNeverHearsARestrictedStream(t *testing.T) {
records := Records{Nodes: []string{"ace", "novox"}, Assigned: map[string][]Declared{
"novox": {{Module: "mesh-mail", Streams: []ModuleStream{mailStream}}, {Module: "news", Streams: []ModuleStream{newsStream}}},
"ace": {{Module: RuntimeModule},
{Module: "carried", Hears: []Heard{mailHeard, newsHeard}, HearsAStream: true},
{Module: "carried", NoAccount: true, Hears: []Heard{mailHeard, newsHeard}, HearsAStream: true},
{Module: "mailer", RunsAs: "mailer", Hears: []Heard{mailHeard}, HearsAStream: true}},
}}
users, err := Users(records)
+12 -1
View File
@@ -206,7 +206,7 @@ func Users(r Records) ([]Principal, error) {
if runtimeHere {
var carried []Declared
for _, d := range r.Assigned[node] {
if d.RunsAs == "" {
if d.RunsAs == "" && runnerServes(d) {
// Never a restricted stream for what the tool runner carries (novox/hq ADR 0376 §1).
d.Hears = ComposedHears(d)
carried = append(carried, d)
@@ -391,3 +391,14 @@ func boundBucketsOf(r Records) []string {
sort.Strings(out)
return out
}
// 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
// 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
}