--- topic: what runs on it status: accepted date: 2026-10-02 deciders: jochen reconstructed: false extends: 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md --- # 175. One tool runtime per node serves every module's tools, on the host side ## Context A module's tools are code the module wrote, one function behind each verb, served on the subjects the controller issues in the module's membership ([ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)). What *runs* that code is [ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md): a supervised process per module under the module's own account, and in the catalogue as built, that process is a container per module per node, built on the tool runtime's base image. Measured on the live mesh ([research 018](../01-RESEARCH/018-the-operators-machine-as-modules/03-one-tool-executor-per-node.md)): 67 module tools, each served from its module's container; the packet-filter seat's three verbs served by a container with `NET_ADMIN` on every one of four machines, for a module that is otherwise a package, three files and a service; and the console, a container per node, calling everything and serving nothing. The operator's environment adds a dozen modules of the packet-filter shape, and the operator's judgement is plain: *I would never run MCP tools inside a container; that is a very bad design.* And: *I don't care about permissions or account per module, that just complicates things for no good reason. Just a node-level tool executor. If a command needs root, that's the module's concern.* The tool runtime itself was written for this. Its own description: *the per-node process that makes a module's tools actually serve — imports the assigned modules' compiled tool entrypoints, each of which registers its tools as it loads; on a node the host resolves the list and starts it like any other supervised workload.* What the catalogue did instead was build one image per module around it. ## Considered Options 1. **Keep a process per module.** Rejected: one container per module per node for software that is not a container, and the account-per-module invariant it exists to protect is one the operator declines to pay for. 2. **The host executes tools itself.** Rejected: the host is a static Go binary that loads no plugins; a module's tools are TypeScript on the SDK, and building a second SDK in Go for the host's sake is the cost ADR 0039 refuses. 3. **One tool runtime per node, a sibling of the host, loading every assigned module's bundle.** Chosen. It is what the runtime was written to be. ## Decision **1. One tool runtime per node, supervised by the host, on the host side — never a container.** The host starts it the way the launcher starts the host ([ADR 0005](0005-the-node-host.md)): a process on the machine, restarted when it dies. It holds one bus credential, the node's. It is module-agnostic: it knows bundles and subjects, nothing of what any module does. **2. It serves every assigned module's tools and every held seat's verbs** on the subjects the memberships issue. ADR 0159 and ADR 0160 are unchanged in what they say about subjects, grants and memberships; what changes is that one process on the node subscribes to all of them instead of one process per module. A module that runs a long-lived service of its own — a daemon, a container — keeps it; this record is about tools. **3. A module brings its tools as a bundle**, the artifact kind the catalogue already has for interpreted code, built by the pipeline and delivered to the node by the host as it delivers any artifact. Never an image. The runtime loads each bundle as the membership names it, and a push that adds or replaces a bundle reaches a running runtime as a reload. **4. Root is the module's concern.** A tool that must change the packet filter or rebuild boot images escalates itself. The runtime does not run as root for everyone's sake; the caller does not know and need not. **5. Any node may call any tool on any node.** The runtime's credential may call everything, as the console's already may. A per-module calling grant is not kept. **6. The console is this runtime's serving mode, renamed.** [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) stands in substance — a module assigned per node, MCP on the machine's loopback, the machine's login is the authority — and changes in form: host-side, serving as well as calling, and named for what it is: **node tools**. The mesh's own verbs stay with the controller ([ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)); a mesh-scoped seat's verbs run on the node that holds it ([ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)). **Where ADR 0047 and ADR 0150 say a module's tools are served by the module's own process under the module's own account, read this record.** Everything else they decided stands: a tool is served on its own subject, only the module that serves it answers, a module's long-lived processes are the machine's to supervise. The invariant 0150 kept — one account per module — no longer holds for tools, and the reason is stated above: every tool is callable from everywhere by decision 5, so the account no longer scopes anything a caller cannot already reach. ## Consequences - The packet-filter module's container goes; its verbs run on the host side and escalate as they need. [ADR 0170](0170-the-firewall-seat-serves-its-verbs.md) §3's container capability is moot for it. - The tool runtime's base image stays the way a module's *service* may be built; it is no longer the way tools reach a node. - The node tools runtime needs an interpreter on the machine. The module that is the runtime declares it as a package. - The container-runtime seat proposed in an open change says its holder *runs as a supervised process and serves the verbs locally to the host and on the bus*. A supervised process serving verbs is what this runtime is; whether that holder keeps a process of its own or serves through the runtime is for that record's build to say. - What got harder: one process carries every module's tool code on a node, so one module's faulty bundle can take down the node's tools. The runtime loads each bundle guarded and names the one that failed; the others serve. ## How it is checked | Rule | Checked by | |---|---| | The runtime loads every bundle its memberships name and serves each tool on its subject | the runtime's tests against a real bus: two bundles, three tools, each answers | | A bundle that fails to load is named and the others serve | the same tests, with one bundle that throws on load | | The host supervises the runtime and restarts it | the host's tests over the launcher's shape | | A push that replaces a bundle reloads it without a restart | the runtime's tests: a bundle replaced on disk, the membership re-read, the new tool answers | | No module in the catalogue declares a container whose only purpose is tools | a catalogue check: a manifest with `tools` and an image artifact built on the tool runtime's base is refused once the runtime is live | | Live | `login-shell.execute@` answers on every node from the node tools runtime; `docker ps` shows no per-module tool container | ## References - [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/03-one-tool-executor-per-node.md) - [ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md), [ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md), [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md), [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md), [ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md), [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md) - [To-be 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md), [to-be 34](../03-DESIGN/01-to-be/34-the-console.md)