--- status: accepted date: 2026-09-04 deciders: jochen reconstructed: false extends: 0048-a-module-broker-account-is-scoped-by-emits-and-consumes.md --- # 52. A module runs its code as its own process, with its own account ## Context A module is one self-contained thing ([ADR 0045](0045-what-a-module-is.md)), and it gets a broker account scoped to what it emits and consumes ([ADR 0048](0048-a-module-broker-account-is-scoped-by-emits-and-consumes.md)). The catalogue now gives modules **tools** and **events** — real code, in the module ([ADR 0044](0044-what-the-sdk-holds-and-refuses.md)) — but nothing has said what *runs* that code. The audit-logger showed one shape and was treated as an exception: a container running the tool runtime carrying the module's compiled code, holding the module's own scoped account. Every module with tools or events needs the same, and the tempting alternative does not work. **A node-wide runtime that loaded every assigned module's code cannot hold a per-module account.** It would run under one account with the union of every module's permissions — able to emit as any of them and read any of their queues — which is exactly the isolation ADR 0048 exists to draw. So the runtime is per-module, not per-node, and treating the audit-logger as special left the other modules' code with nothing to run it: the conversion produced tools and events that, as it stands, never execute. ## Decision ### A module with tools or events runs a process of its own A module that has tools or events runs a **runtime process** — a container, the tool runtime carrying that module's compiled code — assigned and started like the module it is, holding the single broker account the mesh scoped to it (ADR 0048). One module, one process, one account. ### It serves its tools, each on its own key A tool is served on its own key (`serve.`), and a caller invokes a named tool. Only the module that serves it answers, and the module's account is scoped to exactly its tool keys — so one module cannot answer another's calls, the isolation ADR 0048 gives events extended to tools. This supersedes a single `tools.invoke` endpoint that dispatched by name: that shape assumed one runtime for the whole node, and per-module runtimes competing on one key would each be handed calls for tools they do not have. ### It runs its events in the same process, under the same account Emitting under the module's own origin and consuming its own queue ([ADR 0047](0047-the-shape-of-an-event-on-the-wire.md)) happen in that same process, with that same account — not a second one to scope and seal. A module's tool code, its event code and, for a provider, its provisioner are the one module's code and run as the one module's process. ### The runtime image is the tool runtime plus the module's code Built from the module's source like any module image — the audit-logger's shape, made the rule, not the exception. The module declares a `container` for it carrying `MESH_BROKER_FILE` (its sealed credential, ADR 0048) and its compiled code. A module with **neither** tools nor events runs no such process: a plain service module — the plex *server*, dnsmasq the resolver — is its service and files and nothing more. A module that is both a service and code declares both containers: the service, and the runtime beside it. ## Consequences - The catalogue's tools and events become runnable: each tools-or-events module gains a runtime container with its scoped credential, and the audit-logger stops being special. Until this, the converted modules held code with nothing to execute it. - A process, and a small image, per tools-or-events module. That is the cost of ADR 0048's isolation: one account per module means one process per module. It is paid deliberately — a shared runtime is cheaper and cannot be scoped, and a mesh where any module can emit as any other is not one worth the saving. - `serve.` per key replaces the single `tools.invoke` dispatch. The sdk's serving and a module's account scope both come to name tools individually. - **A provider's provisioner is a runtime process too.** It already runs as its own container; its events (`bucket.created`, `database.provisioned`) belong to *that* process and need the same credential. So a provisioner that emits carries `MESH_BROKER_FILE` and its scoped account like any runtime — or it does not emit. (This is the fix for provisioners that emit today with no broker bound: the emit is a runtime's, and the provisioner is a runtime.) ## References - [ADR 0045](0045-what-a-module-is.md) — a module is one self-contained thing; its code runs as one process. - [ADR 0048](0048-a-module-broker-account-is-scoped-by-emits-and-consumes.md) — the scoped account this process holds, and the isolation that makes it per-module. - [ADR 0047](0047-the-shape-of-an-event-on-the-wire.md) — the events this process runs, and the `serve.` queue tools now use. - [ADR 0044](0044-what-the-sdk-holds-and-refuses.md) — the code lives in the module; this runs it.