diff --git a/modules/mesh-delivery-indexer/judge/rules.go b/modules/mesh-delivery-indexer/judge/rules.go index 37dd9f2..fa3ef9e 100644 --- a/modules/mesh-delivery-indexer/judge/rules.go +++ b/modules/mesh-delivery-indexer/judge/rules.go @@ -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 { diff --git a/modules/mesh-delivery-indexer/judge/rules_test.go b/modules/mesh-delivery-indexer/judge/rules_test.go index 1d6c599..75bd736 100644 --- a/modules/mesh-delivery-indexer/judge/rules_test.go +++ b/modules/mesh-delivery-indexer/judge/rules_test.go @@ -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.