Files
hq/02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
T
jochen 709240ec1f ADR 0187: a module's own code is bundles in any language, and a tools bundle speaks MCP to the runtime; design 38 gains WP1b
The operator's direction, absent from every record until now: the SDK must not limit who writes a
module; tools and services may be written in any language; one module may ship several bundles
(tools, a seat's implementation, a daemon); skeleton first, a full implementation when the work
requires it. ADR 0175 had the runtime import a bundle, which only JavaScript can be.

0187 makes a tools bundle a process the node's runtime launches and speaks MCP over stdio to —
the vocabulary the runtime already speaks outward — so any language with an MCP library can write
one today and the mesh's SDK per language is thin; the transport stays in the runtime (0039's
refusal, kept). Importing a TypeScript bundle is the shortcut, not the contract. Notes in 0175,
0039 and 0150 say where their mechanism moved; design 38 records WP1 as built and adds WP1b (the
launcher and the skeleton SDKs); the glossary's bundle widens.
2026-10-02 18:56:57 +02:00

8.7 KiB

topic, status, date, deciders, reconstructed, extends
topic status date deciders reconstructed extends
what runs on it accepted 2026-10-02 jochen false 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

The mechanism changed — 2026-10-02, by ADR 0187. Everything decided here stands: one runtime per node, host-side, every module's tools and every held seat's verbs on the memberships' subjects, root the module's concern, any node calls any tool, the console its serving mode. What moved is how the runtime brings a bundle to life. Decision 3 and the consequence the node tools runtime needs an interpreter on the machine read as though a bundle were always interpreted code the runtime imports; a tools bundle is now a process in any language that speaks MCP over stdio to the runtime, and importing a TypeScript bundle is the shortcut, not the contract.

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). What runs that code is ADR 0150: 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): 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): 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 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); a mesh-scoped seat's verbs run on the node that holds it (ADR 0121).

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 §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@<node> answers on every node from the node tools runtime; docker ps shows no per-module tool container

References