Merge pull request 'ADR 0188: a module's own code is bundles in any language, and a tools bundle speaks MCP to the runtime; design 38 gains WP1b' (#300) from decision/0187-a-modules-own-code-is-bundles-in-any-language into main
This commit was merged in pull request #300.
This commit is contained in:
+1
-1
@@ -108,7 +108,7 @@ term retired here may still appear there, and the mapping above is how to read i
|
||||
memberships issue; its serving mode on loopback is what was called **the console**
|
||||
([ADR 0175](../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)).
|
||||
Replaces **"console"** as the module's name; *console* remains the word for the person's end of it.
|
||||
- **bundle** — the artifact a module's tools are built into, interpreted or compiled; never an image.
|
||||
- **bundle** — the artifact a module's own code is built into — its tools, a seat's implementation, a daemon — in any language the mesh has a toolchain for, interpreted or compiled; never an image. One module may declare several ([ADR 0188](../02-DECISIONS/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md)).
|
||||
- **kept region** — a marked block in a managed file the mesh writes *into*, where the operator's own
|
||||
lines survive every push and are given back when the module goes
|
||||
([ADR 0174](../02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)).
|
||||
|
||||
@@ -8,6 +8,8 @@ reconstructed: false
|
||||
|
||||
# 39. What the SDK holds, and what it refuses
|
||||
|
||||
> **The mechanism changed — 2026-10-02, by [ADR 0188](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md).** The test of this record — frequent *and* cascading does not belong — stands and now applies to one SDK per language. Where *what it holds* names the broker client and the event consumer, read: the local protocol a tools bundle speaks to the node's runtime; the transport lives in the runtime and in no SDK, which is what keeps a bus change from rebuilding any module in any language.
|
||||
|
||||
_Reconciliation note (2026-09-05): supersedes the earlier "repository structure" decision, which the consolidation folded; no standalone record remains to point at, so body references to it now point at the nearest surviving record, [ADR 0015](0015-applications-live-in-their-own-repository.md)._
|
||||
|
||||
## Context
|
||||
|
||||
@@ -9,6 +9,8 @@ extends: 02-DECISIONS/0047-a-module-runs-its-code-as-its-own-process-with-its-ow
|
||||
|
||||
# 150. A module's own code runs as supervised processes under the module's one account
|
||||
|
||||
> **Widened — 2026-10-02, by [ADR 0188](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md).** A module's long-lived process is a bundle in any language the mesh has a toolchain for, run as a unit the host writes; this record never said one language and never meant one, and 0188 says so as the rule.
|
||||
|
||||
> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).** For a module's *tools*, read that record: one runtime per node, the node's one account, bundles loaded from the memberships. This record still governs a module's long-lived processes — a daemon, a provisioner, a scheduled ingest — and the account invariant for them.
|
||||
|
||||
## Context
|
||||
|
||||
+2
@@ -9,6 +9,8 @@ extends: 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under
|
||||
|
||||
# 175. One tool runtime per node serves every module's tools, on the host side
|
||||
|
||||
> **The mechanism changed — 2026-10-02, by [ADR 0188](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md).** 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
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-10-02
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 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](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md) 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](0039-what-the-sdk-holds-and-refuses.md)) 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](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md)), 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
|
||||
|
||||
1. **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.
|
||||
2. **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.
|
||||
3. **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.
|
||||
4. **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](0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)
|
||||
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](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md) and
|
||||
[ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
|
||||
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](../03-DESIGN/01-to-be/38-building-the-operators-machine.md) 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 |
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md),
|
||||
[ADR 0039](0039-what-the-sdk-holds-and-refuses.md),
|
||||
[ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md),
|
||||
[ADR 0156](0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md),
|
||||
[ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
|
||||
- [To-be 38](../03-DESIGN/01-to-be/38-building-the-operators-machine.md) — the work packages this
|
||||
record widens
|
||||
- The Model Context Protocol's stdio transport — the local protocol a tools bundle speaks
|
||||
@@ -285,6 +285,7 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0181** — [The operator account is a node fact, and a home is a placement root](0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)
|
||||
- **0182** — [Inside a home, the mesh owns the directory and the files it places, writes into the tool's own files, and holds everything else as found](0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
|
||||
- **0183** — [The Anthropic licence manager is a module holding a seat; it hands each node's agent its token over the bus, sealed; the controller and the host have no part](0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md)
|
||||
- **0188** — [A module's own code is bundles in any language, and a tools bundle speaks MCP to the runtime](0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md)
|
||||
|
||||
### How it is built
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ decisions:
|
||||
- 02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md
|
||||
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
||||
- 02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md
|
||||
- 02-DECISIONS/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md
|
||||
---
|
||||
|
||||
# 38. Building the operator's machine
|
||||
@@ -97,6 +98,19 @@ what `tools` answers, and the others serve. The runtime reads `MESH_OPERATOR_ACC
|
||||
five tools and two seat verbs answer on their subjects; `tools` names the failed bundle; a
|
||||
membership republished mid-run re-subscribes without a restart.
|
||||
|
||||
*Built and proven 2026-10-02* (mesh-tools, branch `feat/the-operators-machine`, commit `6390d1d`).
|
||||
|
||||
**WP1b — the launcher beside the loader** ([ADR 0188](../../02-DECISIONS/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md)).
|
||||
*mesh-tools, mesh-sdk. A day for the skeleton.* A bundle whose entry is not JavaScript is launched
|
||||
as a child process with the runtime's environment and spoken to over MCP on stdio: `tools/list`
|
||||
once, `tools/call` per call; a tool named `<seat>.<verb>` is the seat's implementation. A child
|
||||
that exits is named as a failed bundle and restarted on the next call. The TypeScript import stays
|
||||
as the shortcut. Beside it, one skeleton SDK per language of the first set — the stdio loop and the
|
||||
tool-definition type, nothing else — each proven by one bundle in that language answering one tool
|
||||
in the runtime's test. **Proof.** The runtime's test: a bundle in a second language, launched, its
|
||||
tool answering on its subject over a real bus; the TypeScript fixture served through the protocol
|
||||
with the shortcut off answers the same.
|
||||
|
||||
## WP2 — The controller composes one runtime per node
|
||||
|
||||
*mesh-controller. Two to three days; the largest package.*
|
||||
@@ -111,12 +125,15 @@ membership republished mid-run re-subscribes without a restart.
|
||||
declaration gains an `archive` placed under a directory the controller derives, so the host
|
||||
fetches and unpacks it as it does any artifact. The bundle's digest is what the build recorded.
|
||||
3. **The runtime's process.** One `process` per node running the runtime from its own bundle
|
||||
(WP3), `MESH_TOOL_MODULES` composed from the unpacked entrypoints, `MESH_OPERATOR_ACCOUNT` and
|
||||
(WP3), `MESH_TOOL_MODULES` composed from the unpacked entrypoints — each as
|
||||
`<module>=<path>`, and the runtime decides from the file whether it is loaded or launched
|
||||
(WP1b) — `MESH_OPERATOR_ACCOUNT` and
|
||||
`MESH_OPERATOR_HOME` from the account fact, `restart-on` naming every bundle so a push that
|
||||
changes one restarts it. A node with no account composes the runtime without the two words.
|
||||
4. **The gate.** A manifest declaring `tools` and a container built on the runtime's base image is
|
||||
refused at registration once the runtime module is registered, naming this record. It is the
|
||||
mechanism that keeps the old pattern from returning by habit.
|
||||
mechanism that keeps the old pattern from returning by habit. ADR 0188 widens it, after WP4:
|
||||
a module whose own code is an image artifact is refused, whatever image it is built on.
|
||||
|
||||
**Proof.** Composition tests: a node with three assigned modules, one holding a seat, yields one
|
||||
process, three archives, one node principal whose grants are the union, and the same three
|
||||
|
||||
Reference in New Issue
Block a user