Two records shared 0187 (issue 155's collision); the branch landing last renumbers, and this is it. Only the number changes.
8.4 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/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md |
188. A module's own code is bundles in any language, and a tools bundle speaks MCP to the runtime
Context
ADR 0175 put one tool runtime on every node and said a module brings its tools as a bundle. The runtime that exists is written in TypeScript and brings a bundle to life by importing it into its own process, which only JavaScript can be. The SDK (ADR 0039) is one TypeScript package. The builder knows three toolchains — TypeScript, Go, Python — and every one of the 35 catalogue modules with tools wraps them in a container on the runtime's TypeScript image. Nothing in the records says a module's code may be written in anything else, and nothing refuses a module that wraps its own code in an image to get around that.
The operator's direction, stated on 2026-10-02 and repeated: the SDK is the most important part; we must not limit developers; tools can be written in any possible language — Rust, C, Go, JavaScript. A service in Go or Rust as a systemd unit must be possible too. One module can deliver all kinds of bundles: one for its tools, one for a seat's implementation, one for a daemon. Support the bare minimum first, as a skeleton; a full implementation comes when the work requires it.
Measured against that: the bundle artifact kind already names a language and the process
resource already runs a command from an unpacked bundle as a unit the host writes
(ADR 0150), so a Go
daemon as a native service is possible today and one module in the catalogue does it. What is not
possible is a tool in any language but one, and what is not written is that any of this is the
rule.
Considered Options
- One SDK, one language, as now. Rejected: it limits who can write a module to one ecosystem, which the operator declines, and it is what made every module's tools a container on one image.
- A full bus client per language. Each SDK speaks the bus itself; the runtime only supervises. Rejected: a transport in every SDK is what ADR 0039 refuses, and a bus change would then rebuild every module in every language — the cascade, multiplied.
- A tools bundle is a process the runtime launches and speaks a small local protocol to, and that protocol is MCP over stdio. Chosen. The runtime already speaks MCP outward (the console); speaking it inward to a child process is the same vocabulary. Every language that has an MCP server library can write a tools bundle today with no mesh SDK at all, and the mesh's own SDK for a language is a thin convenience over it. The transport stays in the runtime, so a bus change rebuilds nothing.
- A protocol of the mesh's own design. Rejected: a second way to describe a tool, its schema and its call, inventing what MCP already settled, for no gain.
Decision
1. A module's own code is bundles, in any language the mesh has a toolchain for, and never an
image. A bundle names its language and what it is for. Images are for third-party software a
module installs — a database, a forge — never for code the module wrote. One module may declare
several bundles: its tools, its implementation of a seat's verbs, a daemon, a step. Each is built
alone and delivered alone, as ADR 0156
already has it.
2. A bundle the runtime serves is a process that speaks MCP over stdio. The node's runtime
launches it as the bundle names it — an interpreter and a file, or a binary — with the runtime's
environment, asks tools/list, and answers each call on the bus by tools/call. A tool whose name
is <seat>.<verb> is the module's implementation of that seat's verb; any other name is the
module's own tool. Everything the runtime does with what it is told — subjects from the membership,
a held seat's verbs, the tools answer, a bundle that fails named and the others serving — stays as
ADR 0175 and
ADR 0160
have it. A TypeScript bundle may still be imported into the runtime's own process; that is a
shortcut over the same contract, not a second contract, and a TypeScript bundle written against
the protocol is served the same way as any other.
3. A bundle that is a service is a process, run by the host as a unit, in whatever language it
is compiled from, exactly as the host's own bundle already is. Nothing new is decided here; it is
said so that it is the rule and not an example.
4. One thin SDK per language, and the test of ADR 0039 applies to each. An SDK for a language holds the MCP-over-stdio loop, the tool-definition type and the few primitives a module's code needs; it holds no transport, no module's client and nothing volatile. Where a language has a sound MCP library, the SDK wraps it rather than re-implementing it. The languages are those that make sense to write a module in; the first set is TypeScript, Go, Python, Rust and C, and the set grows when a module needs one, not before.
5. Skeleton first. Each piece — a toolchain, a launcher, an SDK — exists at the bare minimum that lets one bundle in that language be built, delivered and answer one tool on the live mesh. Anything beyond that is added when a module needs it. A skeleton that is not proven by one bundle answering is not a skeleton; it is a promise.
Consequences
- The runtime gains a launcher beside its loader. The loader, the memberships, the seats and the failure handling built for ADR 0175 stand; the launcher is the one new step.
- The builder gains a toolchain per language, each at the skeleton: compile, pack, name the entrypoint. Rust and C are new; a language that compiles to a binary says its operating system as a Go bundle already does.
- An existing MCP server in any language is already a valid tools bundle. What the mesh adds is the subjects, the seats and the memberships around it.
- The gate to-be 38 WP2 adds — refusing a tools container built on the runtime's image — widens: a module whose own code is an image artifact is refused at registration, naming this record.
- What got harder: a tools bundle is now a process per module on the node rather than code in one process, so the runtime supervises children and restarts one that dies. The one-process shape ADR 0175 counted on for the TypeScript shortcut remains available for it.
- ADR 0039's "what the SDK holds" now reads per language; its refusals are unchanged and are the reason option 2 was rejected.
How it is checked
| Rule | Checked by |
|---|---|
| A module's own code is never an image | the catalogue's registration check: a manifest with a bundle kind of own code and an image artifact built from the module's own directory is refused, naming this record |
| A tools bundle in a language other than TypeScript answers on the bus | the runtime's tests: a bundle written against the protocol in a second language, launched, its tool called over a real bus |
| A TypeScript bundle written against the protocol is served like any other | the same tests, with the TypeScript shortcut off |
| Each SDK is thin | each SDK's own README states what it holds under ADR 0039's test, and its size is in the mesh's records |
| Live | a tool in a compiled language answers from the node's runtime on one machine |