Files
hq/03-DESIGN/01-to-be/38-building-the-operators-machine.md
T
jschoubben da8b4b4ee4 Renumber to ADR 0188: 0187 landed on main first, as the dead-tracker record
Two records shared 0187 (issue 155's collision); the branch landing last
renumbers, and this is it. Only the number changes.
2026-10-02 21:01:02 +02:00

14 KiB

layer, status, code, updated, decisions
layer status code updated decisions
to-be in-progress
mesh-tools
mesh-controller
mesh-host
mesh-catalog
2026-10-02
02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md
02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md
02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md
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

The work of design 37, broken into packages small enough that each ends at something a person can see run, in the order their dependencies allow. Design 37 is the authority on what is built; this document holds only the packages, their order, their sizes and their proofs, and is wrong the moment it disagrees with 37 rather than the other way round. It is the shape design 28 gave the bus work, applied here.

How this is built, and where it is run

On the live mesh, by the operator's decision. Every package is written with unit tests and committed on one branch per repository; its proof runs on the four machines, not in the lab. ADR 0149 already says the live mesh is the test bed; the operator's words on 2026-10-02 were skip the lab, it is not too bad if something is broken. The cost accepted: a package that breaks the runtime breaks every tool on a node until the next push, and the controller's own verbs stay reachable through the controller seat whatever happens to a node's runtime — which is the one thing that must hold, and does by construction (ADR 0154).

Each package names what proves it. A package that cannot name its proof is divided until it can.

What exists already, measured

Counted 2026-10-02 in the four repositories, non-test source. The point of the count is the same as design 28's: nothing here is new ground; every package reshapes something standing.

Piece Today Size Becomes
the tool runtime TypeScript: loads MESH_TOOL_MODULES, serves one module's tools and its claimed seats' verbs; serve is the console ~1 700 lines over six files loads every assigned module's bundle; serve is node tools
the host's process shape Go: fetch a bundle by digest, unpack under the mesh's daemons directory, write the unit, run it 343 lines unchanged — the runtime is one such process
the host's archive shape Go: fetch and unpack an artifact at a path 185 lines unchanged — a module's tools bundle is one such archive
the controller's bus principals Go: one principal per module per node, grants from what it declares 132 lines gains one principal per node for the runtime
the controller's memberships Go: one per assignment, the subjects a runtime serves 143 lines unchanged in shape; the runtime reads several
the controller's declaration composer Go, one file 2 053 lines gains the runtime's process, the bundles' archives, two env words
the catalogue 35 manifests build a per-module tool container on the runtime's base image — none do; the runtime is a module of its own

Two measurements decide the shape. The host needs no change: a process and an archive are what the runtime and a bundle are, and both are applied today. And the runtime already does nine-tenths of the job — the loop over entrypoints, the seat verbs, the membership subscription — for one module; the work is to let it do the same for a list.

The order the work allows

WP1  the runtime serves many modules            (mesh-tools)        ──┐
WP2  the controller composes one runtime a node (mesh-controller)   ──┤ independent, test-proven
                                                                      │
WP3  the runtime is a module; the console is its serving mode (mesh-tools, mesh-catalog)
                                                                      │
WP4  the first holder moves: the packet filter  (mesh-catalog)      ── the live proof
                                                                      │
WP5  the shell, on a server                      (mesh-catalog)      ── the first environment module live
WP6  the service manager, on a workstation       (mesh-host #72, mesh-catalog)
                                                                      │
WP7  the login manager, the display server, the window manager …     ── one record per seat, after this document
WP8  settings for the theme knobs                                    ── after issue 168 closes

WP1 and WP2 touch different repositories and meet only at the membership's shape, which neither changes; they are built in parallel. WP3 needs both. WP4 is the first time anything on a machine changes, and it is the proof of the whole. WP5 and WP6 are the first environment modules; the packages after them are design 37 §4's candidates and are not broken down here, because each begins with a decision record this document cannot anticipate.

WP1 — The runtime serves many modules

mesh-tools. About a day.

What changes. serve takes a list of modules to serve, each with its entrypoints, rather than one module and one credential. The runtime reads one membership per module from the subjects ADR 0160 derives for each, and serves each module's tools on that module's subjects and each held seat's verbs on the seat's. The filter that drops a registration under any name but the one module goes; what remains is the rule that a registration under a seat's name is served only where some module the runtime serves claims that seat. A bundle that throws on import is named in the log and in what tools answers, and the others serve. The runtime reads MESH_OPERATOR_ACCOUNT and MESH_OPERATOR_HOME and hands them to every tool's environment.

What does not change. The SDK. The broker client. The MCP surface. A module's tool code.

Proof. The runtime's test against a real bus: three bundles, one of which throws on import; 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). 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.

What changes, in four pieces, each its own commit:

  1. A node principal. Beside one principal per module per node, one per node of kind node-tools: its serving grants are the union of every assigned module's tool subjects and every held seat's verbs on that node, its invoking grant is *, and it consumes nothing. The per-module memberships are composed as today; nothing else on the bus learns a new shape.
  2. Bundle delivery. For every assigned module whose build produced a bundle, the node's 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 — 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. 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 memberships as before. The gate's test: the packet-filter manifest as it is today is refused once the runtime is registered.

WP3 — The runtime is a module, and the console is its serving mode

mesh-tools and mesh-catalog. A day.

What changes. mesh-tools gains a bundle artifact of itself beside its images, and its manifest becomes the node-tools module: a package for the interpreter, the loopback listener the console declared, invokes: *, and nothing else — the process is the controller's to compose (WP2). In the catalogue, mesh-console is retired as a module and node-tools assigned where it was. The runtime's serve keeps answering MCP on loopback; the person's end of it keeps the name console (glossary).

Proof. On every node: the console's container is gone, node-tools runs as a unit the host wrote, tools/list on loopback answers as before, and the controller's verbs answer through it. This is the first live step, and it is reversible by re-assigning mesh-console.

WP4 — The first holder moves: the packet filter

mesh-catalog. Half a day. The live proof of ADR 0175.

What changes. The nftables module drops its container, its NET_ADMIN and its runtime artifact; its tools bundle stays and its claim stays. Its remove and reload escalate inside the tool where they need root, which they have, since the runtime runs as the node's account.

Proof. node-packet-filter.rules@<node>, reload and remove answer from the runtime on all four machines; docker ps shows no mesh-nftables; status is well. Then the fail2ban holder proposed in an open change follows the same way when it lands.

WP5 — The shell, on a server first

mesh-catalog #224, already written. Half a day to assign and prove.

Order. Assign zsh to one server; push; login-shell.execute@<server> command="uptime" answers; the account's login shell reads zsh; its ~/.zshrc carries the mesh's block with the operator's lines around it. Then the other three nodes. The two things the manifest cannot say — the user shape applying only where the seat is held, and a second shell module installed beside the holder — are the first follow-up record after this document.

WP6 — The service manager, on a workstation

mesh-host #72 merged first; mesh-catalog #224. Half a day.

Order. Merge the host's user-scope change and let it roll. Assign systemd everywhere; node-service-manager.units@<node> scope=user answers on a workstation. Then the first user-scoped unit the mesh sends: the window manager's reload watcher, declared scope: user by the window manager module when WP7 writes it — until then, the host's change is proven by its tests and by the verb answering.

What is deliberately not here

  • The graphical stack's seats (WP7). Each begins with a record naming its holders and verbs, and the first graphical module asks the resolver a question this document cannot answer for it: whether a held seat gates another's assignment.
  • Settings for the theme knobs (WP8). Blocked on the settings record proposed in an open change and on issue 168.
  • Reload without restart. WP2 restarts the runtime on a bundle change; a reload that keeps the other modules' tools up during one module's change is a refinement for after WP4 proves the simple form.
  • Lingering. A user-scoped unit answers only while the account's manager runs; declaring lingering for the account is a field on the user shape, decided when a server first needs a user unit.

How this list is kept true

Each package's proof is run on the live mesh when the package is finished and its line here gains the date and the commit, the way ADR 0170 carries built and proven live. A package whose proof fails is not reworded; the failure is recorded under it and the package stays open. When WP6 is proven, design 37's status moves to implemented for what it covers and this document's to the same.