Let a bus module's tools bundle declare what it invokes (issue 774)
mesh/delivery ready: it is merged on the Approve
mesh/check failed (2 tests): integration: mesh-mail-reader/cmd/mesh-mail-reader.TestAcceptanceOneMatteringMailSendsExactlyOneTellNamingIt: timed out w…
mesh/delivery-group group feat/l3-enrol rejected: composed together: the checker's verdict at the members' heads (3820753740cd): integration: mesh-mail-reader/…

The judge refused nats's manifest for its invokes (seat:mesh-hand-acts.hand-act,
which nats_bucket_expiry asks to record its hand act), and with one manifest
refused the grants part holds the whole bus user list, freezing every build
that adds bus permissions. The bundle asks through the node's tool runner,
which buscompose grants every tool and seat verb, so the invokes are granted:
the same condition that already lets the bundle declare emits (ADR 0357).
ADR 0235's rule stands for a bus module with no bundle.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-02 02:21:05 +01:00
parent b13f65674b
commit 63c8687166
2 changed files with 46 additions and 6 deletions
+11 -6
View File
@@ -2450,19 +2450,24 @@ func parseManifest(raw []byte, predefined catalogue.PredefinedList) (catalogue.M
// holding mesh-broker is granted the snapshot API and nothing else, so anything it declared to say
// or hear on the bus would be granted nothing — refused here rather than silently dropped.
//
// **But its tools bundle may say what it declares** (novox/hq ADR 0357): the node's runtime carries
// the bundle and is granted the events the module emits, under the module's name, as for any module it
// carries — the bus's account is not who publishes them. So emits are refused only for a bus module
// with no bundle to say them.
// **But its tools bundle may say and ask what it declares** (novox/hq ADR 0357): the node's runtime carries
// the bundle, is granted the events the module emits under the module's name, as for any module it carries,
// and asks every tool and seat verb on the bundle's behalf (mesh-sdk go/buscompose, the runtime's invoke of
// `*`) — the bus's account is not who publishes or asks. So emits and invokes are refused only for a bus
// module with no bundle to say or ask them (issue 774: nats's bundle records its hand act).
if _, account := m.OwnSecrets["broker"]; account && m.ClaimsSeat(catalogue.BrokerSeat) {
var said []string
if len(m.EmitsAll()) > 0 && !manifestShipsABundle(m) {
bundled := manifestShipsABundle(m)
if len(m.EmitsAll()) > 0 && !bundled {
said = append(said, "emits")
}
if len(m.Invokes) > 0 && !bundled {
said = append(said, "invokes")
}
for _, f := range []struct {
name string
n int
}{{"consumes", len(m.Consumes)}, {"uses", len(m.Uses)}, {"invokes", len(m.Invokes)},
}{{"consumes", len(m.Consumes)}, {"uses", len(m.Uses)},
{"state", len(m.State)}, {"logs", len(m.Logs)}, {"reads", len(m.Reads)},
{"streams", len(m.Streams)}} {
if f.n > 0 {
@@ -4337,6 +4337,41 @@ func TestTheBussAccountSaysNothingOnTheBus(t *testing.T) {
if err == nil || !strings.Contains(err.Error(), "granted the bus's snapshot API and nothing else") {
t.Fatalf("a bus module declaring what it emits was not refused, or not for the reason: %v", err)
}
// Nor what it invokes, with no bundle for the runtime to ask on its behalf.
unbundledInvoke := []byte(`{"module":"bus","version":"1","provides":[{"name":"mesh-bus","scope":"mesh"}],
"claims":[{"name":"mesh-broker","scope":"mesh"}],"own-secrets":{"broker":"/run/broker"},
"invokes":["seat:mesh-hand-acts.hand-act"]}`)
if _, err := ParseManifest(unbundledInvoke); err == nil || !strings.Contains(err.Error(), "its invokes would be granted nothing") {
t.Fatalf("a bus module with no bundle declaring what it invokes was not refused, or not for the reason: %v", err)
}
// Its tools bundle asks what the module invokes (novox/hq ADR 0357; issue 774): the bundle asks through the
// node's runtime, which is granted every tool and seat verb (mesh-sdk go/buscompose), so a bus module that
// ships one may declare them. The shape of mesh-catalog's modules/nats/module.json at fc5b849d, cut to the
// parts the rule reads, was refused and held every bus user list from 2026-11-02 01:35.
nats := []byte(`{"module":"nats","version":"1","provides":[{"name":"mesh-bus","scope":"mesh","credential":{"form":"none"}}],
"claims":[{"name":"mesh-broker","scope":"mesh"}],
"own-secrets":{"broker":"/run/broker","broker-nats-tools":"${dir:tools-secret}/broker-nats-tools"},
"resources":[{"id":"tools-secret","type":"directory","path":"/var/lib/mesh/nats-tools","mode":"0711","unknown":"program"}],
"emits":["consumer-removed","bucket-keys-removed","stream-removed","bucket-expiry-set"],
"invokes":["seat:mesh-hand-acts.hand-act"],"consumes":[],
"tools":["nats_server","nats_bucket_expiry"],
"build":{"artifacts":[{"name":"tools","kind":"bundle","language":"go","system":"arch","from":"cmd/nats-tools",
"binary":"nats-tools","loads":["nats-tools"],"bus":{"removes":["DEAD_LETTER_NOTICES.controller","ASSIGNMENTS"]}}]}}`)
nm, err := ParseManifest(nats)
if err != nil {
t.Fatalf("the bus module whose tools bundle invokes a seat verb was refused: %v", err)
}
nbuilt, err := nm.Resolve([]catalogue.Built{{Name: "tools", Kind: catalogue.ArtifactBundle, Reference: "store/tools:1", Digest: "sha256:1"}})
if err != nil {
t.Fatal(err)
}
if raw, err = json.Marshal(nbuilt); err != nil {
t.Fatal(err)
}
if _, err := ParseManifestForInputs(raw); err != nil {
t.Fatalf("the built manifest of the bus module whose tools bundle invokes a seat verb was refused: %v", err)
}
// Its tools bundle says what it emits (novox/hq ADR 0357): the node's runtime carries the bundle and is
// granted the module's events, so a bus module that ships one may declare them.