From 179fd7f83f894d8f4cb71a5e6c33cbd491344f40 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 2 Oct 2026 17:20:14 +0200 Subject: [PATCH] To-be 38: building the operator's machine as work packages; ADR 0175 collision renumbered to 0180 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The runtime work of design 37 broken down the way design 28 broke down the bus: what exists measured (the host needs no change — a process and an archive are what the runtime and a bundle are; the runtime does the job for one module and must do it for a list), the order the dependencies allow, and eight packages each ending at a proof on the live mesh — the lab skipped by the operator's decision, ADR 0149 cited. WP1 the runtime serves many modules; WP2 the controller composes one per node (a node principal, bundle archives, the runtime's process, the gate); WP3 the runtime is a module and the console its serving mode; WP4 the packet filter moves first; WP5 the shell on a server; WP6 the service manager on a workstation; WP7–8 named and not broken down. Main carried two records numbered 0175 and cycle.py refused it: the one that landed last (the found front end is uninstalled) takes 0180, the next free across main and the open changes, with a dated note; design 08's citation follows it; the index is regenerated. --- ...ninstalled-once-a-machine-is-converged.md} | 4 +- 02-DECISIONS/README.md | 2 +- 03-DESIGN/01-to-be/08-connectivity.md | 4 +- .../38-building-the-operators-machine.md | 193 ++++++++++++++++++ 03-DESIGN/01-to-be/README.md | 1 + 5 files changed, 200 insertions(+), 4 deletions(-) rename 02-DECISIONS/{0175-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md => 0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md} (90%) create mode 100644 03-DESIGN/01-to-be/38-building-the-operators-machine.md diff --git a/02-DECISIONS/0175-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md b/02-DECISIONS/0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md similarity index 90% rename from 02-DECISIONS/0175-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md rename to 02-DECISIONS/0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md index 9924f4c..0c2d3c7 100644 --- a/02-DECISIONS/0175-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md +++ b/02-DECISIONS/0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md @@ -7,7 +7,9 @@ reconstructed: false extends: 02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md --- -# 175. The found front end is uninstalled once a machine is converged +# 180. The found front end is uninstalled once a machine is converged + +> **Renumbered 2026-10-02.** Written and merged as 0175 while another record already held that number on main (one tool runtime per node, merged minutes earlier); `cycle.py` refused main. The branch that lands last renumbers: 0178 and 0179 are claimed by open changes, so this is 0180. Nothing cited it by number. ## Context diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 43c45f8..f3a46ce 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -182,7 +182,7 @@ python3 00-META/checks/index.py fail if stale - **0169** — [A machine joins through the tunnel, and the bus is never public](0169-a-machine-joins-through-the-tunnel-and-the-bus-is-never-public.md) - **0170** — [The firewall seat serves its verbs, and a foreign rule set is removed through one of them](0170-the-firewall-seat-serves-its-verbs.md) - **0172** — [The lab is a module, and runs a bed when the mesh asks](0172-the-lab-is-a-module-and-runs-a-bed-when-the-mesh-asks.md) -- **0175** — [The found front end is uninstalled once a machine is converged](0175-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md) +- **0180** — [The found front end is uninstalled once a machine is converged](0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md) ### Its tiers, from the bottom up diff --git a/03-DESIGN/01-to-be/08-connectivity.md b/03-DESIGN/01-to-be/08-connectivity.md index 18bf28d..ac4c4a2 100644 --- a/03-DESIGN/01-to-be/08-connectivity.md +++ b/03-DESIGN/01-to-be/08-connectivity.md @@ -9,7 +9,7 @@ code: - mesh-host internal/apply (the service that reflects a rule set) updated: 2026-10-02 decisions: - - 02-DECISIONS/0175-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md + - 02-DECISIONS/0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md - 02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md - 02-DECISIONS/0169-a-machine-joins-through-the-tunnel-and-the-bus-is-never-public.md - 02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md @@ -773,7 +773,7 @@ the host reports as *other* is reached through the packet filter seat's `remove` by name on the bus; the seat also serves `rules` and `reload`, and its holder's runtime declares the `NET_ADMIN` capability on the machine's network. See design 33. -*2026-10-02, [ADR 0175](../../02-DECISIONS/0175-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md):* +*2026-10-02, [ADR 0180](../../02-DECISIONS/0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md):* once a machine is converged, the front end it was found with is uninstalled, not merely disabled — the packet filter's holder declares its package absent after the mesh's filter is loaded, and a return to adopted then enables nothing. The rollback path ADR 0100 kept on disk is given up on purpose. diff --git a/03-DESIGN/01-to-be/38-building-the-operators-machine.md b/03-DESIGN/01-to-be/38-building-the-operators-machine.md new file mode 100644 index 0000000..fbf4049 --- /dev/null +++ b/03-DESIGN/01-to-be/38-building-the-operators-machine.md @@ -0,0 +1,193 @@ +--- +layer: to-be +status: in-progress +code: [mesh-tools, mesh-controller, mesh-host, mesh-catalog] +updated: 2026-10-02 +decisions: + - 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 +--- + +# 38. Building the operator's machine + +**The work of [design 37](37-the-operators-machine.md), 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](28-building-the-bus.md) 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](../../02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md) 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](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)). + +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](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md) +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. + +## 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, `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. + +**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](../../00-META/glossary.md)). + +**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@`, `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@ 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@ 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](../../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md). +- **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](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md) +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. diff --git a/03-DESIGN/01-to-be/README.md b/03-DESIGN/01-to-be/README.md index 7647a51..fba266c 100644 --- a/03-DESIGN/01-to-be/README.md +++ b/03-DESIGN/01-to-be/README.md @@ -42,6 +42,7 @@ document is written and this one's status becomes `implemented`. | [`32-what-a-module-declares.md`](32-what-a-module-declares.md) | **Proposed.** What a module declares and what the bus derives from it: three namespaces, subjects from local names, queues never declared, the five relationships, and the build-publish-deploy lifecycle on one bus | [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md), [ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md), superseded by [ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md) (superseding [ADR 0125](../../02-DECISIONS/0125-the-bus-is-the-only-broker.md)), [ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) | | [`37-the-operators-machine.md`](37-the-operators-machine.md) | **In progress.** Every configurable thing on a node is a module, the home included; one default per module varied by settings or kept regions; roles a machine has once as seats with tool contracts; one tool runtime per node on the host side | [ADR 0173](../../02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md), [0174](../../02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md), [0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md), [0176](../../02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md), [0177](../../02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md) | +| [`38-building-the-operators-machine.md`](38-building-the-operators-machine.md) | **In progress.** The work of design 37 as packages: the runtime serves many modules, the controller composes one per node, the console becomes its serving mode, the packet filter moves first, then the shell and the service manager — tested on the live mesh by the operator's decision | [ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md), [0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md), [0149](../../02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md) | ## Not yet written