diff --git a/02-DECISIONS/0048-a-module-broker-account-is-scoped-by-emits-and-consumes.md b/02-DECISIONS/0048-a-module-broker-account-is-scoped-by-emits-and-consumes.md new file mode 100644 index 0000000..583cc57 --- /dev/null +++ b/02-DECISIONS/0048-a-module-broker-account-is-scoped-by-emits-and-consumes.md @@ -0,0 +1,99 @@ +--- +status: proposed +date: 2026-09-04 +deciders: jochen +extends: 0046-events-are-a-relationship.md +--- + +# 48. A module's broker account is scoped by what it emits and consumes + +## Context + +[ADR 0046](0046-events-are-a-relationship.md) made events a relationship — `emits` and `consumes` +on the manifest. [ADR 0047](0047-the-shape-of-an-event-on-the-wire.md) 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](../04-ISSUES/003-firewall-scope-is-read-by-no-code/00-report.md) 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](0039-the-link-is-the-security-boundary.md)) and delivered as the +module's `own-secrets` broker — `amqps://` with the mesh's fingerprint, the shape +[ADR 0047](0047-the-shape-of-an-event-on-the-wire.md) 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 + `..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..*`. 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](0043-a-declaration-is-an-ordered-list-of-owned-resources.md) 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](0046-events-are-a-relationship.md) — events are a relationship; this scopes the account + by that relationship. +- [ADR 0047](0047-the-shape-of-an-event-on-the-wire.md) — the wire this account secures: the queue, + the origins, the `amqps` credential shape. +- [ADR 0039](0039-the-link-is-the-security-boundary.md) — the link is the security boundary; a + module's account is sealed to its node the same way a node's is. +- [ADR 0043](0043-a-declaration-is-an-ordered-list-of-owned-resources.md) — a declaration is owned + and its additions reviewed; a module's broker permissions are that discipline applied to the bus. +- [04-ISSUES/003](../04-ISSUES/003-firewall-scope-is-read-by-no-code/00-report.md) — a scope declared + in manifests and enforced by no code: the fault this decision closes for events.