From 69a0004fe6dfd9c8e7524d1119a16bd533975451 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 05:32:04 +0100 Subject: [PATCH] EE-D2: the tool runner's runtime hears every event subject MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Where the runtime's permissions are composed, a runtime carrying the tool runner subscribes mesh-sdk's EveryEvent (every seat's, module's and module stream's events) and nothing else for it, replacing the per-family list. Builds: design 34 every-event §6, §8, §25, §27 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../internal/buscompose/buscompose_test.go | 69 +++++++++++++++++++ .../mesh-grants/internal/buscompose/nats.go | 7 ++ .../mesh-grants/internal/buscompose/users.go | 2 +- 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/modules/mesh-grants/internal/buscompose/buscompose_test.go b/modules/mesh-grants/internal/buscompose/buscompose_test.go index 9a623ad2..cebb8e9a 100644 --- a/modules/mesh-grants/internal/buscompose/buscompose_test.go +++ b/modules/mesh-grants/internal/buscompose/buscompose_test.go @@ -20,6 +20,7 @@ import ( "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep" "git.novox.be/novox/mesh-sdk/go/announce" + sdkbc "git.novox.be/novox/mesh-sdk/go/buscompose" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/seatdef" "github.com/nats-io/nats.go/jetstream" @@ -60,6 +61,7 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) { t.Run("a module's ledger is written by its ledger account alone", aLedgerIsWrittenByItsLedgerAccountAlone) t.Run("the bus module's process alone keeps every module's bus state and serves the broker's verbs", theBusModulesProcessKeepsBusState) t.Run("a process serving a verb is granted its calls and chunk subjects, and the reverse goes to an account naming it", chunkSubjectsFollowTheAnswerer) + t.Run("the tool runner's runtime hears every event subject and nothing wider", theToolRunnerHearsEveryEvent) // ADR 0567 §3, §9: a node reads every key of the seat's assignments and its candidates (it composes with the whole // proposal laid over), and none of its open proposals; it writes neither. reads := strings.Join(NodeComposeReads("laptop"), " ") @@ -1447,3 +1449,70 @@ func chunkSubjectsFollowTheAnswerer(t *testing.T) { t.Errorf("the keeper's restore grant reaches a stream that is not restricted") } } + +// EE-D2 (design 34 every-event §6, §8, §25, §27; ADR 0670 §1, §3, ADR 0671 §1): the runtime carrying the tool runner +// subscribes the three event subjects of mesh-sdk's EveryEvent, beside a module with a restricted stream, and nothing +// wider; it publishes no other module's or seat's event and derives no durable consumer or acknowledgement on that +// stream; a runtime carrying no tool runner gets none of the three. +func theToolRunnerHearsEveryEvent(t *testing.T) { + shop := func() Declared { + return Declared{Module: "shop", Serves: []string{"sell"}, Emits: []string{"sold"}, + Streams: []ModuleStream{{Module: "shop", Name: "LEDGER", Events: []string{"booked"}, Restricted: true}}} + } + r := Records{Nodes: []string{"a", "b"}, Assigned: map[string][]Declared{ + "a": {{Module: "node-tools", ToolRunner: true}, shop()}, + "b": {shop()}}} + users, err := Users(r) + if err != nil { + t.Fatal(err) + } + every := sdkbc.EveryEvent() + if len(every) != 3 { + t.Fatalf("EveryEvent answers %v, want three subjects", every) + } + wider := []string{">", "mesh.>", "mesh.*.>", "mesh.seat.>", "mesh.mod.>", "mesh.stream.>"} + runners := 0 + for _, u := range users { + p, err := PermissionsFor(u) + if err != nil { + t.Fatalf("%s: %v", u.Username(), err) + } + runs := false + for _, d := range u.Carries { + runs = runs || d.ToolRunner + } + for _, e := range every { + if slices.Contains(p.Subscribe, e) != runs { + t.Errorf("%s (tool runner %v) subscribes %s: %v", u.Username(), runs, e, !runs) + } + } + for _, s := range p.Subscribe { + if slices.Contains(wider, s) { + t.Errorf("%s subscribes %s, wider than every event", u.Username(), s) + } + } + if !runs { + continue + } + runners++ + for _, s := range p.Subscribe { + if strings.HasPrefix(s, "$JS.") && strings.Contains(s, "LEDGER") { + t.Errorf("the tool runner's runtime hears %s on the restricted stream", s) + } + } + for _, s := range []string{"mesh.mod.other.event.x", "mesh.seat.mesh-catalog.event.manifest-accepted", + "mesh.stream.other.event.x"} { + if MayPublish(p, s) { + t.Errorf("the tool runner's runtime may publish another's event %s", s) + } + } + for _, s := range p.Publish { + if strings.Contains(s, "LEDGER") || strings.HasPrefix(s, "$JS.ACK.") { + t.Errorf("the tool runner's runtime may %s on the restricted stream", s) + } + } + } + if runners != 1 { + t.Errorf("%d runtimes carry the tool runner, want 1", runners) + } +} diff --git a/modules/mesh-grants/internal/buscompose/nats.go b/modules/mesh-grants/internal/buscompose/nats.go index 679a17ad..12fd9c8d 100644 --- a/modules/mesh-grants/internal/buscompose/nats.go +++ b/modules/mesh-grants/internal/buscompose/nats.go @@ -1192,6 +1192,13 @@ func permissionsFor(p Principal) (Permissions, error) { pub = append(pub, logGrants(d.Module, logNames(d.Logs))...) pub = append(pub, readLogGrants(d.ReadLogs)...) } + // **And every event subject** (design 34 every-event §6, §25; novox/hq ADR 0670 §1, ADR 0671 §1): the runtime + // carrying the tool runner hears every seat's, module's and module stream's events, a restricted stream + // included, as a plain subscription: no publish, no durable consumer, no acknowledgement and no deny is added + // for it, and a runtime carrying no tool runner gets none of them. + if slices.ContainsFunc(p.Carries, func(d Declared) bool { return d.ToolRunner }) { + sub = append(sub, sdkbc.EveryEvent()...) + } // **Never the traffic of a trusted holder** (novox/hq ADR 0259 §8): the machine's runtime runs as the // operator's account, which every agent runs as, so a module saying warrants or speaking for a kind // that proves its sender is never composed into it — refused here, naming it, whatever registration diff --git a/modules/mesh-grants/internal/buscompose/users.go b/modules/mesh-grants/internal/buscompose/users.go index 249bfc79..c861f9d2 100644 --- a/modules/mesh-grants/internal/buscompose/users.go +++ b/modules/mesh-grants/internal/buscompose/users.go @@ -589,7 +589,7 @@ func Composed(d Declared) bool { return d.Module != EngineModule } func CarriedByRunner(d Declared) bool { return Composed(d) && d.RunsAs == "" && runnerServes(d) } // 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, +// ADR 0437 §4, issue 1329): tools or loads it answers, a seat it holds, // 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