From f63eca13b3220bc09af928fe052f09ff5353b720 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 4 Sep 2026 21:24:46 +0200 Subject: [PATCH] =?UTF-8?q?ADR=200052=20=E2=80=94=20a=20module=20runs=20it?= =?UTF-8?q?s=20code=20as=20its=20own=20process,=20with=20its=20own=20accou?= =?UTF-8?q?nt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The runtime-model gap the review found. A module with tools or events runs one container — the tool runtime carrying its code — holding the one scoped account ADR 0048 gave it. A node-wide runtime can't: it would hold the union of every module's permissions, the isolation 0048 draws. So per-module: one module, one process, one account. Tools served per key (serve.) so a caller names a tool and only its module answers (superseding a shared tools.invoke); events in the same process under the same account; the runtime image is the tool runtime plus the module's code (the audit-logger's shape, made the rule). A plain service module runs no such process. A provider's provisioner is a runtime too — which is why a provisioner that emits must carry a broker credential or not emit. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF --- ...as-its-own-process-with-its-own-account.md | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 02-DECISIONS/0052-a-module-runs-its-code-as-its-own-process-with-its-own-account.md 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.