From ac9c2d57be33ecd65fa324ad89bce2a74fbba69a Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 3 Oct 2026 22:30:55 +0200 Subject: [PATCH] The node's runtime reads the consumers of the modules it carries (hq ADR 0198) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A module's long-running code is a bundle the runtime launches, and the runtime is its bus: it binds the module's own durable consumer — EVENTS, _, still the controller's to make from the module's principal — and acknowledges what the module's code took. So the runtime principal is granted, for each carried module that consumes, exactly what that module's own principal has for its consumer: its info, its next message, its ack subject. Nothing is pushed to it; it pulls. ADR 0175's "consumes nothing" no longer holds. Memberships need nothing new: the consumer's name is derived, as the module's own runtime derived it. --- internal/broker/nats.go | 20 ++++++++++++++++++-- internal/broker/nats_test.go | 27 +++++++++++++++++++-------- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/internal/broker/nats.go b/internal/broker/nats.go index c5c01c8..86ecace 100644 --- a/internal/broker/nats.go +++ b/internal/broker/nats.go @@ -469,8 +469,24 @@ func PermissionsFor(p Principal) (Permissions, error) { // request once, so the runtime announces everything it carries under its own name. sub = append(sub, announcing(append([]string{RuntimeModule}, serves...)...)...) pub = append(pub, discovering()...) - // Nothing about consumers: it consumes nothing. A module's reactions to events are its - // own long-lived process, which ADR 0175 leaves where it is; what moves here is tools. + // **And it consumes for the modules it carries** (novox/hq ADR 0198, which changes ADR 0175's + // "it consumes nothing"): a module's long-running code is a bundle this runtime launches, and + // the runtime is its bus — it reads the module's own durable consumer and acknowledges what + // the module's code took. Exactly the grants the module's own principal has for that consumer, + // on its name and no other's: asking about it, pulling from it, acknowledging it. The + // consumer is still the controller's to make, from the module's own principal. + for _, d := range p.Carries { + own := Principal{Kind: KindModule, Node: p.Node, Module: d.Module, Emits: d.Emits, + Consumes: d.Consumes, Serves: d.Serves, Holds: d.Holds, Uses: d.Uses, Watches: d.Watches} + if _, consumes := ConsumerFor(own); !consumes { + continue + } + stream, durable := consumerStream(own), consumerDurable(own) + pub = append(pub, + "$JS.API.CONSUMER.INFO."+stream+"."+durable, + "$JS.API.CONSUMER.MSG.NEXT."+stream+"."+durable, + "$JS.ACK."+stream+"."+durable+".>") + } sub = unique(sub) pub = unique(pub) } diff --git a/internal/broker/nats_test.go b/internal/broker/nats_test.go index dad5ed9..404919b 100644 --- a/internal/broker/nats_test.go +++ b/internal/broker/nats_test.go @@ -378,7 +378,7 @@ func TestAModulePullsItsOwnConsumerAndNoOthers(t *testing.T) { // their tools (novox/hq ADR 0175): every carried module's tool namespace, every held seat's verbs // on this node, every module's membership on this node, and a call to anything. Nothing it // consumes, because it reacts to nothing. -func TestTheRuntimeServesTheUnionAndConsumesNothing(t *testing.T) { +func TestTheRuntimeServesTheUnionAndConsumesForItsModules(t *testing.T) { filter := Seat{Name: "node-packet-filter", Scope: "node", Serves: []string{"rules", "reload"}} p := Principal{Kind: KindNodeTools, Node: "anchor", Module: RuntimeModule, Carries: []Declared{ {Module: "nftables", Holds: []Seat{filter}, Serves: []string{"firewall_rules"}}, @@ -408,22 +408,33 @@ func TestTheRuntimeServesTheUnionAndConsumesNothing(t *testing.T) { t.Errorf("the runtime may not publish %s: %v", want, perms.Publish) } } - // Nothing of what a carried module consumes, and no consumer of its own to ack. - for _, s := range perms.Subscribe { - if strings.Contains(s, ".event.") || strings.HasPrefix(s, "_DELIVER.") { - t.Errorf("the runtime was granted a delivery it has no consumer for: %s", s) + // It reads the consumer of every carried module that consumes — that module's, by its name, as + // the module's own principal could (novox/hq ADR 0198) — and of no module that consumes nothing. + for _, want := range []string{ + "$JS.API.CONSUMER.INFO.EVENTS.anchor_zsh", + "$JS.API.CONSUMER.MSG.NEXT.EVENTS.anchor_zsh", + "$JS.ACK.EVENTS.anchor_zsh.>", + } { + if !contains(perms.Publish, want) { + t.Errorf("the runtime may not read zsh's consumer: %s missing from %v", want, perms.Publish) } } for _, s := range perms.Publish { - if strings.HasPrefix(s, "$JS.ACK.") || strings.Contains(s, "CONSUMER") { - t.Errorf("the runtime was granted a consumer's subject and has no consumer: %s", s) + if (strings.HasPrefix(s, "$JS.ACK.") || strings.Contains(s, "CONSUMER")) && !strings.Contains(s, "anchor_zsh") { + t.Errorf("the runtime was granted a consumer no carried module of it consumes on: %s", s) + } + } + // It pulls; nothing is pushed to it, and it subscribes no event subject directly. + for _, s := range perms.Subscribe { + if strings.Contains(s, ".event.") || strings.HasPrefix(s, "_DELIVER.") { + t.Errorf("the runtime was granted a delivery: %s", s) } } if !perms.AllowResponses { t.Error("the runtime answers what it is asked, and may not reply") } if _, needed := ConsumerFor(p); needed { - t.Error("a consumer would be made for the runtime, which consumes nothing") + t.Error("a consumer would be made for the runtime itself; it reads its modules' consumers, never one of its own") } // Each subject once in each list: the file is read as the mesh's authority model. One subject may // stand in both — the runtime answers discovery on `$SRV.INFO` and, as the console, asks it