Merge pull request 'Carry a module on the tool runner only when the runner serves something of it, so a part on its own credential has one writer (issues 1329, 1338)' (#404) from fix/1329-runner-carries-only-what-it-serves into main

This commit was merged in pull request #404.
This commit is contained in:
2026-10-19 23:39:24 +00:00
9 changed files with 260 additions and 16 deletions
+3 -1
View File
@@ -174,7 +174,9 @@ func TestAPlanGoesOnWithoutAMachineAwayWhenItsTierIsSent(t *testing.T) {
}} }}
first, stopFirst := context.WithCancel(ctx) first, stopFirst := context.WithCancel(ctx)
defer stopFirst() defer stopFirst()
startRolloutEvents(first, open, js) waitFirst := startRolloutEvents(first, open, js)
// Ends with the test before the cleanups restore what the look reads, which run after it (issue 1338).
t.Cleanup(func() { stopFirst(); waitFirst() })
if err := inv.SavePlan(ctx, &p); err != nil { if err := inv.SavePlan(ctx, &p); err != nil {
t.Fatal(err) t.Fatal(err)
} }
+15 -5
View File
@@ -138,7 +138,9 @@ func askToLook() {
// startPlanLooks starts the look at the open plans, and asks for the first: the controller took the lease, and a // startPlanLooks starts the look at the open plans, and asks for the first: the controller took the lease, and a
// plan left by a replaced controller resumes from there (ADR 0406 §6). It replaces the rollout's 30 second ticker. // plan left by a replaced controller resumes from there (ADR 0406 §6). It replaces the rollout's 30 second ticker.
func startPlanLooks(ctx context.Context, open *stores) { // It answers a channel that is closed once the look has returned after the context ended, for a test that restores what
// the look reads (issue 1338).
func startPlanLooks(ctx context.Context, open *stores) <-chan struct{} {
l := newLooker(func(ctx context.Context) bool { return lookAtPlans(ctx, open) }) l := newLooker(func(ctx context.Context) bool { return lookAtPlans(ctx, open) })
l.first = func(ctx context.Context) { rearmWalkEnds(ctx, open) } l.first = func(ctx context.Context) { rearmWalkEnds(ctx, open) }
lookerMu.Lock() lookerMu.Lock()
@@ -152,8 +154,13 @@ func startPlanLooks(ctx context.Context, open *stores) {
} }
lookerMu.Unlock() lookerMu.Unlock()
}() }()
go l.run(ctx) done := make(chan struct{})
go func() {
defer close(done)
l.run(ctx)
}()
l.ask() l.ask()
return done
} }
// applySilentAfter is how long a machine may say nothing on a send before the plan names it silent: the mesh's // applySilentAfter is how long a machine may say nothing on a send before the plan names it silent: the mesh's
@@ -163,8 +170,10 @@ var applySilentAfter = inventory.ApplySilentAfter
// startRolloutEvents starts what a rollout advances on, for as long as the context lives (novox/hq ADR 0406 §5, issue 974): // startRolloutEvents starts what a rollout advances on, for as long as the context lives (novox/hq ADR 0406 §5, issue 974):
// the wait for each machine's report as a key on the bus that expires (where the bus will not take it a timer of its own // the wait for each machine's report as a key on the bus that expires (where the bus will not take it a timer of its own
// for that wait looks instead), a provisioner client's words, and the look at the plans, asked for first so a plan left // for that wait looks instead), a provisioner client's words, and the look at the plans, asked for first so a plan left
// by a replaced controller resumes. The serving controller's start and the tests of a rollout call this one. // by a replaced controller resumes. The serving controller's start and the tests of a rollout call this one. It answers
func startRolloutEvents(ctx context.Context, open *stores, js *broker.JetStream) { // a function that waits until the look at the plans has returned, which only a test that ends the context needs (issue
// 1338): the serving controller ignores it.
func startRolloutEvents(ctx context.Context, open *stores, js *broker.JetStream) (wait func()) {
if err := startRolloutSilent(ctx, js); err != nil { if err := startRolloutSilent(ctx, js); err != nil {
fmt.Printf("plans: the wait for the machines' reports could not be put on the bus (%v); each is looked at by a timer of its own\n", err) fmt.Printf("plans: the wait for the machines' reports could not be put on the bus (%v); each is looked at by a timer of its own\n", err)
} }
@@ -174,7 +183,8 @@ func startRolloutEvents(ctx context.Context, open *stores, js *broker.JetStream)
if err := startPlanMovedLooks(ctx, js); err != nil { if err := startPlanMovedLooks(ctx, js); err != nil {
fmt.Printf("plans: the plans moved by other processes could not be followed (%v)\n", err) fmt.Printf("plans: the plans moved by other processes could not be followed (%v)\n", err)
} }
startPlanLooks(ctx, open) done := startPlanLooks(ctx, open)
return func() { <-done }
} }
// servingPlanSaved is what the serving controller does for each plan it keeps: said as plan-moved, looked at (a build, a // servingPlanSaved is what the serving controller does for each plan it keeps: said as plan-moved, looked at (a build, a
+9 -3
View File
@@ -179,7 +179,7 @@ func TestARolloutAdvancesOnEventsAndAReplacedControllerResumesIt(t *testing.T) {
// The first controller. Its plan has tier 0 built and tier 1 not asked: nothing but the save asks anything. // The first controller. Its plan has tier 0 built and tier 1 not asked: nothing but the save asks anything.
first, stopFirst := context.WithCancel(ctx) first, stopFirst := context.WithCancel(ctx)
defer stopFirst() defer stopFirst()
startRolloutEvents(first, open, js) waitFirst := startRolloutEvents(first, open, js)
built := time.Now().UTC() built := time.Now().UTC()
p := inventory.Plan{ID: "plan-974", Repository: "novox/mesh-catalog", Branch: "main", Commit: "c2", Created: built, p := inventory.Plan{ID: "plan-974", Repository: "novox/mesh-catalog", Branch: "main", Commit: "c2", Created: built,
State: inventory.PlanBuilding, Tiers: [][]string{{"a"}, {"b"}}, State: inventory.PlanBuilding, Tiers: [][]string{{"a"}, {"b"}},
@@ -199,12 +199,15 @@ func TestARolloutAdvancesOnEventsAndAReplacedControllerResumesIt(t *testing.T) {
// The controller is replaced: its looks and its watcher end. anchor reports meanwhile, to nobody. // The controller is replaced: its looks and its watcher end. anchor reports meanwhile, to nobody.
stopFirst() stopFirst()
waitFirst()
time.Sleep(300 * time.Millisecond) time.Sleep(300 * time.Millisecond)
reports("anchor") reports("anchor")
second, stopSecond := context.WithCancel(ctx) second, stopSecond := context.WithCancel(ctx)
defer stopSecond() defer stopSecond()
startRolloutEvents(second, open, js) waitSecond := startRolloutEvents(second, open, js)
// Ends with the test before the cleanups restore what the look reads, which run after it (issue 1338).
t.Cleanup(func() { stopSecond(); waitSecond() })
// Its first look finds anchor reported, so its key goes; laptop's stays, and is the new controller's to hear expire. // Its first look finds anchor reported, so its key goes; laptop's stays, and is the new controller's to hear expire.
eventually(t, "the new controller's first look deletes the key of the machine that reported", func() bool { eventually(t, "the new controller's first look deletes the key of the machine that reported", func() bool {
h := heldKeys() h := heldKeys()
@@ -321,7 +324,7 @@ func TestAWalkLetGoByTheDeliverVerbIsAskedWithNoOtherEvent(t *testing.T) {
t.Cleanup(func() { inventory.PlanSaved = wasSaved }) t.Cleanup(func() { inventory.PlanSaved = wasSaved })
started, stop := context.WithCancel(ctx) started, stop := context.WithCancel(ctx)
defer stop() defer stop()
startRolloutEvents(started, open, js) wait := startRolloutEvents(started, open, js)
time.Sleep(500 * time.Millisecond) // the first look: the walk waits, and is asked nothing time.Sleep(500 * time.Millisecond) // the first look: the walk waits, and is asked nothing
// The verb as its child process runs it: the delivery command, whose saves are not the serving controller's. // The verb as its child process runs it: the delivery command, whose saves are not the serving controller's.
@@ -333,6 +336,9 @@ func TestAWalkLetGoByTheDeliverVerbIsAskedWithNoOtherEvent(t *testing.T) {
return verbAnswer{}, deliveryCommand(ctx, []string{"go", waiting.ID, "--why", "its turn"}) return verbAnswer{}, deliveryCommand(ctx, []string{"go", waiting.ID, "--why", "its turn"})
} }
t.Cleanup(func() { runTheVerb = wasRun }) t.Cleanup(func() { runTheVerb = wasRun })
// Registered after every variable cleanup above, so it runs before them (last in, first out): the look at the plans
// has returned when checkEvents, inventory.PlanSaved and runTheVerb are restored (issue 1338).
t.Cleanup(func() { stop(); wait() })
handlers, _, err := seatToolHandlers() handlers, _, err := seatToolHandlers()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -171,7 +171,9 @@ func TestAPlanPassesATierWhoseMachineNeverReportsAfterTheBoundAndSendsTheNextToT
}} }}
first, stopFirst := context.WithCancel(ctx) first, stopFirst := context.WithCancel(ctx)
defer stopFirst() defer stopFirst()
startRolloutEvents(first, open, js) waitFirst := startRolloutEvents(first, open, js)
// Ends with the test before the cleanups restore what the look reads, which run after it (issue 1338).
t.Cleanup(func() { stopFirst(); waitFirst() })
if err := inv.SavePlan(ctx, &p); err != nil { if err := inv.SavePlan(ctx, &p); err != nil {
t.Fatal(err) t.Fatal(err)
} }
+2 -2
View File
@@ -67,8 +67,8 @@ func TestTheGatesBucketsAreWritableByItsMachinesToolRunner(t *testing.T) {
if !canRead(runner, read, "k") { if !canRead(runner, read, "k") {
t.Errorf("novox's tool runner may not read %s, which the gate declares reading", read) 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.) // 1329/AC3: the runner no longer carries the teller, so it may not write the teller's answers either.
if canWrite(runner, read, "k") && read != "mesh-delivery-teller_answers" { if canWrite(runner, read, "k") {
t.Errorf("novox's tool runner may write %s, which the gate only reads", read) t.Errorf("novox's tool runner may write %s, which the gate only reads", read)
} }
} }
+205 -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, // 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. // `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 // The four manifests (the watchman's is the fourth, issue 1329) are copied from mesh-delivery's main at 9c15112d935a
// 2026-10-19), whole, so a change to one of them there that changes its grant is a change to read here. // (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 = `{ const bridgeManifest = `{
"module": "mesh-delivery-forge-bridge", "module": "mesh-delivery-forge-bridge",
"version": "1", "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 = `{ const tellerManifest = `{
"module": "mesh-delivery-teller", "module": "mesh-delivery-teller",
"version": "1", "version": "1",
@@ -488,3 +552,142 @@ 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)
}
}
}
// 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)
}
}
}
+1 -1
View File
@@ -154,7 +154,7 @@ func TestTheToolRunnerNeverHearsARestrictedStream(t *testing.T) {
records := Records{Nodes: []string{"ace", "novox"}, Assigned: map[string][]Declared{ records := Records{Nodes: []string{"ace", "novox"}, Assigned: map[string][]Declared{
"novox": {{Module: "mesh-mail", Streams: []ModuleStream{mailStream}}, {Module: "news", Streams: []ModuleStream{newsStream}}}, "novox": {{Module: "mesh-mail", Streams: []ModuleStream{mailStream}}, {Module: "news", Streams: []ModuleStream{newsStream}}},
"ace": {{Module: RuntimeModule}, "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}}, {Module: "mailer", RunsAs: "mailer", Hears: []Heard{mailHeard}, HearsAStream: true}},
}} }}
users, err := Users(records) users, err := Users(records)
+16 -1
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 // 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. // 48 §3): the machine's node-engine asks them of its own node tools, and is granted that and no more.
Checks []string 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 // 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. // carried by the machine's runtime, and reaches the bus on its own account.
RunsAs string RunsAs string
@@ -206,7 +210,7 @@ func Users(r Records) ([]Principal, error) {
if runtimeHere { if runtimeHere {
var carried []Declared var carried []Declared
for _, d := range r.Assigned[node] { 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). // Never a restricted stream for what the tool runner carries (novox/hq ADR 0376 §1).
d.Hears = ComposedHears(d) d.Hears = ComposedHears(d)
carried = append(carried, d) carried = append(carried, d)
@@ -391,3 +395,14 @@ func boundBucketsOf(r Records) []string {
sort.Strings(out) sort.Strings(out)
return 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,
// 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 || 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). // name (novox/hq ADR 0305).
Issued: issuedTools(m), 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 // 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). // that declares none has nowhere to read it (novox/hq issue 195).
if _, reads := m.OwnSecrets["broker"]; !reads { if _, reads := m.OwnSecrets["broker"]; !reads {