diff --git a/02-DECISIONS/0052-a-module-runs-its-code-as-its-own-process-with-its-own-account.md b/02-DECISIONS/0052-a-module-runs-its-code-as-its-own-process-with-its-own-account.md new file mode 100644 index 0000000..86f532b --- /dev/null +++ b/02-DECISIONS/0052-a-module-runs-its-code-as-its-own-process-with-its-own-account.md @@ -0,0 +1,86 @@ +--- +status: proposed +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.