Files
hq/02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
jochen bf39baf104 Research 018 graduates: ADRs 0173–0177 and to-be 37, the operator's machine
Every configurable thing on a node is a module, the home included, and a
module is whatever it declares (0173, extending 0040). A node varies a module
only through a setting rendered into the file or a kept region, never an edit
(0174, extending 0011; issue 168 first). One tool runtime per node serves every
module's tools on the host side, never in a container; the console is its
serving mode, renamed node-tools (0175, extending 0150; 0047/0150/0152 carry
dated notes). The login shell is a node seat held by one shell module with
`execute` as its contract (0176). A unit may be user-scoped and the service
manager is a node seat held by systemd (0177).

To-be 37 is handed off in-progress to mesh-host, mesh-controller, mesh-tools
and mesh-catalog, with the build in order: the account on every node, the
runtime, zsh, systemd, then the graphical stack. To-be 29 keeps ~/.ssh and
points at 37; 33 §6 and 34 are amended; the glossary gains node tools, bundle,
kept region, installed/holding, and retires flavor.
2026-10-02 16:34:57 +02:00

8.0 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

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