Files
hq/02-DECISIONS/0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md
T

5.0 KiB

topic, status, date, deciders, reconstructed, extends
topic status date deciders reconstructed extends
what runs on it accepted 2026-09-04 jochen false 0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md

47. A module runs its code as its own process, with its own account

Context

A module is one self-contained thing (ADR 0040), and it gets a broker account scoped to what it emits and consumes (ADR 0043). The catalogue now gives modules tools and events — real code, in the module (ADR 0039) — 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 0043 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 0043). One module, one process, one account.

It serves its tools, each on its own key

A tool is served on its own key (serve.<tool>), 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 0043 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 0042) 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 0043) 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 0043'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.<tool> 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 0040 — a module is one self-contained thing; its code runs as one process.
  • ADR 0043 — the scoped account this process holds, and the isolation that makes it per-module.
  • ADR 0042 — the events this process runs, and the serve.<key> queue tools now use.
  • ADR 0039 — the code lives in the module; this runs it.