Files
hq/02-DECISIONS/0048-a-module-broker-account-is-scoped-by-emits-and-consumes.md
T

6.0 KiB

status, date, deciders, reconstructed, extends
status date deciders reconstructed extends
accepted 2026-09-04 jochen false 0046-events-are-a-relationship.md

48. A module's broker account is scoped by what it emits and consumes

Context

ADR 0046 made events a relationship — emits and consumes on the manifest. ADR 0047 gave them a wire shape — the mesh.events exchange, the durable per-consumer queue, the reserved routing-key origins. Neither said how a module reaches the broker: what account it holds, and what that account is allowed to do.

As the code stands, there is no answer. The mesh can provision a node account (at enrolment) and a builder account (scoped to the build queue), and it can deliver any module a sealed own-secret at a declared path — but it has no way to provision a broker account for a general module. A module that declares own-secrets: {broker: …} and nothing more receives thirty-two random bytes, not a credential. So on the broker, emits and consumes are enforced by nothing: a running module could bind any queue, consume any pattern, and publish under any origin, and the manifest that says otherwise would be describing a boundary no code draws — the exact shape of fault 04-ISSUES/003 records, a scope declared in manifests and read by nothing.

This settles it, so a module's place on the bus is a thing the broker enforces rather than a thing the manifest merely claims.

Decision

A module gets a broker account when it is assigned, and its permissions are the manifest

When the mesh assigns a module to a node it provisions a broker account for that module on that node, sealed to the node (ADR 0039) and delivered as the module's own-secrets broker — amqps:// with the mesh's fingerprint, the shape ADR 0047 already carries. The account's permissions are derived from the manifest, and are exactly these:

  • What it consumes. Read on mesh.events, and configure-and-read on its own queue <node>.<module>.events bound to the patterns in consumes. It cannot bind or read another module's queue. A module that consumes nothing gets no read on the events exchange at all.
  • What it emits. Write to mesh.events, restricted to routing keys under its own origin, module.<name>.*. It cannot publish as another module, and cannot publish under the reserved mesh.* or node.* origins — those belong to the mesh and the host (ADR 0047). A module that emits nothing gets no write.
  • Nothing else. The events account reaches mesh.events and that module's own queue, and no more. Tool serving and calling over mesh.rpc is a separate grant on the same principle — a module serves the tool keys it declares and calls the ones it is bound to — and is scoped the same way rather than folded in here.

Consuming everything is a privilege, granted deliberately

consumes: ["#"] — the audit logger — is read across the whole bus: every module's events, the mesh's, every node's. That is not a pattern like any other; it is the power to see everything, and the account is where it becomes visible. The grant that lets one module read the entire bus is one the mesh issues on purpose and can be audited — the answer to who can read everything is a row, not a guess — rather than a breadth any manifest acquires by typing a single character. A # consume is a reviewed grant, not a default one.

The account is how the declaration is enforced

Because the account can do only what emits and consumes name, the broker itself refuses a module that tries to consume a queue it did not declare or emit under an origin it does not own. That is what makes an event relationship a rule and not a comment — the discipline that a stated rule says how it is checked. A manifest that over-declares grants more than the module uses, which is visible and reviewable; one that under-declares makes the module fail closed at the broker, which is the safe direction to be wrong in.

Consequences

  • The control plane gains a generic module broker-account, derived from the manifest. The builder stops being a special case: its access to the build queue becomes an ordinary expression of what it consumes and serves, not a bespoke account method. One rule, and the builder is an instance of it.
  • The runtime reads its credential from a file (the broker own-secret), amqps:// verified against the mesh's fingerprint. The guest account is for raising the substrate, never for a module — a module documented as holding its own credential and handed the broker's administrative one is worse than one with no credential story at all.
  • emits and consumes stop being advisory. They are the module's authority on the bus, so the manifest is now a security boundary and is reviewed as one, the discipline ADR 0043 applies to the declaration vocabulary.
  • Who can read the whole bus becomes an answerable question, because # is a grant and not an accident.

References

  • ADR 0046 — events are a relationship; this scopes the account by that relationship.
  • ADR 0047 — the wire this account secures: the queue, the origins, the amqps credential shape.
  • ADR 0039 — the link is the security boundary; a module's account is sealed to its node the same way a node's is.
  • ADR 0043 — a declaration is owned and its additions reviewed; a module's broker permissions are that discipline applied to the bus.
  • 04-ISSUES/003 — a scope declared in manifests and enforced by no code: the fault this decision closes for events.