Compare commits

...
Author SHA1 Message Date
jschoubben b69ae663bc ADR 0189: the store keeps what the records name, and a maintenance step holds its writers still
Issue 108: the artifact store has never collected anything. Fifty-three
repositories on the machine that serves everything else, and the only outcome
of leaving it is a full disk reported as somebody else's failure.

The mesh decides what may go — from its own build records, so it never names
a digest it did not put there — and the store reclaims the bytes in a nightly
window with its server held still. Deletion on the one door takes nothing a
push did not already have.

Designs 18 and 20 amended; issue 108 resolved.

Also issue 202, found running the controller's suite: a module whose required
setting nobody set is left out of the machine in silence, and dnsmasq became
that module this morning.
2026-10-02 21:48:48 +02:00
jschoubben 0b9fc90885 ADR 0188: a provider declares what it derives for each consumer, and the mesh tells both ends
Issue 124: a value the mesh's own rule produced reached neither end as a
statement. The object store's provisioner derived each consumer's bucket in
its own code; all three consumers transcribed the rule into their own
definitions, one of them wrong, and each of the three also named the machine
it happens to run on.

A served value may now name the consumer the mesh is serving. Design 27
amended; issue 124 resolved.
2026-10-02 21:24:31 +02:00
mesh-admin 983fd412c6 Merge pull request 'ADRs 0180, 0186 and 0187: their live rows, done' (#302) from docs/the-four-fixes-proven-live into main 2026-10-02 18:46:49 +00:00
jschoubben 9ac2493e2c ADRs 0180, 0186 and 0187: their live rows, done — all four machines filtered by the mesh alone, both front ends removed, no machine wrong or behind 2026-10-02 20:46:35 +02:00
mesh-admin 560f25c2c7 Merge pull request 'ADR 0187: a dead tracker is not the machine's failure' (#301) from fix/a-dead-tracker-is-not-the-meshs-failure into main 2026-10-02 18:42:01 +00:00
jschoubben 9e0288128b ADR 0187: a dead tracker is not the machine's failure; design 32 2026-10-02 20:41:30 +02:00
mesh-admin d57289e049 Merge pull request 'ADR 0186: a ban list never holds a neighbour, and the mesh's own bans are its own wherever they hang' (#299) from fix/a-ban-list-never-holds-a-neighbour into main 2026-10-02 16:43:32 +00:00
jschoubben d4a2f99ab5 ADR 0186: a ban list never holds a neighbour, and the mesh's own bans are its own wherever they hang; design 31 2026-10-02 18:42:16 +02:00
mesh-admin a9f91fdd0c Merge pull request 'ADRs 0184 and 0185: a service is still running a moment later; a control plane behind its row serves what it can' (#298) from fix/a-service-asked-to-run-is-still-running into main 2026-10-02 16:25:44 +00:00
jschoubben 131a5e4714 Issue 201: what was established about the race while closing the outage half 2026-10-02 18:19:12 +02:00
jschoubben 329a24fdae ADRs 0184 and 0185: a service is still running a moment later; a control plane behind its row serves what it can; issue 201 half closed 2026-10-02 18:16:24 +02:00
mesh-admin 7f72f3b79a Merge pull request 'ADR 0179: the intrusion seat serves its verbs, a container may log to the journal, and every door declares its jail' (#295) from feat/the-intrusion-seat-serves-its-verbs into main 2026-10-02 15:28:48 +00:00
jschoubben 114a71f36f ADR 0179: built and proven live; the one fault the machine found, and the check that refuses it 2026-10-02 17:28:38 +02:00
jschoubben 0f407417f3 Merge main: the ufw record renumbered to 0180, and ADR 0175 retires the per-module tool runtime this one ships 2026-10-02 17:28:23 +02:00
mesh-admin 4af731df19 Merge pull request 'The operator's agent and its licence manager are modules: ADR 0181–0183, to-be 36 and 39' (#286) from feat/claude-code-module into main 2026-10-02 15:23:38 +00:00
jochen 7b1dabbce0 The operator's agent and its licence manager are modules: ADR 0181–0183, to-be 36 and 39
The predecessor's agent module was retired and its six files stayed on both workstations telling
every session to use tools that no longer exist. This is its successor's design, revised during
review on the operator's directions: the host is module-agnostic, the controller has no part, and a
real licence manager hands out the correct licence in every situation.

- ADR 0181 (reconstructed): the operator account is a node fact stated by the operator; the home is
  derived unless stated; a resource may be placed under it owned by the account; a node with no
  account refuses one. What the controller shipped on 2026-09-27 without a record.
- ADR 0182: inside a home the module owns the directory and the files it places, writes into the
  tool's own files for its few keys, never declares a credential's content, and holds everything
  else as found; a predecessor's leftovers are the operator's to remove once.
- ADR 0183: claude-licence-manager holds the mesh seat anthropic-licence-manager and owns the
  Anthropic licences, grants (encrypted with a key the vault made for it), bindings per touchpoint,
  usage and audit; one rotation source under a lease; a token travels module to module sealed to
  each node's module key on request/reply, never as an event; the agent module alone writes what
  the agent reads; the host delivers package and state and knows nothing else. A bounded exception
  to ADR 0113; dated mechanism notes on ADR 0050 and 0113.
- To-be 36 (claude-code): the mesh's part of the agent's configuration lives in the agent's
  machine-wide managed directory, owned whole by the module and written by its code; nothing under
  the home but the credentials file of a subscription licence; the API-key licence through the
  key-helper; the console as a node-scoped provision (to-be 34 amended); MCP servers as settings
  with an mcp_configure tool; one agent directory per machine shared by every session.
- To-be 39 (claude-licence-manager): store, the two licence kinds, keeping a grant alive, the
  hand-over, who gets which licence with the predecessor's fallbacks, adoption with the identity
  guard, the seat's verbs.

Numbers taken across main and every open branch at the time of the merge; to-be 14, 29 and 34
carry dated notes; the glossary gains "operator account".
2026-10-02 17:22:12 +02:00
mesh-admin 2caa5e827b Merge pull request 'To-be 38: building the operator's machine as work packages; ADR 0175 collision renumbered to 0180' (#296) from feat/the-operators-machine into main 2026-10-02 15:20:38 +00:00
jochen 179fd7f83f To-be 38: building the operator's machine as work packages; ADR 0175 collision renumbered to 0180
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.
2026-10-02 17:20:14 +02:00
jschoubben d0d5799884 Issue 201: a push recreated the controller at a digest older than the seat row its successor wrote 2026-10-02 17:15:22 +02:00
jschoubben 9ba4de5557 ADR 0179: the intrusion seat serves its verbs, a container may log to the journal, and every door declares its jail; designs 31 and 33 2026-10-02 17:02:49 +02:00
mesh-admin e1203e5a43 Merge pull request 'Research 019: a warm twin of the running mesh' (#294) from jschoubben/research-019 into main 2026-10-02 14:59:44 +00:00
jschoubben 4ce967619a Research 019: a warm twin of the running mesh in the lab 2026-10-02 16:59:36 +02:00
mesh-admin 6df2cfecd6 Merge pull request 'Research 018 graduates: ADRs 0173–0177 and to-be 37, the operator's machine' (#293) from feat/the-operators-machine into main 2026-10-02 14:58:48 +00:00
jochen 73047501f6 ADR 0154: the controller seat gains a generic command verb (dated note, by ADR 0175) 2026-10-02 16:53:15 +02:00
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
mesh-admin 5eadf36937 Merge pull request 'ADR 0175: the found front end is uninstalled once a machine is converged' (#292) from feat/the-found-front-end-is-uninstalled into main 2026-10-02 14:29:00 +00:00
jschoubben 8c9a2c7501 ADR 0175: the found front end is uninstalled once a machine is converged; design 08 note 2026-10-02 16:27:33 +02:00
mesh-admin 54213ba90c Merge pull request 'Research 018: the operator's machine as modules' (#291) from research/018-the-operators-machine into main 2026-10-02 14:27:27 +00:00
jochen 7fb59bde98 Research 018: the operator's machine as modules
The predecessor is retired on every node and what it still owned on the two
workstations — some thirty modules of dotfiles, user units and /etc files — is
owned by nothing. To-be 29 covers one directory under the home; the operator
wants the whole machine, system folders and home alike, as modules: one
default configuration each, varied per node by settings or a kept region,
never an edit; roles the machine has once as node-scoped seats with tool
contracts; the graphical stack gated by a capability so the same catalogue
serves the servers.

Four documents: the intended behaviour in the mesh's words; the predecessor's
desktop measured (34 modules, one with 88 files, 4 flavors and ~90 theme
variables) against what the records already give and what is missing (the
account is empty on every node, no user-scoped units, settings leak, tools run
in a container per module per node); the direction the operator set for where
tools run — one executor per node, host-side, module-agnostic, the console
renamed and moved out of its container, superseding 0047/0150 for tools; and
the candidate seats of the environment with first verbs, the shell first.
2026-10-02 15:19:25 +02:00
mesh-admin a4d24d7b65 Merge pull request 'ADR 0168: built and proven live' (#290) from decision/0168-built into main 2026-10-02 13:09:22 +00:00
jschoubben 116b2d1793 ADR 0168: built and proven live — the live row read on the home server and the control node, the five rule sets removed through ADR 0170's verb 2026-10-02 15:08:58 +02:00
mesh-admin 68a14493c9 Merge pull request 'ADR 0169 → 0170: the firewall seat's record renumbered after a collision on main; built note; cycle.py refuses two records sharing a number' (#289) from decision/0170-renumbered-and-built into main 2026-10-02 12:52:17 +00:00
jschoubben 98eb3aa76f ADR 0169 → 0170: the firewall seat's record renumbered after a collision on main; its built note; cycle.py refuses two records sharing a number
Another session's 0169 landed first. The collision check from issue 155
covered issue folders only; it covers decision records now, and would have
refused this.
2026-10-02 14:51:22 +02:00
mesh-admin 91bbe648a8 Merge pull request 'Issues 199 (resolved: a node-scoped seat's verb through the console) and 200 (the controller's answer to a long console call is refused by the bus)' (#288) from issues/199-200-console-seat-verbs-and-inbox into main 2026-10-02 12:25:37 +00:00
jschoubben 0e7b85f184 Issues 199 (resolved: a node-scoped seat's verb through the console) and 200 (the controller's answer to a long console call is refused by the bus) 2026-10-02 14:25:03 +02:00
mesh-admin dac49de6e7 Merge pull request 'ADR 0172: the lab is a module, and runs a bed when the mesh asks' (#287) from jschoubben/the-lab-is-a-module into main 2026-10-02 12:17:21 +00:00
jschoubben 0d9208dbbf ADR 0172: the lab is a module, and runs a bed when the mesh asks 2026-10-02 14:15:44 +02:00
mesh-admin bf0ee7cb25 Merge pull request 'ADR 0169: the firewall seat serves its verbs, and a foreign rule set is removed through one of them' (#285) from feat/the-firewall-seat-serves-its-verbs into main 2026-10-02 11:29:15 +00:00
jschoubben 4567e13071 ADR 0169: the firewall seat serves its verbs, and a foreign rule set is removed through one of them
Designs 33 and 08 revised. The first node-scoped seat with verbs: rules,
reload, remove; the nftables module holds it from a runtime with NET_ADMIN,
the first container to declare a capability.
2026-10-02 13:27:03 +02:00
mesh-admin aa5d9f1045 Merge pull request 'ADR 0169 (proposed): a machine joins through the tunnel, and the bus is never public' (#284) from jschoubben/a-machine-joins-through-the-tunnel into main 2026-10-02 11:17:42 +00:00
jschoubben 79642251a1 ADR 0169 accepted; design 08's join order starts with the tunnel 2026-10-02 13:17:32 +02:00
jschoubben 331cb94c6e ADR 0169 (proposed): a machine joins through the tunnel, and the bus is never public
The bus was public only so a new machine could enrol before it had a
tunnel. The machine now makes its tunnel key first, the token is issued
for it and makes it a peer of the hub, and enrolment happens over the
tunnel.
2026-10-02 13:13:15 +02:00
mesh-admin 78351560f7 Merge pull request 'Issue 198: the home network's DNS server ran outside the mesh, and its filter closed it' (#283) from jschoubben/the-lans-dns-is-the-mesh into main 2026-10-02 10:22:51 +00:00
mesh-admin 62cc2f89c7 Merge pull request 'Issue 197: a physical link that is down is not filtered when it comes up' (#280) from jschoubben/every-physical-link-faces-outside into main 2026-10-02 10:22:40 +00:00
jschoubben 426f741ad0 Issue 198: the home network's DNS server ran outside the mesh, and its filter closed it 2026-10-02 12:22:31 +02:00
jschoubben 9bed54d3be Issue 197 resolved: the wired port is guarded before it is plugged in 2026-10-02 12:22:15 +02:00
jschoubben 413daf8ad5 Issue 197: a physical link that is down is not filtered when it comes up 2026-10-02 12:22:15 +02:00
mesh-admin 5c993c09b7 Merge pull request 'Issues 143 and 144 resolved: the live row of ADR 0168 read on the home server and the control node' (#282) from issues/143-144-resolved into main 2026-10-02 10:11:37 +00:00
jschoubben 7c3be48db2 Issues 143 and 144 resolved: the live row of ADR 0168 read on the home server and the control node 2026-10-02 12:11:17 +02:00
mesh-admin b665d06701 Merge pull request 'ADR 0168: a converged machine is filtered by the mesh alone, and the host says what else refuses (group 7)' (#281) from feat/one-thing-filters-a-converged-machine into main 2026-10-02 10:03:48 +00:00
jschoubben 1bd13446d4 ADR 0168: a converged machine is filtered by the mesh alone, and the host says what else refuses (group 7)
Designs 08 and 05 revised; 141 resolved by ADR 0140 and 084 by ADR 0102 and
issue 128, both by reading; 143 and 144 decided, built on the matching
branches in mesh-host and mesh-controller, resolved when the home server's
record names the predecessor's chain.
2026-10-02 11:58:18 +02:00
mesh-admin 14adaafa53 Merge pull request 'to-be 29: the account fact and home-scoped resources shipped; the ssh-client module, CA and ~/.ssh boundary did not' (#279) from design/29-what-shipped into main 2026-10-02 09:49:52 +00:00
mesh-admin bd673cc6ec Merge pull request 'Group 6 closed: 086, 090, 098, 099, 100, 101 resolved on the operator's decision' (#278) from issues/group-6-closed into main 2026-10-02 09:38:11 +00:00
jschoubben bd6c55d225 Group 6 closed: 086, 090, 098, 099, 100, 101 resolved on the operator's decision, each saying the live row was not run 2026-10-02 11:37:08 +02:00
mesh-admin 2bbbc56502 Merge pull request 'Issue 194 resolved: the fixed host forgot its former archive on all four machines' (#277) from issue/194-resolved into main 2026-10-02 09:31:49 +00:00
jschoubben c8935aceca Issue 194 resolved: the fixed host forgot its former archive on all four machines 2026-10-02 11:31:29 +02:00
mesh-admin 29b656f2c0 Merge pull request 'Issue 196: the hub relays the mesh only on the ports it publishes itself' (#276) from jschoubben/the-hub-relays-the-mesh into main 2026-10-02 09:23:55 +00:00
jschoubben d05ac367f1 Issue 196 resolved: the hub relays the mesh, re-swept live 2026-10-02 11:23:37 +02:00
jschoubben 1c0dafb918 Issue 196: the hub relays the mesh only on the ports it publishes itself 2026-10-02 11:17:11 +02:00
mesh-admin 018ee359ae Merge pull request 'Issue 194: the host's own former archive stops every machine applying anything' (#274) from issue/194-the-hosts-own-former-archive-stops-every-apply into main 2026-10-02 09:04:30 +00:00
mesh-admin c5535eeebd Merge pull request 'ADR 0163 built: the take digest, the minted-secret refusal, the networks setting, settings judged where stored, genesis raising the forge as declared' (#270) from feat/a-take-is-a-comparison-the-rest into main 2026-10-02 09:04:21 +00:00
mesh-admin dbb9d2bc16 Merge pull request 'Issue 191 and ADR 0167: a membership carries what its module receives, and who the mesh is' (#273) from jschoubben/an-internal-only-route into main 2026-10-02 07:49:27 +00:00
jschoubben 28d53dcc28 Issue 191 resolved: internal-only routes are served to the mesh, live on both proxies 2026-10-02 09:49:25 +02:00
jschoubben df667eb710 ADR 0167: a membership carries what its module receives, and who the mesh is
Issue 191's route proxy needs to know who the mesh is to serve an
internal name correctly, and the first fix had it work that out alone.
The membership on the bus now carries it, from the same list the filter
uses. ADR 0138 gains an insight that the proxy is where internal reach
is kept; designs 08 and 25 say how.
2026-10-02 09:49:19 +02:00
jschoubben 098a2ca485 Issue 191: a route with only an internal name is dropped as naming nothing 2026-10-02 09:49:19 +02:00
mesh-admin a34cedeb5d Merge pull request 'Issue 195: every assigned module is counted as a bus user without a credential' (#275) from jschoubben/issue-195 into main 2026-10-02 00:44:29 +00:00
jschoubben db5ff5a5ee Issue 195: every assigned module is counted as a bus user without a credential
The status warning names 49 users; 48 are modules that never speak on the
bus, and the one real fault, a declared broker secret filled with a
generated value, looked the same as the rest.
2026-10-02 02:44:24 +02:00
jschoubben 780c2b6e58 Issue 194: the host's own former archive stops every machine applying anything
Rule 5 of ADR 0163 (a former target is removed) met issue 162 (an archive
has no removal) in the host's own archive, the first time a host carrying
former targets replaced itself; every machine applied nothing from then on.
2026-10-02 02:42:19 +02:00
jschoubben c4151e6bc4 ADR 0163 built: the take digest, the minted-secret refusal, the networks setting, settings judged where stored, genesis raising the forge as declared; issues 096, 097, 126 resolved
The record gets its built note; designs 05 and 09 the revisions; 086, 098,
099, 100 and 101 stay located because every machine is converged and the
record's live row — a take read on an adopted machine — has not been run;
090 is built in part, its network difference left for the take to say.
2026-10-01 23:45:57 +02:00
jochen 8c231102f8 to-be 29: the account fact and home-scoped resources shipped; the ssh-client module, CA and ~/.ssh boundary did not
The controller merged §1, §2 and the composed ssh config on 2026-09-27 while hq still
called the design proposed. Record what is on main, what is held on a branch, and what
has no code — and that the account fact shipped without a decision record, which is the
next thing to write. Also drop the real account and node names the public repository must
not carry, and correct ADR numbers that drifted in a renumbering.
2026-10-01 23:14:10 +02:00
mesh-admin d8083bcf9e Merge pull request 'Issue 189: the plan's half is built; the moved word stays for a decision' (#269) from issue/189-the-plan-half into main 2026-10-01 20:28:33 +00:00
jschoubben 6f26f97fdb Issue 189: the plan's half is built; the moved word stays for a decision 2026-10-01 22:25:31 +02:00
mesh-admin 63ed4a7c96 Merge pull request 'Issue 189: a rebuild from the same commit is not a move, so a packaging module's new image never rolls out' (#268) from issue/189-a-rebuild-from-the-same-commit-is-not-a-move into main 2026-10-01 19:55:20 +00:00
jschoubben 48ca2fb41b Issue 189: a rebuild from the same commit is not a move, so a packaging module's new image never rolls out 2026-10-01 21:54:44 +02:00
mesh-admin 3976f09738 Merge pull request 'ADR 0163: taking a module over is a comparison (group 6)' (#266) from decision/0163-taking-a-module-over-is-a-comparison into main 2026-10-01 19:13:53 +00:00
jschoubben 2904c359b8 ADR 0163: taking a module over is a comparison — what it compares, refuses and carries; designs 05 and 09; group 6's issues located, 093 resolved 2026-10-01 21:12:36 +02:00
mesh-admin b277f3b4ba Merge pull request 'ADR 0162: built, and proven live by the first tiered plan' (#265) from decision/0162-live into main 2026-10-01 19:00:53 +00:00
jschoubben 50e4d9c2a7 ADR 0162: built, and proven live by the first tiered plan 2026-10-01 21:00:10 +02:00
mesh-admin a7d0dd83d1 Merge pull request 'Issues 184 and 188 resolved' (#264) from issues/184-188-resolved into main 2026-10-01 18:54:28 +00:00
jschoubben a2c9fbb665 Issues 184 and 188 resolved: the merge handler returns at once; a machine is resolved with its pins and a dropped one is said 2026-10-01 20:54:10 +02:00
mesh-admin 0278766dfb Merge pull request 'Issue 188: a refusal inside "who is on the network" drops a machine silently' (#263) from issue/188-a-refusal-inside-on-the-network-drops-a-machine-silently into main 2026-10-01 16:22:51 +00:00
jschoubben 606fbb7add Issue 188: the second fault beneath the first, and its fix 2026-10-01 18:22:19 +02:00
jschoubben a92e4bf121 Issue 188: what the live fault was and how it was resolved 2026-10-01 18:14:37 +02:00
jschoubben 187442ec7b Issue 188: a refusal inside on-the-network drops a machine silently 2026-10-01 18:10:57 +02:00
mesh-admin 47c45d4386 Merge pull request 'ADR 0162: a merge produces a tiered plan the mesh keeps; dependencies are one relation with three kinds' (#262) from decision/0162-a-merge-produces-a-tiered-plan into main 2026-10-01 15:54:23 +00:00
jschoubben 82496536cd ADR 0162: the three kinds of dependency, where the edges come from, and the one real cycle 2026-10-01 17:53:57 +02:00
jschoubben b0de267301 ADR 0162: the link to 0157 by its name 2026-10-01 17:46:07 +02:00
jschoubben b0a74b23fd ADR 0162: a merge produces a tiered plan the mesh keeps; dependencies are one relation; design 30; issues 184, 186 2026-10-01 17:45:48 +02:00
mesh-admin 6a5f68d11f Merge pull request 'ADR 0084: a pin names the module as well as the node (#258)' (#261) from docs/pin-names-the-module into main 2026-10-01 15:26:03 +00:00
jschoubben 821cd3b489 ADR 0084: a pin names the module as well as the node (#258) 2026-10-01 17:23:34 +02:00
mesh-admin 49b0319230 Merge pull request 'Issues 106 and 138 resolved (ADR 0161 built and live); 187 notes a report lost without retry' (#260) from issues/106-138-resolved into main 2026-10-01 15:20:05 +00:00
jschoubben 86d1763cfa Issues 106 and 138 resolved (ADR 0161 built and live); 187 notes a report lost without retry 2026-10-01 17:18:17 +02:00
mesh-admin a7e9e6dea0 Merge pull request 'ADR 0160: the live proof, and three facts it taught' (#259) from decision/0160-live into main 2026-10-01 14:52:04 +00:00
jschoubben ea0853ca46 ADR 0160: the live proof, and three facts it taught 2026-10-01 16:51:43 +02:00
mesh-admin fcd27c8399 Merge pull request 'Issue 184: a handler replaced mid-merge loses the rest of its work' (#257) from issue/184-also-seen into main 2026-10-01 14:25:53 +00:00
jschoubben 03e39316ea Issue 184: a handler replaced mid-merge loses the rest of its work, and the redelivered announcement reads as history 2026-10-01 16:25:38 +02:00
mesh-admin 65a252dc00 Merge pull request 'Issue 187: the mesh tells nobody when it stops working' (#256) from issue/187-the-mesh-tells-nobody-when-it-stops-working into main 2026-10-01 14:21:27 +00:00
jschoubben 6be284c781 Issue 187: the mesh tells nobody when it stops working 2026-10-01 16:21:05 +02:00
mesh-admin ea9a433385 Merge pull request 'Issue 186 located: the delivery dropped the asks, not the queue; a merge now rebuilds dependents; the release decision stands' (#255) from issue/186-located into main 2026-10-01 14:19:45 +00:00
jschoubben 9ddd7215ad Issue 186 located: the delivery dropped the asks, not the queue; a merge now rebuilds dependents; the release decision stands 2026-10-01 16:16:59 +02:00
mesh-admin 626af3e8e2 Merge pull request 'Issue 186: a release across repositories is an order in a person's head, and a build is a line in a queue nobody keeps' (#254) from issue/186-the-mesh-has-no-picture-of-the-end-state-a-release-aims-for into main 2026-10-01 14:02:04 +00:00
jschoubben 180e3b8f7e Issue 186: a release across repositories is an order in a person's head, and a build is a line in a queue nobody keeps 2026-10-01 16:01:45 +02:00
mesh-admin 4ae452d0ad Merge pull request 'ADR 0161: what deserves a seat (issues 105, 106, 138; design 26)' (#253) from decision/0161-what-deserves-a-seat into main 2026-10-01 13:57:24 +00:00
jschoubben f35f3757bb ADR 0161: what deserves a seat — the vault's seat, the hub as a placement of capacity one, the uplink holder as the machine's dialect; design 26; issues 105, 106, 138 2026-10-01 15:55:15 +02:00
mesh-admin 6b8fb562ce Merge pull request 'Issue 185: a refused membership publish stopped the controller; 183 points to it' (#252) from fix/issue-185-a-refused-membership-stops-the-controller into main 2026-10-01 13:44:17 +00:00
jschoubben 2175d13935 Issue 185: a refused membership publish stopped the controller; 183 points to it 2026-10-01 15:42:37 +02:00
mesh-admin 39f0d64840 Merge pull request 'ADR 0160 built: both halves and what the first roll-out taught; issues 183 and 184' (#250) from feat/the-runtime-serves-what-it-is-issued into main 2026-10-01 13:24:00 +00:00
jschoubben eef03f2b08 ADR 0160 built: both halves, and what the first roll-out taught; issues 183 (the controller's grant) and 184 (a merge blocks the receive loop) 2026-10-01 15:23:26 +02:00
mesh-admin 64acc94de8 Merge pull request 'ADR 0160: the mesh issues an assignment's subjects, and a runtime serves what it is issued' (#249) from decision/0160-the-mesh-issues-subjects into main 2026-10-01 12:34:33 +00:00
jschoubben 99eb322de5 ADR 0160: the mesh issues an assignment's subjects, and a runtime serves what it is issued; designs 25, 32, 33, 34 2026-10-01 14:33:56 +02:00
mesh-admin 5460681117 Merge pull request 'ADR 0159: a tool call names the machine, every answer says which answered, and a holder's runtime serves its seat's verbs' (#248) from feat/a-tool-call-names-the-machine into main 2026-10-01 12:01:16 +00:00
jschoubben 0acb47fa55 ADR 0159: a tool call names the machine, every answer says which answered, a holder's runtime serves its seat's verbs; issue 182; designs 33 and 34 2026-10-01 14:00:23 +02:00
mesh-admin 7a633f2780 Merge pull request 'ADR 0158: the controller's half is built (mesh-controller 184); the provider definitions remain' (#247) from decision/0158-built into main 2026-10-01 10:27:58 +00:00
jschoubben bef510fda2 ADR 0158: the controller's half is built (mesh-controller PR 184); the provider definitions remain 2026-10-01 12:27:39 +02:00
mesh-admin c58f4d6790 Merge pull request 'ADR 0158: a provider with one credential shares it with every consumer, and the vault remakes it for all at once' (#246) from decision/0158-a-provider-with-one-credential-shares-it into main 2026-10-01 10:18:11 +00:00
jschoubben d29d3dfc23 ADR 0158: a provider with one credential shares it with every consumer, and the vault remakes it for all at once; designs 24 and 13 carry it 2026-10-01 12:17:49 +02:00
mesh-admin d85b41ae4d Merge pull request 'Issue 180's live proof, and issue 181 (was 163): two records answered to one number' (#245) from issue/180-live-proof into main 2026-10-01 10:15:53 +00:00
jschoubben e00e3bc3ce Issue 181 (was 163, was 161): two records answered to 163; renumbered to the next free number across main and open pull requests 2026-10-01 12:15:31 +02:00
jschoubben b8d8101c45 Issue 180: the live rotation done — searxng's secret on the home server through the console 2026-10-01 12:14:49 +02:00
mesh-admin 85961f8348 Merge pull request 'Issue 180: a module's own secret rotates when it is read at start; the applied form stays open' (#244) from feat/231-an-own-secret-rotates into main 2026-10-01 09:43:46 +00:00
jschoubben 48a620249b Issue 180: a module's own secret rotates when it is read at start; the applied form stays open (controller PR 183); design 13 and ADR 0114 carry the word 2026-10-01 11:43:23 +02:00
mesh-admin 7f0fe27cf2 Merge pull request 'Issue 170: assigning a module claims every seat it could hold' (#218) from issue/170-assigning-a-module-claims-the-seat-it-could-hold into main 2026-10-01 09:24:14 +00:00
mesh-admin bfc410fafe Merge pull request 'Issue 169: a machine shares its files, and the mesh does not know' (#215) from issue/169-a-machine-shares-files-and-the-mesh-does-not-know into main 2026-10-01 09:24:11 +00:00
mesh-admin d436122be2 Merge pull request 'Issues 164-168: found provisioning every dependency on ace' (#213) from issue/164-168-found-provisioning-ace into main 2026-10-01 09:24:09 +00:00
mesh-admin 76a535e69e Merge pull request 'Issue 161: an assignment does not record which provider answers it' (#210) from issue/161-an-assignment-does-not-record-its-provider into main 2026-10-01 09:24:07 +00:00
mesh-admin 027e5b8d73 Merge pull request 'Issue 179: an adopted identity provider's admin never took the secret the mesh minted' (#242) from issue/179-an-adopted-identity-providers-admin-never-took-the-minted-secret into main 2026-10-01 09:02:37 +00:00
jschoubben 79d1619f16 Issue 179: an adopted identity provider's admin never took the minted secret (fixed by hand through the server's bootstrap; the design question left open) 2026-10-01 11:02:18 +02:00
mesh-admin 5a6b7ca4f8 Merge pull request 'Issue 178: a routed name resolves to a provider merely told it, and flips between plans' (#241) from fix/227-a-name-resolves-to-the-node-that-serves-it into main 2026-10-01 00:05:14 +00:00
jschoubben e1f2c6bd5b Issue 178: a routed name resolves to a provider merely told it, and flips between plans (fixed, controller PR 181) 2026-10-01 02:04:55 +02:00
mesh-admin cf8134e318 Merge pull request 'Issue 177: the controller's check is run by nobody, and two of its tests failed for days unseen' (#240) from fix/the-converged-declaration-guard into main 2026-09-30 23:38:36 +00:00
jschoubben 35f7f4401b Issue 177: the controller's check is run by nobody; the two rotted tests fixed (controller PR 180), the process half open 2026-10-01 01:38:22 +02:00
mesh-admin 62d61938ad Merge pull request 'Issue 176 resolved: a build is taken in where its outcome is heard' (#239) from fix/176-a-build-is-registered-where-it-is-heard into main 2026-09-30 23:28:10 +00:00
jschoubben f841845b0d Issue 176 resolved: a build is taken in where its outcome is heard; the build tool answers with the id 2026-10-01 01:27:49 +02:00
mesh-admin 3627f7e9db Merge pull request 'ADR 0157: a build says what it does on the bus, as it happens' (#238) from feat/a-build-says-what-it-does into main 2026-09-30 22:43:59 +00:00
jschoubben 2ffe1d0915 ADR 0157: a build says what it does on the bus, as it happens; designs 25 and 18 carry it 2026-10-01 00:43:26 +02:00
mesh-admin 763e327610 Merge pull request 'Issue 176: the console's build tool neither waits nor registers, and does not take a forge path' (#237) from issue/176-the-consoles-build-tool-neither-waits-nor-registers into main 2026-09-30 22:29:25 +00:00
jschoubben 93f828c5eb Issue 176: the console's build tool neither waits nor registers, and does not take a forge path 2026-10-01 00:29:05 +02:00
mesh-admin fe706af63a Merge pull request 'Issues 173 and 174 resolved; the installation check refuses at registration' (#236) from feat/the-mesh-places-its-own-files into main 2026-09-30 22:07:38 +00:00
jschoubben 52e9df0f02 Issue 153 resolved: an assignment places a module's directories and its accesses
mesh-controller PR 176 and mesh-catalog PR 198. Designs 27 and 18 carry the words: places,
accesses, ${access:<id>}, the default a definition still holds while the catalogue converts.
2026-10-01 00:04:10 +02:00
jschoubben 36454d7e4a Issues 173 and 174 resolved; the installation check refuses at registration (ADR 0155)
A setting overrides a key a contribution or served fact declares and adds none; a provider that must
tell its consumers an operator's value declares it as ${setting:…} (173). The mesh's own files for a
module are a placed directory, `place: "mesh"`, and forty-eight definitions name no host path for
them (174). Registration refuses a definition naming an installation, the day the list emptied
rather than a release later (0155, progressive insight; 134). Designs 27 and 18 carry the rules.
2026-09-30 22:36:20 +02:00
jschoubben e84c822e89 Merge pull request 'Issue 175: the link to issue 127 resolves' (#235) from fix/issue-175-link into main 2026-09-30 20:10:41 +00:00
jschoubben a170913202 Issue 175: the link to issue 127 resolves 2026-09-30 22:10:38 +02:00
jschoubben 9a20c16d9b Merge pull request 'Issue 175: an announcement queued behind a long build came back, and the build ran again' (#234) from fix/one-announcement-at-a-time into main 2026-09-30 19:38:49 +00:00
jschoubben 8bd0ca0bdc Issue 175: an announcement queued behind a long build came back, and the build ran again 2026-09-30 21:38:45 +02:00
jschoubben 598f6a8952 Merge pull request 'ADR 0156: an artifact is what a build produces, and the store's seat is named for its scope (group 4, step 3)' (#233) from feat/the-artifact-store-seat-is-named-for-its-scope into main
Reviewed-on: #233
2026-09-30 19:17:28 +00:00
jschoubben 3341c037cb Merge pull request 'Issue 119 resolved for a module's own data; issue 174 for the mesh's files (group 4, step 2)' (#232) from feat/definitions-place-their-directories into main
Reviewed-on: #232
2026-09-30 19:17:21 +00:00
jschoubben 22a28ad548 ADR 0156: an artifact is what a build produces, and the store's seat is named for its scope
Issue 123 resolved; glossary corrected; design 26 and ADR 0121 point at the rename.
2026-09-30 21:14:40 +02:00
jschoubben 1e1957a9c4 Issue 119 resolved for a module's own data; issue 174 for the mesh's files; design 27 phase 3 in part 2026-09-30 21:11:42 +02:00
jschoubben 5292f4176a Merge pull request 'Issue 173: a module's settings reach every fact it contributes; what the site's rename cost' (#230) from feat/group-4-step-1-closed into main 2026-09-30 19:04:36 +00:00
jschoubben 822e8b03f8 Issue 173: a module's settings reach every fact it contributes; what the site's rename cost 2026-09-30 21:04:34 +02:00
jschoubben a82941ee0c Merge pull request 'ADR 0155: a definition names no installation, how that is checked, and the three ways out (group 4, step 1)' (#226) from feat/a-definition-names-no-installation into main
Reviewed-on: #226
2026-09-30 18:36:42 +00:00
jschoubben 9ffb7eec55 ADR 0155: a definition names no installation, how that is checked, and the three ways out
Issues 122 and 134 resolved; design 27 in progress with its first cases; design 18 names the words.
2026-09-30 18:40:05 +02:00
jschoubben 7499f1e50c Merge pull request 'Issue 006 resolved: the record is read where it is written, and the console lists it' (#225) from feat/group-3-closed into main
Reviewed-on: #225
2026-09-30 16:19:50 +00:00
jschoubben 214b486a50 Design 33 implemented: the mesh's verbs answer through the console, and what shipped bent 2026-09-30 18:18:44 +02:00
jschoubben 90b44a48df Issue 006 resolved: the record is read where it is written, and the console lists it
Design 35 implemented with what shipped and the live check; 006 closes on ADR 0025's own test,
run through the console.
2026-09-30 18:10:18 +02:00
jschoubben 860331dc37 Merge pull request 'ADR 0153 and ADR 0154: the record is read by a module, and the mesh's verbs are its seat's tools' (#224) from feat/the-mesh-answers-for-itself into main
Reviewed-on: #224
2026-09-30 15:55:18 +00:00
jschoubben 37b46d5349 ADR 0153 and ADR 0154: the record is read by a module, and the mesh's verbs are its seat's tools
Design 33 in progress against ADR 0154 (the twelve verbs, the prerequisites built); design 35 for
the records module under ADR 0153, extending 0025; as-is 07 rewritten to a mesh that keeps no store;
as-is 12 and 13 updated; issue 006 built and waiting on its live check.
2026-09-30 17:47:49 +02:00
jschoubben 16855ade02 The console shipped: design 34 implemented, as-is 13, issue 147 verified live 2026-09-30 17:13:09 +02:00
jschoubben 8dd566c0aa Merge pull request 'ADR 0152: the operator's surface is a module, the console (group 3)' (#222) from feat/the-console into main
Reviewed-on: #222
2026-09-30 14:47:36 +00:00
jschoubben a98ee0f529 Issues 147 and 148 name what fixed them 2026-09-30 16:21:33 +02:00
jschoubben ad4a5ea004 ADR 0152: the operator's surface is a module, the console
The work order's group-3 question answered: an ordinary module the mesh assigns to the machine a
person sits at, holding a minted credential, calling tools under a manifest grant (invokes), serving
MCP on loopback. Design 34; pointers in 33, 25 and 0095; module check designed into 12 (issue 148);
README stops claiming an indexing nothing provides (issue 006).
2026-09-30 16:08:52 +02:00
jschoubben 0cf1ad5dad Merge pull request 'Issue 172: the ssh client block matches one spelling of a machine's name' (#221) from issue/172-the-ssh-client-block-matches-one-spelling-of-a-machine into main 2026-09-30 13:25:34 +00:00
jschoubben 69a002fce3 Issue 172: the ssh client block matches one spelling of a machine's name
Reported by the operator: ssh by the bare name logs in, by the mesh name
is refused. The predecessor's generator writes the bare name only; the
mesh's ssh-client roster already matches both and is not yet shipped.
2026-09-30 15:25:30 +02:00
jschoubben 16a1a52cd8 Merge pull request 'Issue 171: a module that names its own resolver knows no mesh name' (#220) from issue/171-a-modules-own-resolver-knows-no-mesh-name into main 2026-09-30 13:20:30 +00:00
jschoubben af170e3a67 Issue 171: a module that names its own resolver knows no mesh name
Found and fixed the afternoon ADR 0148 landed: mailu-admin lost its
database behind Mailu's own resolver. Two catalogue PRs; an insight on
0148 that a container's dns is a decision, not a preference.
2026-09-30 15:20:26 +02:00
jschoubben 6c2d5f5913 Merge pull request 'Group 2 is resolved: containers resolve, nothing is copied, a route's name says where it arrives' (#219) from issue/110-resolved into main 2026-09-30 13:07:14 +00:00
jschoubben 04c9500b5b Group 2 is resolved: containers resolve, nothing is copied, a route's name says where it arrives
Issue 110's cause was not the filter: the runtime had never been told,
and the resolver dropped a query arriving on a bridge. ADR 0148 step 3
landed once it did (109, 151 resolved). ADR 0151 composes a route's
internal name under the serving node and drops the suffixed alias
(139, 157 resolved). Design 08 amended; a fact in 0148 corrected.
2026-09-30 14:56:43 +02:00
jschoubben d0044cf555 Issue 170: assigning a module claims every seat it could hold
Assigning postgres on ace claimed mesh-store and made novox's own store
assignment unresolvable. The resolver reads a manifest's claims as 'does
hold'; ADR 0110 says the assignment holds, by a deliberate act the controller
already has (seat …, HoldSeat) but resolution does not consult.
2026-09-30 14:29:11 +02:00
jschoubben 105ae9a56a Issue 169: network-share is the module responsible for a node's network shares — a node role 2026-09-30 13:56:24 +02:00
jschoubben ee801a6441 Issue 169: the consumer half is a module (network-share), not a host resource kind; the access-on-a-mountpoint check is the data-loss case 2026-09-30 13:56:07 +02:00
jschoubben 90b89aa1c9 Issue 169: the consumer's half — the host mounts the share, a mount is a resource of the consuming module, not a client module 2026-09-30 13:53:32 +02:00
jschoubben e33191161d Issue 169: two module-defined seats, and the gap — a seat definition has no neutral home
nfs-share and smb-share share an intent, not a contract; one seat would be
a union with every field optional. What the exemplar exposes is that a
seat declared inside one module cannot be implemented by another without
depending on it.
2026-09-30 13:52:20 +02:00
jschoubben a0a930b1cd Issue 169: file sharing is a core seat, one per protocol
The operator's call: a node-scoped system seat family (node-nfs-share,
node-smb-share, …) defined by the control plane, one per protocol as
package registries are (ADR 0109), so several modules occupy it and a
machine may hold both.
2026-09-30 13:50:31 +02:00
jschoubben 8d9c9ab6b5 Issue 169: a machine shares its files, and the mesh does not know
ace exports the operator's media library over NFS and Samba with host
services no module declares: they close at converge, nothing owns their
configuration, and no module elsewhere can require the share. Proposes a
file-sharing module that accesses the paths, holds a node-scoped seat it
defines, and provides file-share to consumers.
2026-09-30 13:49:25 +02:00
jschoubben 49b1136ded Issues 164-168: found provisioning every dependency on ace
164 a credential that must be accepted is minted anyway
165 one accepted value must be accepted once per consumer
166 a requirement cannot be optional
167 code several modules share has no home
168 a setting reaches every file and every contribution
2026-09-30 13:28:53 +02:00
jschoubben 743051efe7 Issue 163 (was 161): another record took 161 on main first 2026-09-30 13:28:22 +02:00
jschoubben e8470057aa Merge remote-tracking branch 'origin/main' into issue/161-an-assignment-does-not-record-its-provider 2026-09-30 13:28:22 +02:00
jschoubben 39340fcd76 Issue 161: an assignment does not record which provider answers it
ADR 0110 decided each assignment records where its requirements are answered
from; the control plane keeps only a per-machine pin (none recorded) and
resolves every requirement implicitly. Harmless with one provider; a second
one silently moves consumers' data.
2026-09-30 11:54:31 +02:00
158 changed files with 9094 additions and 223 deletions
+16
View File
@@ -131,6 +131,22 @@ def main():
else:
seen[number] = name
# And decision records, which 155's fix left out: on 2026-10-02 two ADRs numbered 0169 landed
# on main from two sessions within the hour, and every check passed.
seen_records = {}
for path in sorted(glob.glob(os.path.join(ROOT, "02-DECISIONS", "[0-9]*.md"))):
name = os.path.basename(path)
number = name.split("-", 1)[0]
if not number.isdigit():
continue
if number in seen_records:
bad(os.path.join("02-DECISIONS", name),
"is numbered %s, and so is %s -- a record's number is how it is cited. Take the next "
"free number across main AND every open pull request; the branch that lands last "
"renumbers" % (number, seen_records[number]))
else:
seen_records[number] = name
for path in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md"))):
front = frontmatter(path)
if front is None:
+40 -2
View File
@@ -9,6 +9,11 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
- **node** — a machine in the mesh. There are 0..n of them, and each runs the host agent. A node is
just a machine that has joined; being one implies nothing about what it runs.
- **operator account** — the login name of the person who works on a node, stated on the node
record; empty for a machine nobody logs into. Everything the mesh places under a person's home is
resolved against this account's home and owned by it
([ADR 0181](../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)).
Not "the user" (ambiguous with a module's own account) and not a name a definition carries.
- **control-node** — the one node that also holds the `mesh-controller` seat. There is exactly one
per mesh. "control-node" is not a separate kind of machine — it is a node that additionally runs
the controller (and, today, the foundation). Lose it and the other nodes keep running what they
@@ -52,8 +57,11 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
- **package** — what code resolves when it is **compiled**: an npm/cargo/pypi dependency, by
**version**. Served by the **package-registry** (gitea). Only a builder talks to it.
- **artifact** — what the mesh delivers to a machine to **install and run**: an OCI image, by
**digest**. Served by the **artifact-store** (distribution). Every node pulls from it.
- **artifact** — anything a build produces and the mesh delivers to a machine by **digest**: an
`image`, a mirrored `upstream` image, a `bundle` of the module's own code, an `archive`. Served by
the **artifact-store**, an OCI registry that holds every kind as content-addressed blobs
([ADR 0156](../02-DECISIONS/0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)).
Every node pulls from it. An image is one kind of artifact, and a module is not an image.
- These are two protocols, not one store being weak — see [ADR 0075](../02-DECISIONS/0075-two-stores-and-which-provides-what.md).
## How modules relate to the mesh
@@ -75,8 +83,38 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
is a role you occupy, and the two meet where a seat delivers a provision: occupying the seat is
what makes a module *the* provider of it.
## The surfaces
- **console** — the module (`mesh-console`) that puts the mesh's tools in front of whoever is on a
machine: an MCP endpoint on the machine's loopback for an agent, the same endpoint for a person. It
is assigned like any module, holds a credential the mesh minted, and calls tools under a grant its
manifest declares (`invokes`). Loopback is the authority boundary: whoever is on the machine owns the
mesh there ([ADR 0152](../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)).
Not "the tool bridge", "the brain" or "the MCP server" — those name the predecessor's program or a
protocol, and the console is a module.
- **invokes** — the manifest word for the tools a module calls, `<module>.<tool>` each or `*` for
every one. A grant on the publish side and nothing else; a module that declares none calls nothing.
## How this page is kept
A new name for an existing thing lands here first, in the same change that introduces it in code. A
record under `02-DECISIONS/` keeps whatever word it was written with — those are immutable — so a
term retired here may still appear there, and the mapping above is how to read it.
## The operator's machine
- **node tools** — the one tool runtime per node, a host-side process the host supervises, that loads
every assigned module's tools bundle and serves every tool and held seat's verb on the subjects the
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.
- **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)).
One of the two ways a node varies a module; the other is a **setting**.
- **installed / holding** — a module may be assigned (its package installed, its files placed) without
holding the seat its family declares; *holding* is being the one — the login shell, the display
session — on that node ([ADR 0176](../02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)).
- ~~flavor~~ — not used. What a flavor varied is a setting or a separate module.
@@ -0,0 +1,86 @@
---
status: graduated
initiated: 2026-10-02
touches:
- 02-DECISIONS/0040-what-a-module-is.md
- 02-DECISIONS/0011-managed-files-are-generated-never-edited.md
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
- 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
- 02-DECISIONS/0161-what-deserves-a-seat.md
- 03-DESIGN/01-to-be/05-the-node-host.md
- 03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md
- 03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md
- 03-DESIGN/01-to-be/34-the-console.md
- 03-DESIGN/00-as-is/10-module-catalogue.md
- 04-ISSUES/160-a-machine-says-little-about-itself-and-only-when-asked/00-report.md
- 04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md
became:
- 03-DESIGN/01-to-be/37-the-operators-machine.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/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.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
---
# 018 — The operator's machine as modules
**What.** The mesh owns the whole machine, not only the services on it. Everything a person
configures on a node — the login manager, the display server, the window manager, the shell, the
terminal, the launcher, the notifier, the audio setup, the boot images, the downloads folder, the
agent at the terminal — is a module: a package, the files it owns under `/etc` and under the
operator's home, the seat it holds, the tools it serves. One default configuration per module,
varied per node only through settings rendered into the file or a kept operator region, never
through an edit. The servers take the universal modules (shell, prompt, git, the agent); the
workstations take those and the graphical stack, which a capability the machine reports gates.
This effort writes that behaviour down, measures what the predecessor's desktop modules actually
contain, and settles what the mesh must gain before the first of them can be written.
**Why.** The predecessor is retired on every node. What it still owned on the two workstations —
about thirty modules' worth of dotfiles, user units and `/etc` files — is now owned by nothing:
no generator regenerates them, and a fix to one of them is a hand edit that nothing records. The
migration scoped these modules out as *the workstation's own environment*, and
[to-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) names them as the last
thing the predecessor was keeping alive. To-be 29 covers one directory, `~/.ssh`, and draws a
boundary inside it. The operator wants no boundary: the machine is the mesh's, as far as it makes
sense to configure it. That is a wider scope than any design states, and it reaches three records
that were written for services: what a module is, where a module's tools run, and what a managed
file may be.
**What it touches.** The module definition ([ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md)),
seats and their contracts ([ADR 0132](../../02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md)),
where a module's tools run ([ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md),
[ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md),
[to-be 33](../../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §6), the host's vocabulary
([to-be 05](../../03-DESIGN/01-to-be/05-the-node-host.md)), managed files and settings
([ADR 0011](../../02-DECISIONS/0011-managed-files-are-generated-never-edited.md),
[issue 168](../../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md)),
and the catalogue's shape ([as-is 10](../../03-DESIGN/00-as-is/10-module-catalogue.md)).
**Documents.**
- [01 — The intended behaviour](01-the-intended-behaviour.md): the operator's wish, written as
how the mesh behaves, in the mesh's own words.
- [02 — What exists, and what is missing](02-what-exists-and-what-is-missing.md): the
predecessor's desktop modules measured; which records already say what is wanted; the gaps.
- [03 — One tool executor per node](03-one-tool-executor-per-node.md): where a module's tools
run. The direction the operator set, the evidence for it, and what it supersedes.
- [04 — The seats of the environment](04-the-seats-of-the-environment.md): the roles a machine
has once, their candidate contracts, and what gates each.
**What it had to settle, and where each landed.** *(Graduated 2026-10-02.)*
1. A module is one *managed thing*, software or not, and every module may serve tools — or ADR
0040 already says this and only its examples are narrow.
2. One tool executor per node, host-side, module-agnostic; which records it supersedes and
in what form the console continues.
3. Per-node variation is a setting rendered into the file or a kept region, never an edit —
ADR 0011 stands — and issue 168 is fixed before any environment module carries a setting.
4. User-scoped units on the host's `service` shape, and a service-manager seat whose holder
serves the tools about them.
5. The operator account stated on every node; today no node record carries one.
6. The seats of the environment and their verbs, one record per seat, slowly, because a
seat's tools bind every future holder.
7. Where the environment modules live: this catalogue, or one of their own as the media chain
has; and whether a third-party organisation's tooling belongs in a public catalogue at all.
@@ -0,0 +1,99 @@
# 01 — The intended behaviour
*Written 2026-10-02 from the operator's words, in the mesh's words. What is wanted, before what
exists. Where a sentence restates a record, the record is named; where it goes further, that is
said.*
## The machine is the mesh's
**Everything configurable on a node is declared by a module.** Not only the services the mesh
runs: the login manager, the display server, the window manager, the bar, the launcher, the
notifier, the compositor, the lock screen, the terminal emulator, the clipboard, the shell and its
prompt, the editor, the audio setup, the boot images, the package manager's configuration, the
agent a person runs at a terminal, and the folders a person works in — a downloads folder that is
tidied, backed up, distributed to other nodes and asked questions of. System folders and the
operator's home alike. The operator is the only person on every node, so the mesh manages the
person's machine, not a machine with a person on it.
This is [ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md)'s definition applied without
the service bias its examples carry. A module is one managed thing, named once, described
completely by its manifest. It may have a package, files, a container, a unit, a binary, a seat it
holds, and tools it serves — any one of these, or all, or two. There is **no kind of module**: zsh
has a package, files, a seat claim and the tools that claim obliges it to serve; downloads has a
folder, a process and tools; nftables has a package, files, a service, a seat and tools. The
difference is what each declares, not what each is.
**The home has no boundary.** [To-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md)
owns one directory under the home and draws a line inside it between the mesh's and the person's.
Here the line is drawn only by what the modules declare: every file some module places is the
mesh's; what no module declares is found and left alone, exactly as the adoption rules already
say for a machine. The reach is bounded by sense, not by a rule — the mesh configures what can
be configured, and a person's documents, projects and history are data under
[ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md), not configuration.
**A module names no node and no path.** The operator account is a node fact and the home is
derived from it ([to-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) §1–2,
shipped in the controller; its record is proposed in an open change). A module places a file
*under the home, owned by the account*, and the same manifest lands on a server and a laptop.
## One default, varied by settings, never by edits
**One module, one default configuration.** The window manager module ships the configuration
that is right for every node. There are no flavors: the predecessor's one desktop module carried
four, one per class of machine, and what differed between them is what settings are for.
**A node varies a module in exactly two ways.** A **setting**, declared by the module with its
type, meaning and default (proposed alongside the container-runtime records), set for the mesh
or for one node, and rendered into the file at composition — the value is in the file, not in an
environment variable the file reads. Or a **kept region**: a block in a file the mesh writes
*into*, where the operator's own lines survive every push
([ADR 0102](../../02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md)). An
edit to a managed file outside such a region is not a third way; it is overwritten, as
[ADR 0011](../../02-DECISIONS/0011-managed-files-are-generated-never-edited.md) says, and the
predecessor's habit of adopting disk drift back into its database is not carried over.
The predecessor's theming — some ninety environment variables substituted into templates at sync
time, with tools to list and set them — is the same idea with the wrong rendering. The knobs
become declared settings; the file carries the value.
## Roles a machine has once are seats, and seats carry tools
**A role a machine fills at most once is a node-scoped seat**, declared by a module
([ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md),
[ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md)): the login shell, the
display session, the display server, the terminal emulator, the launcher, the notifier, the
compositor, the lock screen, the service manager, the boot loader. Several modules may be able to
hold one — zsh, fish and bash can all hold the login shell — and the assignment on each node says
which does. Installing a shell is installing software; holding the seat is being *the* shell.
**A seat's contract is its tools** ([ADR 0132](../../02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md)).
Every holder of the login-shell seat serves `execute`, which takes one string, the command, and
runs it on the node the seat is scoped to. Every holder of the boot seat serves "rebuild the boot
images", so *"rebuild your boot images"* is a verb addressed to a machine, not a one-off step in
a hook. Every holder of the service-manager seat answers for the units on the machine, system and
user scope. A module may serve its own tools beside the seat's
([ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md) §2): show the rendered
configuration, set a theme value, report status.
**Any tool may be called from any node.** The operator's statement, and the grant model it
implies: the executor on each node may call everything, as the console already may. A verb that
needs root on the machine is the module's concern — the tool escalates, the executor and the
caller do not know.
## Servers and workstations differ by capability, not by catalogue
The same catalogue serves every node. A module declares what it needs — a graphical session, a
display server, a container runtime — and the machine reports what it has, as the profile already
reports eight capabilities today ([issue 160](../../04-ISSUES/160-a-machine-says-little-about-itself-and-only-when-asked/00-report.md)).
Assignment refuses the wrong placement by name
([ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md) §3). So every node takes the shell,
the prompt, git and the agent; only a node with a graphical session can take the display server,
and only a node holding the display server can take a window manager. Nothing in a module says
"workstation".
## What the operator would say to the mesh
*Set the login shell on the build node to fish. Rebuild the laptop's boot images. Show me the
window manager's effective configuration on the desktop and where each value comes from. Give
the downloads folder on the laptop to the home server. Run `uptime` on every node.* Each of these
is a seat verb or a module tool, addressed to a node, answered by whatever holds the role there.
@@ -0,0 +1,91 @@
# 02 — What exists, and what is missing
*Measured 2026-10-02 on one installation: two workstations, two servers, all four converged to
the mesh; the predecessor retired on the last workstation the day before. Numbers are from the
machines and the repositories, not from memory.*
## 1. What the predecessor's desktop looks like
The predecessor's catalogue on the laptop held **34 modules**, of which **28** are the operator's
environment rather than services. By what they declare:
| shape | count | examples |
|---|---|---|
| package only | 9 | browser, mail client, process monitor, media player, file manager, chat |
| package + `/etc` files + system service | 5 | login manager, display server, power and thermal daemons, package manager configuration |
| package + files under the home | 6 | shell and prompt, the agent at the terminal, scripts, the sync client, a music player |
| files under the home + user units + hooks | 2 | the desktop environment, audio |
| third-party organisation tooling | 6 | out of scope here |
**The desktop module alone** declares **88 files**, **4 flavors** (the window-manager stack, and
one per class of machine), **2 user units** with a hook to enable them, 8 files under `/etc`, a
wallpaper shipped as an asset, and reads **about 90 environment variables** as theme knobs,
substituted into its templates at sync time and set through a theming tool. Its hook exists
because *shipping a unit file does not run it*: one unit had been deployed for months and ran on
one machine only, because somebody had enabled it there by hand.
**The shell module** ships `~/.zshrc`, the prompt configuration, an `~/.ssh/config` that the
predecessor generated from its registry, and a `LOGIN_SHELL` variable applied with `chsh` by a
hook. Two flavors: the prompt theme, and autocompletion.
**Other modules write into the desktop module's files.** The chat client places i3 and notifier
snippets into `config.d` directories the desktop module owns, and its launch flags, window
placement and notification colours are each a variable with a default.
**One-off steps live in hooks** across the set: enable user units, `chsh`, create a swap file,
`mkinitcpio`, enable a vendor VPN service the package ships disabled. Every one is state the
host could declare or a verb a seat could serve; none is today.
## 2. What the migration did with them
The migration's module to-do scoped the whole set out as *desktop / workstation ricing — the
workstation's own environment* and *node/OS tooling — managed on the node, never catalogue*. The
last workstation's runbook then split the same set three ways: **A**, system scope, which the
host's vocabulary can express today (the login manager, the display server, the power daemons,
the package manager, the container runtime); **B**, under a home or a user unit, waiting on
to-be 29; **C**, package only, the operator's call. The migration log closes the workstation with
*the operator's desktop awaiting its design*.
Two things followed from scoping them out. Nothing regenerates those files now, so a fix is a hand
edit — the login manager's session script was fixed this way on the day of writing, and recorded
in a repository nothing deploys from. And the one piece of this family written as a mesh module,
the ssh client, was closed on hold in the catalogue until the controller carried the account fact.
## 3. What the records already give
| wanted | record | state |
|---|---|---|
| one module per managed thing; every module may have tools | [ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md) | accepted; examples are services, and the shell is named as a *shared* seat |
| a module declares its own node-scoped seat | [ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md) | accepted |
| a seat's contract is its tools; a holder may add its own | [ADR 0132](../../02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md), [ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md) | accepted; one node seat serves verbs live |
| a capability the machine reports gates a holder | [ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md) §3 | accepted; the profile already reports `graphical-session` |
| the account as a node fact; a file under the home owned by it | [to-be 29](../../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) §1–2 | built in the controller; its record proposed in an open change |
| inside a home: owned, written into, written by the module, found | proposed in the same change | proposed |
| a setting declared with type, meaning, default and cost | proposed with the container-runtime records | proposed |
| a managed file is derived; an edit is overwritten | [ADR 0011](../../02-DECISIONS/0011-managed-files-are-generated-never-edited.md) | accepted |
| the mesh writes into a shared file, never over it | [ADR 0102](../../02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md) | accepted |
| a module names no path; the host resolves the home | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) | accepted |
| the `user` shape: a login shell is declared state | [to-be 05](../../03-DESIGN/01-to-be/05-the-node-host.md) | designed; used by no module |
## 4. What is missing
1. **The account is recorded nowhere.** The node record has the column; on all four nodes it
is empty. Every home-scoped module is unassignable until the operator states it.
2. **User-scoped units.** The host's `service` shape has no user scope. To-be 29 says it
plainly: *a workstation's per-user daemons have no form the mesh can send.* The desktop
module's two units, the audio masks, the power module's memory guard and the thermal
daemon's profile switcher all need it.
3. **One-off steps.** `mkinitcpio`, `chsh`, creating a swap file. Each is either declared
state the host lacks a shape for, or a verb a seat should serve. An action in a declaration
is refused over the link, and rightly.
4. **Settings leak** ([issue 168](../../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md)):
a setting reaches every mergeable file and every contribution of its module. Ninety theme
knobs on that mechanism would reach ninety files. The proposed settings record says a setting
names the file it lands in; that has to ship first.
5. **Where tools run.** Every module that serves a tool today does so from its own container
per node. See [03](03-one-tool-executor-per-node.md).
6. **A seat's verbs are undecided for every seat but three.** To-be 33 leaves which verbs each
seat serves as *a decision per seat, slowly*. The environment adds a dozen seats.
7. **Catalogue placement.** The media chain left this catalogue for its own; whether the
environment does the same, and whether a third-party organisation's tooling belongs in a
public catalogue, are unasked.
@@ -0,0 +1,88 @@
# 03 — One tool executor per node
*The direction the operator set on 2026-10-02, the evidence it rests on, and what it supersedes.
A direction, not yet a decision: the record is written when this effort graduates.*
## Where tools are served today
[To-be 33](../../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) names three families — a
role's tools on the seat, a module's own tools on the module, the mesh's own verbs on the
controller seat — and [ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
says a runtime serves the subjects its membership issues. What *runs* that runtime is
[ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md):
one supervised process per module, under the module's own account, carrying that module's
compiled tools. Measured on the live mesh:
| who answers | how it runs | count |
|---|---|---|
| the mesh's own verbs | the controller binary, on its node | 17 verbs |
| the store seat | the store's own runtime | 2 verbs |
| the packet-filter seat | **a container per node**, built on the tool-runtime base image, with the network namespace and `NET_ADMIN`, on all four nodes | 3 verbs and 1 own tool |
| every module's own tools | the module's container, one per node it runs on | 67 tools across the catalogue |
| the console | a container per node, loopback MCP, `invokes: *` | serves none, calls all |
| the host | — | serves nothing; answers no question about the machine |
**The packet-filter holder is the case to look at.** The module is a package, three files and a
system service. To serve three verbs it also declares a built image and a container on every
node whose only job is to answer them. Scaled to the environment — a shell, a prompt, a launcher,
a notifier, a compositor, a login manager, a service manager, a boot loader, a downloads folder —
that is one container per module per node for software that is itself not a container, and the
operator's judgement is that tools should not run inside a container at all.
## The direction
**One tool executor per node, on the host side.** A process the host supervises, the way the
launcher supervises the host ([ADR 0005](../../02-DECISIONS/0005-the-node-host.md)): not a
container, one bus credential for the node, module-agnostic. It loads the tool code of every
module assigned to the node and serves each module's tools and each held seat's verbs on the
subjects the membership issues — nothing changes in what [ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)
and [ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
say about subjects, grants and memberships; what changes is that one process subscribes for the
node instead of one per module.
- **A module brings its tools as a built artifact**, a bundle the pipeline produces, never an
image. The executor knows bundles and subjects; it knows nothing of zsh or nftables.
- **A tool is code the module wrote**, one function behind an MCP verb. `execute` on the shell
seat is a function with a string argument. The executor does not declare, template or
interpret tools; it runs them.
- **Root is the module's concern.** A tool that must change the packet filter or rebuild boot
images escalates itself. The executor does not run as root for everyone, and the caller does
not know.
- **Any node may call any tool on any node.** The executor's credential may call everything,
as the console's already does; per-module grants on the calling side are not kept.
- **The mesh's own verbs stay with the controller** ([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)),
and a mesh-scoped seat's verbs run on the node that holds it
([ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)).
No hub is added; the controller's node is already one.
**The console is the executor, renamed.** It already runs on every node with a credential that
may call everything, and it already serves the mesh's tools to whoever is on the machine over
MCP on loopback ([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)).
It moves out of its container into the host's process tree, gains the serving half, and takes a
name that says what it is — *the node's tool runtime* or simply *node tools*; "console" names
the operator's half only.
## What it supersedes, and what it keeps
| record | effect |
|---|---|
| [ADR 0047](../../02-DECISIONS/0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md), [ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md) | superseded *for tools*: one process per node runs every module's tool code, under one account. A module's long-running service — a daemon, a container — is untouched; the executor runs tools, not services. The record must say why one account for every module's tools is acceptable: every tool may be called from every node anyway, and root is taken by the tool, not granted to the process |
| [ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md) | kept in substance — a module, assigned per node, loopback MCP, the machine's login is the authority — changed in form: host-side, not a container; serves as well as calls; renamed |
| [to-be 33](../../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §6, [to-be 34](../../03-DESIGN/01-to-be/34-the-console.md) | amended the same way |
| [ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md) §3, *a container may ask for a capability* | moot for that holder: the verbs run on the host side and escalate as they need |
| the container-runtime seat, proposed in an open change: *the holder runs as a supervised process and serves the verbs locally to the host and on the bus* | consistent — a supervised process serving verbs is what the executor is; the open question is whether that holder keeps its own process or serves through the executor like everyone else |
| the tool-runtime base image | no longer the way tools reach a node; may remain the way a module's *service* is built |
## What stays open
- **The executor's language.** The host is a static Go binary and loads no plugins, so the
executor is a sibling process, and its language decides the language of every tool bundle.
One decision, taken once.
- **How a bundle reaches the node.** An artifact of the module's build, delivered as the host
delivers everything else; whether it is a file resource in the declaration or a thing the
executor fetches by digest.
- **Reload.** A push that adds or upgrades a module's bundle reaches a running executor as a
reload, not a restart, or every tool on the node blinks on every push.
- **The host's own questions.** [Issue 160](../../04-ISSUES/160-a-machine-says-little-about-itself-and-only-when-asked/00-report.md)
wants a machine to say more about itself. With an executor on every node, "what is this
machine made of" is a seat verb like any other, served there.
@@ -0,0 +1,65 @@
# 04 — The seats of the environment
*Candidates, not decisions. To-be 33 says which verbs a seat serves is a decision per seat,
taken slowly, because a seat's tools bind every future holder. This document lists the roles the
operator's machine has once, who could hold each, what gates it, and a first verb or two — so
each record has a starting point.*
## The rule for what is a seat here
A role the machine fills **at most once** is a node-scoped seat, declared by the module family
that fills it ([ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md)). A thing
several of which coexist without contention — editors, browsers, media players — is not a seat;
each is a module with its own tools, and nothing is singular about it. A seat is held by one
assignment per node; other modules of the same family may be installed beside it without
holding it ([ADR 0040](../../02-DECISIONS/0040-what-a-module-is.md) §1, read with the sharper
distinction: *installed* is not *holding*).
## Candidate seats
| seat | holders | gated by | first verbs |
|---|---|---|---|
| **login shell** | zsh, fish, bash | nothing: universal | `execute(command)`; `show-config`; the holding itself sets the account's login shell through the host's `user` shape |
| **service manager** | systemd | the `service-manager` capability the profile reports | units: list, status, start, stop, restart, enable, journal; **user scope** on each |
| **boot** | grub, systemd-boot | a machine that boots itself (not a container host) | `rebuild-images`; `entries` |
| **package manager** | pacman, apt | the `package-manager` capability | search, installed, upgrade, orphans; today a capability the host uses, not a seat anyone holds |
| **display server** | xorg, wayland compositors that are their own server | the `graphical-session` capability | `displays`; `layout` |
| **display session** | i3, sway | the display server seat held on the node; i3 needs x11, sway needs wayland | `reload`; `workspaces`; `windows`; `move` |
| **terminal emulator** | xterm, alacritty, foot | display session | `open`; `font` |
| **launcher** | rofi, dmenu | display session | `show`; `theme` |
| **notifier** | dunst, mako | display session | `send`; `history`; `rule` |
| **compositor** | picom | display server (x11 only) | `restart`; `effects` |
| **lock screen** | i3lock, swaylock | display session | `lock` |
| **bar** | i3status-rust, waybar | display session | `reload`; `blocks` |
| **login manager** | lemurs, greetd | graphical session | `sessions`; `default-session` |
| **audio** | pipewire, pulseaudio | the machine reports a sound device | `sinks`, `sources`, `default`, `volume`, `mute` |
| **clipboard** | greenclip, cliphist | display session | `history`; `clear` |
Not seats, modules with their own tools: the editor, the browser, the mail client, the file
manager, the media player, the chat client, the agent at the terminal, the downloads folder, the
scripts folder, the sync client, the power and thermal daemons that are specific to one machine's
hardware.
## What the table implies
**Capabilities come first.** `graphical-session`, `service-manager` and `package-manager` are
reported today. *A display server is held* is not a capability but a seat being held, and a
module that needs it declares a dependency on the seat, not on a capability: *i3 needs the
display server seat held by xorg*. Whether a held seat can gate another's assignment is a
question for the controller's resolver, and the first environment module after the shell will
ask it.
**The service manager comes early.** Four of the predecessor's modules ship user units, and the
executor itself is a unit. User scope on the host's `service` shape is a host change whichever
module holds the seat; the seat's holder answers the questions about units, it does not apply
them — the host does, as it does for every declared resource.
**The shell comes first.** Universal, no capability, one verb that is immediately useful on
every node, and the `user` shape already makes the login shell declared state. It is the module
that proves the pattern: a package, files under the home owned by the account, a seat claim,
tools served by the executor, settings for the few things that vary per node, and a kept region
for the operator's own lines.
**The login manager is the first system-scope one**, because it needs nothing new: a package,
two files under `/etc`, a service — the same shape the ssh daemon module has today — and the
session script it owns is the file that was hand-fixed the day this effort opened.
@@ -0,0 +1,55 @@
---
status: active
initiated: 2026-10-02
touches: [lab, the lab module, the catalogue, assignments, settings, the controller's store]
became: []
---
# 019 — A warm twin of the running mesh
## What is being investigated
Whether the lab can keep a **warm twin of the mesh as it actually runs**: the same machines, carrying
the same catalogue, the same assignments and the same settings as the live mesh, raised once and kept
ready, so that a change can be tested against the mesh as it is rather than against a scenario
written to resemble it. A run against the twin would go through the lab module like any other run:
a branch per repository, the twin restored from its snapshot, the change applied, the beds run.
## Why
The lab's beds raise meshes from declarations written for the bed. They prove the mechanism. They
do not prove that a change works on the mesh that runs, with its accumulated assignments, its
operator settings, its adopted machines and its modules in their real combinations. The gap showed
on 2026-10-02:
- a change to how a module's settings reach its files was correct in every bed, and would have put a
setting into the container runtime's configuration on every machine running that module. Only the
composed plan for a real machine showed it;
- a firewall change composed cleanly and still left one machine's wired port unfiltered, because
of a link that machine had and no bed did;
- a recovery step was needed on every machine at once, after a change that every bed had passed.
The lab already has a warm mode, a snapshot of a raised scenario restored between attempts. What it
does not have is a scenario that **is** the running mesh, kept current with it.
## What it touches
- **What a twin is made of.** The catalogue and the assignments are records; settings are records;
secrets are sealed to machines and cannot be copied. Which of these can be carried to the lab as
they are, which must be substituted, and how a twin says what it substituted.
- **Data.** A twin with the real catalogue and no real data proves composition and delivery, not a
migration. Whether a twin carries data, a sample of it, or none.
- **Keeping it current.** A twin raised once goes stale with the first merge. Whether it is
re-derived from the live records on each run, refreshed on a schedule, or rebuilt only when asked.
- **Machines.** The live mesh has machines of different kinds: a server on the internet, machines
behind a home router, a laptop that sleeps. Which of their properties a twin must reproduce for a
test to mean anything (reachability, the private network, the found firewall).
- **Cost.** The lab machine's memory and disk, and how long a twin takes to raise from cold.
- **The lab module's tools.** A run against the twin rather than a named bed: one more tool, or an
argument to the run tool.
## Starting point
The lab module (ADR 0172) runs beds through the mesh, and the lab's warm mode already snapshots and
restores a raised scenario. The beds that raise a machine shaped like one live machine from the
catalogue are the nearest existing thing, and the first to compare against.
@@ -72,6 +72,12 @@ answers from it. Nothing flows back: this repository is public, the mesh is not,
would be how installation-specific detail arrives into documents that must not carry it
([`README.md`](../README.md)).
> **The mechanism changed — 2026-09-30, by ADR 0153.** What stands: read where it is written, no copy,
> one-way. What moved: the reader is a module the mesh assigns (`records`, a checkout at a commit every
> answer names) rather than the agent session of design 15, which is not built; and "the search consults
> the agent" has no store to consult since the cut-over — the console's tool list is where the record
> appears beside everything else. [ADR 0153](0153-the-record-is-read-by-a-module-and-the-console-lists-it.md).
## Consequences
**This repository stops being a fourth knowledge system, properly.** The original objection was
+2
View File
@@ -78,6 +78,8 @@ capability. The host hardcodes no firewall, supervisor, package manager or runti
generic apply primitives and platform detection, so it runs where none of those exist — an Android
phone has no ufw, systemd, pacman or Docker.
> **The mechanism changed — 2026-10-02, by [ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md).** The shell example above — *bash, zsh and fish all join `shell`; one may be default* — is read as *installed is not holding*: the three may all be installed, and the `login-shell` seat is node-scoped and held by exactly one. The decision — what a module is, and the three relationships — stands; [ADR 0173](0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md) applies it to the operator's whole machine.
## Consequences
- **Supersedes the earlier "grouped by domain" decision** (folded in consolidation; see the
@@ -9,6 +9,8 @@ extends: 0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md
# 47. A module runs its code as its own process, with its own account
> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).** A module's *tools* are no longer served by the module's own process under its own account: one tool runtime per node, on the host side, serves every assigned module's bundle. A tool is still served on its own subject and only the module that serves it answers; what moved is the process and the account.
## Context
A module is one self-contained thing ([ADR 0040](0040-what-a-module-is.md)), and it gets a broker
@@ -204,3 +204,14 @@ only, the refresh token only, the manager node only.**
record extends, amended to describe the adapter generalisation.
- The read-only vendor-agnostic analysis, 2026-09-05 (code workspace) — the inventory and the decisions
taken on the open questions this record encodes.
> **The mechanism changed — 2026-10-02, by [ADR 0183](0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md).**
> What stands: `model-access` is one vendor-blind provision for the consumers that do not care which
> vendor answers; a vendor's lifecycle is an adapter's; the carve-out that one node holds a refreshable
> grant's refresh token readably. What moved: the Anthropic adapter is no longer a part of the
> controller's licences context but a module, `claude-licence-manager`, holding the seat
> `anthropic-licence-manager`, with the grants in its own store encrypted with a key the vault made for
> it; and the agent at a terminal is not a consumer of `model-access` — it is coupled to an Anthropic
> grant and uses the seat. The consequence above that the three binding columns *become three ordinary
> consumers of `model-access`* therefore no longer describes the agent's bindings; they are the
> manager's. The static-key adapters and the vendor-blind records stay where this record put them.
@@ -106,3 +106,14 @@ is refused with the candidates named, never resolved by picking.
gap, and the day-one evidence.
- [`03-DESIGN/01-to-be/23-choosing-a-provider.md`](../03-DESIGN/01-to-be/23-choosing-a-provider.md)
— the design.
> **Widened, 2026-10-01 (issue #258).** The pin named a node, on the reasoning that "the same
> module on two machines is two answers, and which machine is the whole question". Half right:
> two modules on one machine can both answer a provision — `public-acme` and `step-ca` both offer
> `acme-ca` on novox — and then which *module* is the whole question, and a node alone cannot ask
> it. A provider is a (node, module) pair ([design 23](../03-DESIGN/01-to-be/23-choosing-a-provider.md)),
> and a pin now names the pair: `pin <node> <provision> <from-node> <module>`. The resolver
> refuses a node that answers twice instead of taking the last one listed, and refuses two
> providers beside the consumer instead of settling them by a map walk — the same stance design 23
> takes: ambiguity is refused, never resolved by picking. Records made before are completed by
> migration where the node they name answers once. mesh-controller: `feat/pin-names-the-provider`.
@@ -47,6 +47,14 @@ Anything with the control plane in reach can ask any module anything it serves.
harder: nothing outside the control plane can, and the control plane's connection is one more
thing on the path of every question — a cost accepted for the audit it buys.
> **The mechanism changed — 2026-09-30, by ADR 0152.** What stands: `ask` on the control plane, and
> that every call passes an account whose permission list says what it may ask. What moved: "nothing
> outside the control plane can" stopped being true when a person's account gained a publish grant per
> tool (design 25 §7, 2026-09-28), and [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)
> takes the first option above for a module as well — a manifest declares `invokes`, and the bus grants
> exactly that publish side. The audit the second option bought is the bus's permission list, which
> derives both.
## How it is checked
A tools-only bed asks a served tool through the control plane and asserts an answer arrived —
@@ -283,3 +283,13 @@ modules in the catalogue require it — so a shared secret is a requirement answ
which is what this record asks for. Private keys are still made where they are used and never
travel, which is the other half and was never in question.
> **The mechanism changed — 2026-10-02, by [ADR 0183](0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md).**
> What stands: every shared secret the mesh makes is the vault's, a private key is made where it is
> used, and a long-lived value a backend issues enters the vault's custody — here as the key the vault
> makes for the licence manager, which encrypts the vendor's grants at rest with it. What this record
> did not foresee: a credential that lives hours, issued by a vendor to the one module that holds its
> grant, and handed by that module to the agent on each node sealed to that node's module key, on
> request/reply over the bus, never through the vault and never as a file the host writes. ADR 0183
> states that as a bounded exception — one vendor, tokens that live hours, one recipient per message —
> and a second such channel is a decision of its own.
@@ -163,6 +163,14 @@ value, the requirement is marked not rotatable by the mesh, and a rotation is re
**The number of parties decides, never the provider.** The resolver knows it from the requirement's
recipients, leaving out the vault's custody copy, so no definition declares it.
> **Progressive insight — 2026-10-01.** The number of parties is the resolver's to know; *which form*
> a single party's credential takes is not, and cannot be: whether a module reads its secret when it
> starts or applies it once to a backend is a fact about the software, visible nowhere in the graph.
> So the definition declares that half — `taken: at-start` or `taken: applied` on an own secret — and
> a secret that declares neither is not rotated, refused with the word to write (issue 180). The
> read-at-start form is built; the staged form for an applied credential is not. The decision stands;
> the sentence above was one fact short.
### Until an adapter can
**An adapter that cannot yet ensure a second credential says so.** The two-party credentials it applies
@@ -81,7 +81,9 @@ closed set stays what its name says it is: the *system's* roles, not everyone's.
- **`the-uplink` → `node-uplink`** ([ADR 0125](0117-a-machines-uplink-is-a-seat.md)). Unheld, so it
renames with no migration.
- **The registry seats — `the-artifact-store`, `npm-package-registry` (→ `mesh-artifact-store`,
`mesh-npm-package-registry`) — and `git` (→ `mesh-git`) — are decided but deferred.** They each
`mesh-npm-package-registry`) — and `git` (→ `mesh-git`) — are decided but deferred.** *(The
mechanism changed — 2026-09-30, by ADR 0156: `the-artifact-store` is renamed, one update and one
alias under ADR 0122; the other two stay deferred.)* They each
*deliver* a provision, so renaming them is a delivering-seat migration: a holder that stops
resolving mid-flight takes a provision away from every consumer. That risk is not worth carrying in
the same pass as the node-* renames, so they keep their names until done deliberately.
@@ -124,6 +124,32 @@ This corrects a fact, not the decision: one statement per endpoint, three things
none of them deciding on its own, all stand. The table in the decision should be read with the filter
column applying to an unrouted endpoint.
## Progressive insight — 2026-10-02, from issue 191
**For a routed endpoint, "the proxy serves the internal name" has to mean "serves it to the private
network", and only the proxy can make it mean that.** The decision says `internal` means the proxy
serves the internal name and not the public one. It does not say to whom, and the proxy answered
every name it routes to any request that carried it, on the same listeners as its public names. A
name being internal kept nobody out: a request from the internet only had to send it. While every
routed endpoint also had a public name, nothing showed it. Once an endpoint could be internal alone
([issue 191](../04-ISSUES/191-a-route-with-only-an-internal-name-is-dropped/00-report.md)), serving
its name to everyone would have published exactly what `internal` was chosen to keep private.
The earlier insight above says the port is not the path for a routed endpoint. This is its other
half: the proxy is the path, so the proxy is where `internal` is enforced. It serves an internal name
only to the machines of the mesh and to the machine itself
([ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md)). Who the mesh is, it is told,
not left to work out: its membership carries the same list of machine addresses the filter's "from the
mesh" is rendered from ([ADR 0167](0167-a-membership-carries-what-its-module-receives-and-who-the-mesh-is.md)).
To anyone else, the name is answered as one never routed, in the handshake and in the request, and not
listed among the names it serves. This holds for the internal name of a `both` endpoint too, whose
outsiders have its public name.
The decision, the options and the consequences stand: one statement per endpoint, three things
derived from it. Checked in the proxy's own tests: an internal-only name is served to a machine the
membership names and to loopback, and refused, unlisted and uncertified for any other request; until
the mesh is issued, it is served to the machine alone.
## Consequences
- **A manifest gains endpoint names, and a route contribution names an endpoint instead of a port.**
@@ -103,6 +103,13 @@ reintroduces 109 and 135 — silently, and on a live mesh, which is exactly how
([issue 110](../04-ISSUES/110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/00-report.md));
and on two of four machines the resolver binds loopback only, so the runtime hands containers a
public resolver instead. Both are prerequisites, not related work.
> **Progressive insight — 2026-09-30, later the same day. The loopback claim was wrong.** The
> resolver bound the private address on all four machines; on two the runtime had never been told
> to use it, and on all four the resolver discarded a query that arrived on the runtime's bridge.
> The step stands; the facts under it were those. Both fixed the same day
> ([issue 110's resolution](../04-ISSUES/110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/01-resolution.md)),
> and step 3 landed after them.
2. **The runtime is told which resolver to use, per machine, as a file** — not per container as a
creation-time argument, or the resolver's address is back in every container's identity and the
problem has only got smaller.
@@ -143,6 +150,16 @@ closed by this record, only answered by it.
resolvable inside the mesh — holds unchanged and by the same means the machine already uses.
- **Issue 110 stops being a container-DNS inconvenience and becomes a prerequisite** for the mesh not
restarting itself whenever it learns a name.
- **A container that names a resolver of its own has opted out of the machine's**, and the copy this
record removes was the only reason such a container could reach anything by a mesh name.
> **Progressive insight — 2026-09-30, the afternoon this landed. Found the hard way.** The mail
> system's admin, behind Mailu's own resolver, lost its database the moment the copy went
> ([issue 171](../04-ISSUES/171-a-modules-own-resolver-knows-no-mesh-name/00-report.md)). A `dns` on
> a container is a decision about whether mesh names exist inside it, not a preference; the module
> was corrected, and whether the controller should refuse the contradiction is that issue's open
> question.
- **A container started by hand gets the mesh's names too**, where before only declared containers did.
Design 08 drew that boundary deliberately, on the grounds that reaching into every container is what
a nameserver would be for. This record accepts that consequence rather than working around it: a
@@ -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
> **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
The repository answers "what runs a module's own code" two ways and reconciles them nowhere
@@ -0,0 +1,99 @@
---
topic: the tiers
status: accepted
date: 2026-09-30
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0066-public-routing-is-name-agnostic.md
---
# 151. A route's internal name is composed under the node that serves it
## Context
A module that requires a route is given two names from one label: a public one, `<label>.<public
domain>`, and an internal one, `<label>.<node>.internal`
([ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md)). Both were composed
from the node the module runs on.
The two are answered differently. The public name is published into every machine's roster at the
address of the node whose proxy serves it ([ADR 0066](0066-public-routing-is-name-agnostic.md)), so
it reaches the proxy from anywhere in the mesh. The internal name is answered by every machine's
resolver as *anything under a node's name goes to that node*
([design 08 §2](../03-DESIGN/01-to-be/08-connectivity.md)) — the node it was composed from, which is
the consumer's. Where the proxy runs on another machine, that name sends a client to a machine with
nothing listening, while the public name works
([issue 139](../04-ISSUES/139-an-internal-route-name-resolves-to-the-consumers-node/00-report.md)).
Every route on this mesh today is served beside its module, so it has not been seen; `route` is
provided mesh-wide precisely so that stops being true.
Beside it, the roster gave every routed name a second entry with the mesh's suffix appended —
`<name>.<public domain>.internal` — because it composed a full name for every entry as it does for a
machine. That name resolved on every machine, was served by nothing, and was refused by the proxy at
the handshake; the first three names tried while reproducing an unrelated issue were those, and the
evidence pointed at a regression that had not happened
([issue 157](../04-ISSUES/157-a-routed-names-internal-alias-is-served-by-nothing/00-report.md)).
## Considered Options
**1. Keep the consumer's name and publish it at the serving node's address**, as the public name is.
The name stays `<label>.<consumer>.internal` and an exact roster entry overrides the wildcard.
Rejected: it makes `<x>.<node>.internal` mean *goes to that node* except when it does not, which is
the one rule the resolver design states; it needs an entry per route where the wildcard needed none;
and which of an exact entry and a wildcard a resolver answers first is the resolver's business, which
the mesh deliberately does not know.
**2. A proxy on every machine, so the serving node is always the consumer's.** Rejected for this
question: it is a different decision about what `route` is — a node-scoped seat with a mesh-wide
fallback — and this mesh runs one proxy on the hub today. Whatever is decided there, a route served
from another machine must have a name that reaches it.
**3. Compose the internal name under the node that serves the route.** Chosen.
## Decision
**A route's internal name is `<label>.<serving node>.internal` — composed under the node whose proxy
answers the route, which is the machine the request arrives at.** The public name is unchanged:
`<label>.<public domain>` of the node the module runs on, which is where the operator put it.
Where the proxy runs beside the module — every route on this mesh today — the two nodes are one and
nothing changes. Where it does not, the name says where the request goes, which is what a name under
a node's name has always meant.
**A routed name has no mesh form.** The roster publishes it as itself, once, at the serving node's
address. Only a machine has a bare name beside its full one.
What certifies the internal name is unchanged by this: the proxy that terminates it obtains a
certificate from the mesh's authority for the names it is given, and it is given this one.
Taken on the operator's standing instruction to answer the open design questions in the work order.
## How this is checked
- **Composition.** A controller test contributes a route from a module on one node to a proxy offered
from another, gathered the way the controller gathers a consumer's contribution for a provider on
another machine, and asserts the internal name carries the serving node.
- **Publication.** A controller test renders a roster with a machine and a routed name and asserts
the routed name appears as itself, once, and never with the suffix appended.
- **On the mesh.** After the change no machine's roster carries a `<domain>.internal` entry, and a
route's internal name still answers from a container with a certificate from the mesh's authority.
## Consequences
- **A route served from another machine now has a usable internal name.** The first module assigned
that way will resolve, where before it would have resolved to the wrong machine with no error.
- **The internal name of a route can change when its proxy moves.** A route re-homed from one proxy
to another gets a new internal name, as the design's rule implies; clients that dialled the old one
reach the old machine. The public name does not move with the proxy and is the stable one.
- **The roster is one line shorter per routed name**, and a person reading a hosts file no longer
finds names that resolve to a refusal.
- **Issue 139's second question — a per-node route holder — is left open**, and is a decision about
what a seat is rather than about a name.
## References
- [issue 139](../04-ISSUES/139-an-internal-route-name-resolves-to-the-consumers-node/00-report.md) — the question
- [issue 157](../04-ISSUES/157-a-routed-names-internal-alias-is-served-by-nothing/00-report.md) — the alias
- [ADR 0066](0066-public-routing-is-name-agnostic.md) — routed names propagate mesh-wide; extended here
- [ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md) — how the two names are composed and how far each reaches
- [design 08 §2](../03-DESIGN/01-to-be/08-connectivity.md) — anything under a node's name goes to that node
@@ -0,0 +1,162 @@
---
topic: what runs on it
status: accepted
date: 2026-09-30
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
---
# 152. The operator's surface is a module the mesh assigns: the console
## Context
**Since the bus moved, nobody can ask the mesh anything without opening a shell on a machine.** Every
tool call an operator's assistant makes fails, on every machine including the one the operator sits
at, with *AMQP not connected*
([issue 147](../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md)).
The program answering is the predecessor's tool server, started on the workstation by hand, with the
predecessor's broker address written into the assistant's own configuration. It has no manifest, no
assignment, no account on the bus, and the mesh has never known it exists. Nothing regressed: the
mesh removed a transport that a program outside the mesh still dials.
**The mesh has a tool model, and it works.** A module serves each tool on its own subject and its
account may serve nothing else ([ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md));
a person is issued an account whose only permission is to publish the tool subjects named at issue
([25 — The bus on NATS](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §7); a client on the
runtime repository's main branch speaks that account as a command line and as an MCP server. Measured
on the live mesh on 2026-09-28: a call to the forge's `gitea_list_repos` answered with real
repositories; the tool list came back empty, because it asks the catalogue for a tool nothing serves
([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)).
**Two records have already said where the surface belongs.** ADR 0132's consequences: *the MCP
surface belongs inside the mesh — a module the mesh assigns to the machine where the agent sits, with
a credential the mesh minted and authority derived from what it may call, not a program started by
hand with a credential printed to a terminal.* Design [33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md)
§6 says the same. Neither is a decision about the surface: 0132 decided where a seat's tools live,
and named the surface in passing.
**And one record says the opposite, in the letter.** [ADR 0095](0095-the-control-plane-is-the-way-to-ask-a-module.md)
made the control plane *the* way to ask a module, deferred "calling is a grant" until something asked
for it, and recorded that *nothing outside the control plane can*. A person's account (design 25 §7,
built 2026-09-28) is exactly that grant, minted for a person. So 0095's exclusivity has already been
widened once without a record saying so; a module that calls tools widens it a second time, and this
record is where that is said.
**What a module's account may do today, counted from the composition** (`internal/broker`): publish
its own events, publish the accept subjects of seats it uses, subscribe its own tools and what it
consumes. No module principal may publish another module's tool subject. Of 72 modules in the
catalogue, 45 serve tools and 0 may call one.
The question the work order asks before any code: **does the mesh grow its own operator surface, or is
the surface an ordinary module that happens to serve tools?**
## Considered Options
**1. The control plane serves the agent protocol itself** — a listener on the controller, or a verb
its binary runs. Rejected. It puts a surface for tools the control plane does not implement on the one
component that must stay answerable while it is itself being replaced, which is the reason 0132
rejected the control plane as the answer to discovery. A person on a workstation would reach it over
the network, and the networked surface [ADR 0035](0035-one-implementation-several-surfaces.md)
reserves for that authenticates through an OAuth2 provider that is not configured — so the controller's
`api` verb correctly serves nothing today, and this option would either wait for it or bypass it.
**2. A program a person installs and starts by hand with a printed credential** — what exists on the
runtime repository's main. Rejected as the end state. It is outside the mesh in every way issue 147
names: no assignment, no declaration, no seat, no check that it reaches anything, revoked only by a
person remembering to. It is the predecessor's arrangement one bus later, and it fails the same way
the next time an address moves. It stays as the recovery path, the way the command line does
(ADR 0035): a credential from `operator issue` and the `mesh` client work with no console assigned.
**3. An ordinary module, assigned to the machine the person sits at, holding a credential the mesh
minted, serving the mesh's tools on that machine's loopback.** Chosen.
## Decision
**The console is a module.** `mesh-console` is built by the mesh, registered like any module,
assigned to a machine, and given a bus credential sealed to that machine. It serves the mesh's tools to
whoever is on that machine: to an agent over MCP, and to a person through the same endpoint. Assigning
it to a machine is what makes the mesh reachable from there; unassigning it revokes that, at the next
composition, with nothing on the machine to remember to remove.
**A grant to call is a manifest word: `invokes`.** A module declares the tools it calls, each as
`<module>.<tool>`, or the single entry `*` for every tool on the mesh. The bus grants exactly that
publish side and nothing beside it — no event, no subscription, no seat. This is ADR 0095's deferred
first option, taken now that a consumer asks for it; a person's account already has this shape, and
the same composition derives both. The control plane's `ask` stands, and 0095's audit point with it:
every call still passes one account whose permission list says what it may ask.
**Authority is the machine's login.** The console listens on the machine's loopback only, declared
`from: machine`, so whoever can open a socket on the machine is whoever owns the machine, and *the
account that installed the host owns the mesh on that node*
([ADR 0034](0034-the-local-account-owns-the-mesh.md)). Anything on a machine may call anything on it,
and that is the whole of local ([ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md)).
The mesh knows no person: what the audit sees is which console asked, under the account
`<node>.mesh-console`. How a person's identity reaches a session is the question design 15 leaves
open, and this record does not close it.
**What the console lists is asked of the modules.** Design 33 §5: a module's own tools are answered by
the module, from the code that defines them. The tool runtime therefore answers one reserved verb for
every module it serves — `tools`, the module's tool names, descriptions and argument schemas — and the
console assembles its list by asking the catalogue which modules the mesh holds and each module what
it answers. A module that is not running is absent from the list and says so; a tool an agent already
knows the name of can be called whether or not it was listed. A module may not name a tool of its own
`tools`; the runtime refuses the collision at load rather than letting one shadow the other. A
role's tools, and the mesh's own verbs, join the list when the `mesh-controller` seat serves them
(design 33 §1, third family) — the console reads whatever the mesh can say about itself, and grows as
that does.
**The console holds one credential and one grant: `*`.** It is the operator's surface on a machine the
operator owns; narrowing what it may call is a setting on its assignment, which
[ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md) already provides for
and nothing here builds.
> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).** The surface stays a module assigned per node, on loopback, with the machine's login as the authority. It is no longer a container: it is the node tools runtime's serving mode, host-side, and that runtime also serves every assigned module's tools. The module is renamed `node-tools`.
## Consequences
- **The way a person drives the mesh is inside the mesh.** It is declared, delivered, replaced and
revoked by the same machinery as everything else, and `status` says whether the machine carrying it
has applied. Issue 147's shape — a surface kept alive by an address in a file — cannot recur, because
there is no file: the console's credential names the bus the mesh is on, and moves when it does.
- **A module may now call tools, which ADR 0095 had reserved to the control plane.** The grant is
explicit, per tool or `*`, and derived by the same composition that grants everything else. A module
that declares no `invokes` gains nothing. What got harder: a manifest reviewer has one more field to
read for authority, and `*` in it deserves the reader's attention every time.
- **A new manifest word ships one release before any manifest uses it**, and must reach both parsers:
the build machine's and the running controller's. The console's manifest cannot be registered until
the controller and the builder that packages it have been rebuilt with the word.
- **Discovery costs a fan-out per list.** One request per module the mesh holds, answered at once by
the bus for every module nothing serves, so the cost is bounded by the modules that are up. The list
is cached briefly in the console; a module assigned a moment ago appears at the next refresh.
- **Every tool runtime must be rebuilt once** to answer `tools`. Until a module is, it is callable and
not listed, and the console says which modules did not answer.
- **The mesh's own verbs are not in the console yet.** `status`, `push`, `assign` are the
`mesh-controller` seat's tools under 0132, and the three prerequisites 0132 names are still not in
place. A person asking what a node runs still opens a shell for that question, and that gap is design
33's to close, not this record's — recorded here so nobody reads the console as the whole of 147.
- **The person's credential is not retired.** `operator issue` and the `mesh` client remain the path
when no console is assigned, and the path an operator uses to bring a mesh up far enough to assign
one.
## How this is checked
| Rule | Checked by |
|---|---|
| A module's `invokes` becomes exactly that publish grant, and nothing else | the bus user composition test: a module invoking `shop.price` may publish that subject and no other tool's; `*` may publish every tool subject; neither may publish an event or subscribe anything it did not consume |
| A malformed `invokes` entry is refused at registration | a parser test: an entry naming no tool is a problem named in the manifest's words |
| The runtime answers `tools` for every module it serves | the runtime's test: a module registering two tools answers three names, and a module naming one of its own `tools` is refused at load |
| The console's list is what the modules answer | the client's test against a real bus: two modules up, a third the catalogue holds and nothing serves, and the list carries the two and names the third as not answering |
| A call from the console reaches a module over the bus | the same test, and the live mesh: the console assigned to a workstation answers `tools/list` on its loopback and a call to the forge returns repositories |
| The console listens on loopback and nowhere else | its manifest declares `from: machine`, and the filter composed for the machine opens nothing for it |
## References
- [issue 147](../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md) — the surface outside the mesh
- [ADR 0095](0095-the-control-plane-is-the-way-to-ask-a-module.md) — extended: a grant to call, for a module as for a person
- [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) — where a seat's tools live, and the sentence that named the surface
- [ADR 0035](0035-one-implementation-several-surfaces.md) — three surfaces over one implementation
- [ADR 0034](0034-the-local-account-owns-the-mesh.md), [ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md) — why loopback is the authority boundary
- [33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §5, §6 — discovery, and what serves it to an agent
- [34 — The console](../03-DESIGN/01-to-be/34-the-console.md) — the design this record authorises
- mesh-tools `src/client.ts`, `src/mcp.ts`, `src/mesh.ts` — the client this makes a module of
@@ -0,0 +1,113 @@
---
topic: how we work
status: accepted
date: 2026-09-30
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
---
# 153. The record is read by a module the mesh assigns, and the console lists it
## Context
[ADR 0025](0025-the-design-record-is-read-not-copied.md) decided that this repository is **read where
it is written, never copied to be found**: an agent reads it directly, and a search of the mesh's
memory consults that agent so its answers appear beside ordinary results. It named the check that
closes [issue 006](../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md): search
for a phrase that appears only in a design document here, and get it back. It gated the build on an
agent that did not exist — the mesh session of
[15 — The agent session](../03-DESIGN/01-to-be/15-the-agent-session.md) — and on a search that
does not exist either, now: the knowledge base 0025 meant was the predecessor's, and since the
cut-over nothing reaches it ([issue 147](../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md)).
**So the two halves of 0025 have no home.** There is no store to be "beside", and no session to be
the reader. What the mesh has instead, since today: a tool model in which every module answers what
it serves, and a console on the machine a person sits at that lists every tool the running modules
answer ([ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)). An agent holding the
console does not search a store; it reads a tool list and calls what fits the question.
**What 0025 could not tolerate was a derived copy** — the enforced copy winning while the reasoned one
quietly stops being true. It rejected a sync for that reason and for no other. A git checkout is not a
derived copy: it is the same bytes at a commit the answer names, and the only way it can differ from
the source is by lagging behind it, which is measurable and stated. 0025's own words allow it —
*retrieval is an agent reading this repository, not a copy living in a second store* — and the
transformation that makes a copy dangerous is exactly what a checkout does not do.
## Considered Options
**1. Wait for the mesh session.** Rejected. Design 15 is `designed` with nothing built, its model
access is a provisions question with no consumer identity yet, and 006 has waited since 2026-08-23.
A record whose check cannot run is a rule enforced by nothing.
**2. The console reads the repository itself.** Rejected. The console holds nothing and decides
nothing (ADR 0152, ADR 0035); a reader inside it would be a second implementation of a thing that
should be one module, unavailable to a person's client and to any other module.
**3. A module that keeps a checkout of the repository and answers questions about it, listed by the
console like any tool.** Chosen.
## Decision
**The reader is a module: `records`.** It requires the `git` provision — the forge — clones the
repository its settings name, keeps the checkout current on every merge the forge announces and on a
timer, and answers over the bus: where a phrase appears as written (document, line, nearest heading),
one document whole, what a folder holds, and where the checkout stands — always with the commit it
read. Nothing is indexed, ranked or summarised: a design record is found by its own words, and a
reader deciding which words matter would be a second opinion about somebody else's document.
**The repository is a setting, not a manifest field.** The module names no mesh
([ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md)): which repository it reads is
the assignment's business, and an installation that keeps its record elsewhere sets that. Until a
repository is set it serves no tools and says why. Public repositories only; it asks for no
credential, because a secret it did not need would be one more thing to seal.
**"Beside everything else" is the console's tool list.** 0025's second half — the search consults the
agent — has no store to consult and needs none: the console lists `records_search` beside the forge's
tools and the mesh's own verbs, with a description that says when to call it, and an agent choosing
tools for a symptom is the search. That is surfacing, not merely reaching: nobody has to know this
repository exists to be offered it.
**Reading stays one-way.** The module reads the forge and answers; nothing flows back into the
repository. It holds no credential that could write.
**The mesh session, when it exists, is a caller of this module, not a replacement for it.** Design 15's
*it holds the design record by reading it* is satisfied by asking `records`; the session brings
judgement, this brings the text.
## Consequences
- **Issue 006 closes on 0025's own check**, run through the console: `records_search` for a phrase
that appears in one design document here returns that document. The module's test does the same
against a repository it makes.
- **The as-is knowledge document is rewritten.** [`07-knowledge.md`](../03-DESIGN/00-as-is/07-knowledge.md)
described the predecessor's two stores; neither is reachable from the mesh, and what the mesh knows
is now what its modules answer. Saying otherwise is the failure this repository exists to name.
- **A checkout lags.** Between a merge and the next sync — seconds when the forge announces it,
minutes when it does not — an answer is the previous commit's, and says which. That is the cost of
no copy, and it is a number rather than a silence.
- **The reader depends on the forge module's event, by name.** `consumes: gitea.pull.merged` names a
module rather than the `git` seat, because the seat declares no events. A forge that is not gitea
leaves the timer as the only refresh, which still works.
- **What got harder:** the record is now reachable from every machine holding a console, which is what
was wanted, and a reader must remember that this repository is public and the mesh is not — the
module reads the public repository and nothing about the installation.
## How this is checked
| Rule | Checked by |
|---|---|
| A phrase in one document comes back from where it is written, with the commit | the module's test against a repository it makes; and live, through the console |
| A merge on the origin is pulled and the next answer names the new commit | the same test |
| A path outside the checkout is refused, not resolved | a test per shape |
| A failed sync leaves the checkout standing and is said | a test against an unreachable origin |
| Without a repository set, no tools are served and the log says why | the module's own start |
| The console lists `records_search` beside every other tool | the console's listing, live |
## References
- [ADR 0025](0025-the-design-record-is-read-not-copied.md) — extended: the reader is a module, the search is the console's list
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — what lists it
- [issue 006](../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md) — what closes
- [35 — Reading the record](../03-DESIGN/01-to-be/35-reading-the-record.md) — the design
- mesh-catalog `modules/records` — the module (PR 183)
@@ -0,0 +1,140 @@
---
topic: the mesh
status: accepted
date: 2026-09-30
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
---
# 154. The mesh's own verbs are the mesh-controller seat's tools, and which verbs those are
## Context
[ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) decided that a seat's protocol
carries its tools in full, that holding a seat means serving them, and that the mesh's own verbs are
the `mesh-controller` seat's. It named three prerequisites, none in place: the protocol in the store
rather than in compiled defaults; a protocol richer than a list of verbs; a node-scoped seat's subject
carrying the node. And it left one thing to a decision per seat: **which verbs each seat serves**,
because a seat's tools bind every future holder.
The console shipped the same day ([ADR 0152](0152-the-operators-surface-is-a-module-the-console.md))
and made the gap visible from the operator's chair: a person on a workstation could call every tool a
*module* serves and none of the mesh's own. What a node runs, what is assigned, whether a push
applied — the questions issue 147 opened with — still meant a shell on the control node. The console's
own handshake said so.
The control plane already answers every one of those questions, as commands: `status --json`,
`node show`, `plan --json`, `assign`, `push`. [ADR 0035](0035-one-implementation-several-surfaces.md)
says a surface is an adapter over those with no decisions in it, and the `api` verb proves the shape:
every route calls the function the command line calls.
## Considered Options
**1. Leave the mesh's verbs to the shell until an identity provider authenticates the HTTP API.**
Rejected. The authenticated network surface is for a browser on another machine; the console is
already behind the machine's login (0152), and the bus already carries every other tool call under an
account whose permission list says what it may ask. Waiting would keep the one surface the mesh has
from answering the mesh's own questions, for a reason that does not apply to it.
**2. Serve the verbs as the mesh-controller *module's* tools, `mesh.mod.mesh-controller.tool.<verb>`.**
Rejected; 0132 rejected it already. The controller holds a seat, and the verbs must keep their address
while the control plane is being replaced, which is the moment they are most needed. A module's name
would change with the implementation; the seat's does not.
**3. Call each command's function inside the serving process.** Rejected on two facts: the commands
print, to the process's standard output, and two calls answered at once would read each other's
words; and each command opens and closes its own stores, which the serving process holds open. Making
every command return a value is the larger refactor, and it would give the tools a second code path to
keep in step with the command line — the thing ADR 0035 forbids.
**4. The holder of the seat runs the command it names, in its own binary, and answers what it
printed.** Chosen.
## Decision
**The `mesh-controller` seat serves twelve verbs**, and these are its interface, additive within a
version ([33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §7):
| verb | answers with | takes |
|---|---|---|
| `tools` | every seat's tools, from the mesh's records | nothing |
| `status` | what is wrong, quiet, behind, waiting — `status --json` | nothing |
| `nodes` | every machine and its mode | nothing |
| `node` | what one machine reported, what it is assigned, why | `node` |
| `modules` | every module, its version, commit and machines | nothing |
| `seats` | every seat, what it delivers, who holds it — `seats --json` | nothing |
| `builds` | what was built lately and what came of it | `module` (optional) |
| `plan` | the declaration a machine would be sent — `plan --json` | `node` |
| `assign`, `unassign` | the mesh's own words, refusal included | `node`, `module` |
| `push` | that it was sent; `status` says what the machine did | `node` (optional: every machine behind) |
| `build` | that the build machine was asked; `builds` says what came of it | `repository`, `path`, `ref` |
**Each verb runs the command it names, in the controller's own binary, and answers what the command
printed** — the output, whether it succeeded, and, where the command speaks JSON, the same as data. A
refusal is the command's refusal in the command's words, because it is the same output. A verb takes
only the arguments its schema names; nothing reaches a flag the schema did not declare. `push` and
`build` are sent and not waited for: a call that blocked for a whole apply would time out on every
machine that takes a minute and say nothing about the others.
**The three prerequisites are built.** A seat's protocol is three columns on its row, seeded from the
compiled defaults where a row had none and additively thereafter, so a verb a release adds joins the
row and nothing an operator wrote is taken away. A served verb is its name, what it does, and the
schema of its arguments and answer; a manifest may still write a bare name. A node-scoped seat's tool
carries the node it is asked of, as the last token of its subject; a mesh-scoped seat's stays flat.
**Holding a mesh seat requires serving its verbs**, judged where the store's set is loaded, and the
refusal names the missing verbs. The controller's own manifest lists the twelve under `tools`.
**Discovery reads the records, through the seat.** `tools` is one of the twelve because the console
cannot read the store and should not: the mesh answers for its own records through the role that owns
them, and the answer is true while any *other* holder restarts. It is not true while the control plane
itself restarts, and the console says so rather than hiding the modules' tools with it.
**A grant of `*` reaches a role's tools; `seat:<seat>.<verb>` grants one.** The console's `*` needed no
change to reach the mesh's verbs, which is what a grant meaning *every tool* should mean.
> **The mechanism changed — 2026-10-02, by [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).**
> The seat gains a generic verb beside the named ones: `command`, which takes one command line as the
> controller's binary takes it and answers what it printed. The named verbs stand and keep their
> schemas; `command` is the whole binary, added because the operator decided any node may call any
> tool and a verb per command was the only thing keeping `node account`, `node show` and the rest
> behind a shell on the control node. Additive within the version, as §"additive" above allows.
## Consequences
- **The console answers the mesh's own questions.** Issue 147's first paragraph closes: what a node
runs, what is assigned, whether a push applied, from the machine the person sits at, over the bus,
under an account whose permission list says so.
- **Whoever may call `mesh-controller.push` may change the mesh.** That is the console's `*` on a
machine whose login owns the mesh (0152), and a person's account only if `operator issue` says so.
A grant reviewer reads `*` and `seat:mesh-controller.` with the same care.
- **A verb here binds every future controller.** Twelve is deliberate: what an operator asks weekly,
and nothing that is still finding its shape (`take`, `converge`, `settings`, `secret` stay commands).
- **A command's text is the answer**, and text changes. The three verbs that speak JSON carry it as
data; the rest are read by a person or an agent, not parsed. Anything that needs a shape asks for
`--json` to be added to the command first, which is the right order.
- **What got harder:** the mesh-controller seat's row now carries a protocol an operator could edit, and
a verb removed from the row is a verb the controller stops serving without a build. That is
ADR 0122's arrangement applied to tools, and `seats` shows the row.
## How this is checked
| Rule | Checked by |
|---|---|
| Every declared verb is one the binary can run, with the arguments its schema names | a test walks the table and derives a command line for each |
| A verb missing a required argument is refused in its own words, before anything runs | a test per shape |
| A holder that does not serve a mesh seat's verbs cannot hold it, and the refusal names them | a catalogue test against a seat with two verbs and a holder with one |
| A node-scoped seat's tool carries the node; a mesh seat's does not | the bus composition test: two nodes derive two addresses |
| The controller subscribes its seat's tools and may answer | the composition test, and the golden user list |
| `*` reaches a role's tools; `seat:` grants one and refuses a name with no verb | the composition test |
| The protocol is seeded into the row and widened additively | the store-backed seat test |
| Live: the console lists `mesh-controller.status` and a call answers what `status --json` prints | the rollout of this record |
## References
- [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) — extended: the prerequisites built, the verbs decided
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — the surface that lists them
- [ADR 0035](0035-one-implementation-several-surfaces.md) — a surface is an adapter with no decisions in it
- [ADR 0122](0122-a-seat-is-data-a-rename-is-a-database-update.md) — the row is the mesh's, and now carries the protocol
- [33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) — the design this completes
@@ -0,0 +1,128 @@
---
topic: what runs on it
status: accepted
date: 2026-09-30
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
---
# 155. A definition names no installation: how that is checked, and the three ways a value that did gets out
## Context
[ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) decided that a module definition
names no node, no mesh and no host path, and said how the name half is checked: *a catalogue test
finds no domain name in any definition value*. No such test existed
([issue 134](../04-ISSUES/134-a-definition-may-still-name-the-mesh/00-report.md)). Written and run
over the 77 definitions on 2026-09-30, the check it describes finds **42 values**, in 15 definitions,
and they are of four kinds that want four different answers:
| kind | count | example |
|---|---|---|
| a service told its own public name as a literal | 5 | an identity provider's `KC_HOSTNAME`, an object store's console redirect, an automation tool's webhook URL |
| an operator's value written into the definition | 10 | a mail server's domain, site name, website, and the address it trusts a real-IP header from |
| this mesh's forge, by URL, as a recipe's build context | 2 | the builder and the proxy, which package the controller's source |
| an application built outside the mesh, pulled from this mesh's registry | 7 | four sites and tools whose repositories are the operator's own |
| the world's servers, named by upstream defaults | 10 | a Matrix homeserver's trusted key server, Element's integration manager |
| a module named after the domain it serves | 8 | one site module, with its paths and network named after it |
Not one was careless. Each was the value the software needs, and until today there was nowhere else
to put it ([issue 122](../04-ISSUES/122-a-module-cannot-ask-for-its-own-public-name/00-report.md)).
Two of the answers were built before this record: a module is told the name its route composes
(`${bound:<route>:name}`, controller PR 149, 2026-09-30), and a source may be a path on the git seat
([ADR 0111](0111-a-build-source-is-on-the-git-seat-or-external.md)). What was missing: an operator's
value in a file the software reads, the same for a build *context*, a way to say a name is meant, and
the check.
## Considered Options
**1. A string search for the installation's own names.** Rejected. The controller is as
mesh-agnostic as the definitions; it does not know which names are "this mesh's", and a check that had
to be told would be configured per installation and pass everywhere else. What it can know is the
*shape*: a name under a public top-level domain, a public address.
**2. Report every such shape.** Rejected. Eight of the 42 were `why` strings — prose the mesh never
reads, explaining what a port is for — and a check that reports those beside `KC_HOSTNAME` teaches
people to ignore the report. And a Matrix homeserver *must* name the federation's public key server;
a check with no way to say so would be a check people argue with rather than obey.
**3. Judge what the mesh acts on; let a definition say which names it means, one by one, with a
reason; exempt the world's services that a definition may name as a policy default.** Chosen.
**For an operator's value**, one option was to wait for design 27's requirement form in full. Rejected
for the reason 0112 gave against a slow operator provider: if asking a person for a value takes more
than a setting, module authors route around it and the literals come back. `${setting:<key>}` is the
operator provider in its first form, on the settings a module already has.
## Decision
**The check.** Every string value of a definition that the mesh acts on is judged for a hostname under
a public top-level domain and for a public address. Not judged: `why` and `description`, which are
prose. Allowed where they can only mean the world: the public registries an `image` may be pulled
from, the public resolvers a machine may forward to, and the public certificate authorities' ACME
directories. The container runtime's alias for its own host is the runtime's. A module's own name is a
value too. The check runs in `module check` and as a catalogue-wide test; **it does not yet refuse at
registration**, because the list it prints is the list that shrinks, and a registration that refused a
manifest whose only remedy is a merge elsewhere would refuse the mesh's own catalogue on the day the
check landed. It moves to registration when the list has been empty for a release.
> **Progressive insight — 2026-09-30.** The list was empty the day the check landed — every remaining name declared with its reason — and the operator asked for registration to refuse at once rather than after a release. It does, since mesh-controller PR 175: `module add` and a build's result are refused in the check's words, naming the way out, and the build stays recorded. The decision stands; only the day moved.
**A name a definition means is declared with its reason.** `names-on-purpose` on a resource maps each
such name to why: *the federation's public key server, the world's*; *built outside the mesh, from the
application's own repository, until that repository is a build source on the git seat*. A name the map
does not cover is still reported. The host never sees the word.
**An operator's value reaches a file as `${setting:<key>}`**, filled from the module's settings
layers — the mesh's, then the node's — the same layers a mergeable file and a contribution take, so
`settings set <module>` stays the one place a person's values go. Refused, naming the key and the
command, when nothing set it: a default for a mail domain would be the literal this removes, and a
blank written silently would be a service that comes up wrong somewhere that names nothing.
**A build context may live on the git seat.** `context: {"seat": "git", "repository": "<owner>/<name>"}`
is composed by the mesh that builds it: the request carries each seat's clone base, and a builder told
no base for a seat a context names refuses the build by the seat's name rather than guessing a forge.
**A module is named for what it is.** The site module named after its domain is `website`.
## Consequences
- **The catalogue names no installation, and a test says so.** The forty-two became zero the same day,
by the four answers above; seven of them are declared on purpose and stay visible as the list to
shrink — four applications the mesh does not build yet.
- **An operator's values are the assignment's.** The mail module takes its domain, its site name, its
website and the address it trusts a real-IP header from as settings; a mesh that installs it without
them is refused at composition, by name, which is the right moment. The module's own README says
which.
- **What got harder:** a manifest reviewer has one more word to read, and `names-on-purpose` on an
application's image is a debt visible in the definition until the application is built here. A
reader of `settings set` output sees more keys than files, because a key a file asks for is a
destination too.
- **Not decided here:** ADR 0112's requirement form (design 27) still replaces `${setting:…}` and the
other placeholders when it lands; this is its first case, the way [ADR 0038](0038-the-mesh-assigns-the-port.md)
was for ports. Host paths ([issue 119](../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md))
are the next step of the same group, and the registry's name ([issue 123](../04-ISSUES/123-the-image-registry-is-named-after-a-role/00-report.md))
the one after.
## How this is checked
| Rule | Checked by |
|---|---|
| A value naming an installation is reported at its path, in the definition's words | a catalogue unit test over a definition with a hostname in an env value and a public address in a file |
| Prose, the world's registries in an image, public resolvers, ACME directories and the runtime's own alias are not reported | the same tests |
| A name declared on purpose is not reported; a name beside it that is not declared is | a test with a homeserver's config |
| An image from an installation's registry needs a reason | a test without and with the word |
| A module named after a domain is reported | a test |
| No definition in the catalogue names an installation | `TestNoCatalogueManifestNamesAnInstallation` over the checkout, and `module check modules/` |
| A definition naming an installation is refused at registration, and one declaring its names passes | `TestRegistrationRefusesADefinitionNamingAnInstallation` (2026-09-30) |
| `${setting:key}` fills from the layers, node over mesh; refused by name when unset; not stray when set | three tests |
| A context on a seat is cloned from the base the mesh sent; a seat with no base is refused by name | the builder's test |
## References
- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) — extended: the check it promised, and the operator provider's first form
- [ADR 0111](0111-a-build-source-is-on-the-git-seat-or-external.md) — a source on the seat; now a context too
- [issue 122](../04-ISSUES/122-a-module-cannot-ask-for-its-own-public-name/00-report.md), [issue 134](../04-ISSUES/134-a-definition-may-still-name-the-mesh/00-report.md) — what this closes
- [27 — A module requires, the mesh resolves](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md) — where this sits in the larger design
- mesh-controller PR 169, mesh-catalog PR 188 — the check, the words, and the catalogue that passes it
@@ -0,0 +1,83 @@
---
topic: the mesh
status: accepted
date: 2026-09-30
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0075-two-stores-and-which-provides-what.md
---
# 156. An artifact is what a build produces, the artifact store serves every kind, and its seat is named for its scope
## Context
[Issue 123](../04-ISSUES/123-the-image-registry-is-named-after-a-role/00-report.md) found three
wordings disagreeing about the mesh's registry. The glossary defined *artifact* as "an OCI image, by
digest"; the manifest's build vocabulary names four kinds — `image`, `upstream`, `bundle`, `archive` —
and the catalogue builds all four; the seat was `the-artifact-store`, the last of the mesh's own seats
named after the job it does rather than for the mesh
([ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) decided the
rename and deferred it). The issue asked whether the seat and provision should be renamed after
images, and whether the mesh needs two registry implementations at all.
Reading what the store actually serves settles the first question the other way. A kept reference
has two shapes — `artifact-store://<module>/<artifact>@sha256:…` for an image and
`artifact-store://<module>/<artifact>/blobs/sha256:…` for an archive — and both are served by the
same OCI registry, by digest. [ADR 0075](0075-two-stores-and-which-provides-what.md) already
defined the provision that way: *content-addressed blobs, pinned by digest, no versions, no ranges;
what the mesh delivers to machines*. The provision was never an image registry. Only the glossary said
so, and only the seat's name was odd.
## Considered Options
**1. Rename the seat and the provision after images.** Rejected. The store serves archives too, by the
same protocol; naming it for one kind would be the glossary's mistake made permanent, in the name
every manifest uses.
**2. Fix the word, rename the seat for its scope, keep the provision.** Chosen. The rename ADR 0121
deferred as a delivering-seat migration is, since [ADR 0122](0122-a-seat-is-data-a-rename-is-a-database-update.md),
one update and one alias: the former name resolves forever, a held record follows by cascade, a claim
written with the old name still holds.
**On two implementations:** left as 0075 decided. Two provisions because two protocols; the OCI
registry the genesis installs because something must serve images before the mesh can build; the
forge may provide `artifact-store` too and a mesh may choose it. The bootstrap argument is weaker than
it reads, as 123 says, and the day the forge is raised at genesis and adopted in place is the day to
retire the second server — a migration a mesh performs, not a decision to take here.
## Decision
- **An artifact is anything a build produces** — an image, a mirrored upstream image, a bundle, an
archive — and the glossary says so. *Image* is one kind. A module is not an image; a module may
build several artifacts and install none.
- **The artifact store serves artifacts of every kind a machine fetches**, images and archives, by
digest, over the OCI registry protocol. The provision keeps its name.
- **The seat is `mesh-artifact-store`.** `the-artifact-store` is its alias. The catalogue's registry
module claims the new name; a definition elsewhere claiming the old one still holds.
- The two other deferred renames — `npm-package-registry` and `git` — stay deferred, and for the
same reason no longer. They are one migration each when wanted; nothing here needs them.
## Consequences
- The glossary stops contradicting the manifest vocabulary, and a reader of `artifact-store` reads
it as what it is: where the mesh's built things are kept.
- One migration on the seat table; no manifest but the registry's changes; no consumer of the
provision changes, because the provision did not.
- **What got harder:** nothing measurable. A record that says `the-artifact-store` is read through the
alias; design 26's table already carried the new name as intent.
## How this is checked
| Rule | Checked by |
|---|---|
| The former name resolves to the seat once the store's aliases are loaded | a catalogue test |
| The seat is in the compiled set under its new name, delivering `artifact-store` | the seat tests, updated |
| The registry module holds the seat under the new name on the live mesh | `seats` after the rollout |
| The glossary's *artifact* matches the build kinds a manifest may declare | design 18's table and the showcase module list the kinds; the glossary names the same four |
## References
- [issue 123](../04-ISSUES/123-the-image-registry-is-named-after-a-role/00-report.md)
- [ADR 0075](0075-two-stores-and-which-provides-what.md) — extended: the provision as defined stands, the word is corrected
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0122](0122-a-seat-is-data-a-rename-is-a-database-update.md) — the rename, decided and made cheap
- mesh-controller migration 0048; mesh-catalog `modules/distribution`
@@ -0,0 +1,99 @@
---
topic: the mesh
status: accepted
date: 2026-10-01
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md
---
# 157. A build says what it does on the bus, as it happens
## Context
[ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) made a build work submitted to a role: the
build-machine seat accepts a build and emits its outcome, one publish that reaches whoever asked, the
controller that records it and the catalogue that places it. Everything **between** the request and
the outcome — which command is running, how long it has taken, where it hung, the compiler's error,
the clone's refusal — lived in one container's standard error on one machine.
The night of 2026-09-30 showed the cost three times over. A build that failed showed a person one
line, the first of its failure, in the controller's `builds`; the rest was read with `docker logs` over
ssh, which the mesh's own rule forbids. A build that ran for minutes could not be told from one that
had hung. And the builder has no tools and emits nothing but the outcome, so the console
([ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)) had nothing to show while a
build ran, and no viewer could be built on top of it. The operator's ask was plain: the builder is to
be fully transparent, with its log on the bus, so that a log viewer can be built on the bus later.
## Considered Options
1. **Keep the log in the outcome.** The result carries the whole log when the build ends. Nothing new
on the bus; nothing while the build runs; a viewer sees a build only once it is over, which is
exactly when the log matters least.
2. **A log store.** The builder writes its log to a file or a table and a tool reads it. A second
place to keep something the bus already carries, with its own retention, access and failure modes,
and no live reading without inventing a subscription over it.
3. **The log is the role's own events.** Two more events on the build-machine seat beside `built`:
`started` when work is taken, and `log.<build id>` for every line, published as the build runs.
The events stream already retains every role's events for a week, so a reader follows a build
live by subscribing its subject, or reads it back afterwards from the stream, and a viewer is a
subscriber and nothing more.
## Decision
**Option 3.** A build machine says everything it does on the bus, as the role it holds, under the
build's id, and the mesh keeps no other copy.
- The build-machine seat's protocol gains `started` and `log.*`. A holder may therefore publish
`mesh.seat.mesh-build-machine.event.started` and `…event.log.<id>`, and no other subject, by the
same derivation every seat's grants follow ([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)).
The event's tail token is the build's id, so one build is one subject: a reader filters by subject
alone, on the server, and a week of other builds does not travel to show one.
- **Every line goes two ways**: to the machine's own standard error as before, and onto the bus. That
includes every command the builder runs, its duration and its failure, and on failure the command's
own output line by line — the compiler's words, the clone's refusal. A build machine with nobody
listening still prints; a listener reads the same lines.
- A line is a core publish, unawaited. The stream that holds the role's events captures it on its way
through, and a build does not slow to the pace of an acknowledgement per line. Each line carries a
sequence number from one, so a reader who joined late, or reads two copies, sees order and gaps.
`started` and `built` are published into the stream and awaited, because they are the two facts a
later reader must never find missing.
- **The mesh reads it back from the stream**, never from a record of its own: `builds --log <id>`, and
the same verb on the controller's seat ([ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)),
reads one build's subject with a consumer that is gone when the reading is done. `builds` lists
each build's id beside it, and `build` says the id it asked with, so a person can follow.
- Nothing is declared by the builder module for this. The protocol is the seat's, seeded additively
into the store ([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)), and the
holder's grant follows on the next composition of the broker node.
## Consequences
- A build is watchable while it runs, from anywhere on the mesh, with no access to the build machine.
The console's gap of 2026-10-01 — no live progress, no per-merge view — closes on the progress half;
the per-merge view is a reader over these subjects and the outcome, and is not built here.
- A log viewer on the bus is now a plain subscriber: live on `mesh.seat.mesh-build-machine.event.>`,
historical from the events stream filtered by a build's subject. NATS carries and retains; it does
not view. The `nats` command-line client can tail or replay a subject today; a viewer of our own is
later work and needs nothing more from the builder.
- The events stream grows by a build's log per build, for a week. A build is a few hundred lines; the
stream's limits are the bound, as for every other event, and a stream that fills drops the oldest.
- A line the bus did not take is lost, deliberately, and visible as a gap in the sequence. The outcome
is not affected: a build's result never depended on its narration.
## How this is checked
| Rule | Checked by |
|---|---|
| The seat's holder may publish `started` and `log.<id>` and nothing wider | `TestTheBuildMachineMaySayWhatItDoesUnderTheBuildsId` (broker) |
| A build's lines reach a reader of its subject in order, and the stream holds them afterwards | `TestNatsABuildIsTakenAndItsOutcomeReachesEverybody` against a real server (link) |
| The seat verb `builds` with a build's id reads that build's log | `TestBuildsWithAnIdReadsThatBuildsLog` |
| Every command the builder runs is said, with its output on failure | `Command` speaks through the hook every build sets; the builder's tests still see the lines on standard error when nothing listens |
| Live: a build triggered after the roll-out is readable line by line through the console | done by hand after the merge of mesh-controller PR — see the design's note |
## References
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) — extended: the role now narrates as well as answers
- [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md), [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md) — the protocol and the verb
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — the console this feeds
- [Design 25 — The bus on NATS](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §3, [Design 18 — Building a module](../03-DESIGN/01-to-be/18-building-a-module.md)
- [Issue 176](../04-ISSUES/176-the-consoles-build-tool-neither-waits-nor-registers/00-report.md) — the tool that starts a build and hears nothing; this gives it something to hear
@@ -0,0 +1,114 @@
---
topic: the mesh
status: accepted
date: 2026-10-01
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md
---
# 158. A provider with one credential shares it with every consumer, and the vault remakes it for all of them at once
## Context
[ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) gave every consumer of a
provision its own credential: the mesh mints one per pair, the provider's own code creates the
login, and rotating one consumer's touches nothing else. That is right for a database, a broker, an
object store — software that can hold many logins.
The media software on the home server cannot. A download client has one web password; an indexer
has one API key; each of the library managers has one key in its configuration; the media server
holds one token issued elsewhere. There is no login per consumer to create, so
[ADR 0113](0113-the-vault-makes-every-secret.md)'s only remaining form applied: the value is
*accepted*. On 2026-10-01 the home server held forty-seven accepted own secrets and twelve accepted
pair credentials, every one rotatable only by a person changing the software by hand and accepting
the new value, and one pair credential sat *made* and wrong because nobody could accept the real one.
The operator asked for every password in the vault and rotatable, and for a library manager's
definition to receive the download client's credential and address through provisioning like
anything else (filed as the forge's issue 243 on this repository).
The address half already works: the library manager requires the download client's API provision,
the provider serves scheme, port and user name, and the binding carries them. Only the credential
half had no form.
## Considered Options
1. **Keep accepting.** Honest about what the software can do and what the mesh cannot, and it is
the state the home server was in: nothing rotates, a consumer added later needs a person, and an
unknown predecessor password stays unknown for ever.
2. **Put a login per consumer in front of the software.** A proxy that holds the one credential and
issues many. A second service per provider, with its own credential to keep, to make the mesh's
model fit software that does not share it.
3. **Let the provider say its one credential is the credential.** An offer names which of the
provider's own secrets *is* what every consumer receives. The vault keeps one record, sealed to
the provider's machine, every current consumer's machine and the operator, and because it stores
no plaintext it cannot seal an existing value to a later consumer — so it **remakes the value
for all of them at once** whenever the set of consumers changes or a rotation is asked. The
provider takes it the way an own secret is taken ([ADR 0114](0114-a-shared-credential-rotates-over-two-credentials.md),
issue 180); consumers read it at start.
## Decision
**Option 3.** A provider whose software holds one credential shares that credential, and the mesh
owns its whole lifecycle.
- **The offer says so.** `{"name": "download-client-api", "credential": {"own": "password"}}` on a
provider's `provides` entry names one of its own secrets as the credential of that provision. The
named own secret must say how it is taken (`taken: at-start` or `taken: applied`); an offer
naming an undeclared or untaken secret is refused at parse.
- **One record, many seals.** The vault keeps one value per (provider assignment, provision). It is
sealed to the provider's machine, to each consumer's machine that currently binds the provision,
and to the operator. Every consumer's binding file carries the provider's one user name and the
secret file carries the shared value; the shape a consumer reads is the pair credential's, so a
consumer's definition does not know whether its credential is shared.
- **Remade for all, together.** When a consumer binds or unbinds, or `secret rotate` is asked on the
provider's own secret, the vault makes a new value and seals it to every current holder in one
act, and the mesh sends every holding machine. The provider restarts on the new value or applies
it at start; each consumer restarts on it. There is no window between two credentials, because
there is one credential; there is the restart, stated as the cost below.
- **An accepted shared value is sealed to everyone the moment it is accepted.** `secret accept` on
the provider's own secret is the one moment the mesh holds the plaintext, and it seals copies for
every current consumer then. It is not remade afterwards ([ADR 0113](0113-the-vault-makes-every-secret.md)):
a consumer that binds later is refused until the value is accepted again, in words that say so.
- **A value the software issues itself stays accepted.** A token the media server obtains from its
vendor cannot be set by the mesh; its provision keeps the accepted form until a module can deliver a
value it did not mint to the vault, which this record does not build.
- **Nothing changes for software that holds many logins.** ADR 0048's form stays the default; this
is the form for an offer that says it has one credential.
## Consequences
- The media stack's six providers stop needing a person per consumer. A library manager binding
the download client gets a working credential the mesh made, and an unknown predecessor password
is replaced by one the mesh knows, recoverable with the operator's key.
- **Adding or removing a consumer restarts every consumer of that provision and the provider.**
That is the price of one credential, and it is paid when a definition binds, not at an hour of
nobody's choosing. It is stated in the plan's words when it happens.
- Rotation of a shared credential is [ADR 0114](0114-a-shared-credential-rotates-over-two-credentials.md)'s
single-party form across several machines: in place, all holders sent together. The staged form
for a backend that takes its credential once is still not built, and a provider whose own secret
says `applied` refuses rotation by name until it is.
- The vault can name who holds a shared value — the copies are the record — so *who has this* stays
a query, as design 13 requires.
- The accepted count on the home server becomes a list that shrinks, provider by provider, as each
one's start applies the file.
## How this is checked
| Rule | Checked by |
|---|---|
| An offer may name one of its own secrets as its credential; an undeclared or untaken secret is refused at parse | manifest tests |
| A consumer of a shared provision receives the provider's value as its pair credential, under the provider's one user name | resolver and declaration tests |
| The record is sealed to the provider, every current consumer and the operator; a consumer binding or unbinding remakes it for all | inventory tests against a raised store |
| Rotating the provider's own secret remakes every holder's copy, and an accepted value is sealed to current consumers once and not remade | inventory tests |
| Live: a library manager on the home server binds the download client with a value the mesh made, the client takes it at start, and a rotation through the console reaches both | done by hand after the media catalogue's providers apply the file at start |
*2026-10-01:* the first four rows pass in mesh-controller PR 184 (`make check` green); the live row waits for the first provider definition to say `credential` and `taken`.
## References
- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) — extended: the per-consumer form stays the default; this is the form for one credential
- [ADR 0113](0113-the-vault-makes-every-secret.md), [ADR 0114](0114-a-shared-credential-rotates-over-two-credentials.md) — the accepted form and the single-party rotation this rests on
- [Issue 180](../04-ISSUES/180-a-modules-own-secret-cannot-be-rotated/00-report.md) — the `taken` word and the rotation this reuses
- [Design 24 — The secrets vault](../03-DESIGN/01-to-be/24-the-secrets-vault.md), [Design 13 — Credentials and their rotation](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md)
- The forge's issue 243 on this repository, where the operator's ask and the home server's count were recorded
@@ -0,0 +1,99 @@
---
topic: the mesh
status: accepted
date: 2026-10-01
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
---
# 159. A tool call names the machine it is for, every answer says which machine answered, and a holder's runtime serves its seat's verbs
## Context
[ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) made a module's tools subjects on
the bus and the console the place a person reaches them. [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)
made the mesh's own verbs the controller seat's tools, served by the controller. Design 33 said what
a seat's tools are, that holding a seat means serving them, and that a node-scoped seat's verb
carries the machine.
What was built stopped short in two places ([issue 182](../04-ISSUES/182-a-tool-call-reaches-whichever-instance-answers-first/00-report.md)).
A module's tools were one subject per module in one queue group, so with the database engine on two
machines a call reached whichever instance answered first, unnamed, and nobody could ask one machine's.
And no module served the verbs of a seat it held: the runtime did not know which seats its module
claimed, and no seat but the controller's declared verbs. The operator named it: a tool call must be
able to say *the store on the control node*, and the engine holding the store seat must serve the
store's tools as well as its own.
## Considered Options
1. **Leave the queue group and ask the controller which machine answered.** Nothing changes on the
bus; a caller cannot choose, only learn afterwards. Useless for the question that was asked.
2. **A subject per machine instead of one per module.** Every call names a machine; a stateless
module on three machines loses the one-of-them answer a queue group gives for free, and every
caller has to know where things run.
3. **Both subjects, and the machine in every answer.** An instance serves its module's subject in the
queue group as before, and the same subject with its machine as the last token. A caller that
names no machine gets one instance and is told which; a caller that names one gets that one. The
grant for a tool covers both. And a holder's runtime serves its seat's verbs by the same means,
from what the credential tells it.
## Decision
**Option 3.**
- **Two subjects per tool, one default.** `mesh.mod.<module>.tool.<tool>` in the queue group, and
`mesh.mod.<module>.tool.<tool>.<node>` served by the instance on that machine alone. In the
caller's words, `<module>.<tool>@<node>`. A runtime that does not know its machine serves only the
first, which is what it always did.
- **Every answer says which machine answered.** The reply carries the node; the console appends
*answered by <node>* as its own line after the module's unshaped answer, and `mesh call` prints it.
An answer from a module on several machines is never an answer from nowhere.
- **The console offers the machine on every module tool** as an optional `node` argument, lists it,
strips it into the subject and never passes it to the module. A seat's verb takes none: the seat's
scope decides where it is served.
- **The grant covers both subjects.** `invokes: [<module>.<tool>]` permits the plain subject and the
machine-addressed one; `*` already permitted everything beneath `tool`.
- **A holder's runtime serves its seat's verbs.** The broker credential the mesh writes names the
seats the module claims and, for each, its scope and the verbs the seat promises. The runtime
serves each verb with the module's tool of the same name on the seat's own subject — flat for a
mesh seat, with the machine for a node-scoped one — and the bus admits that subscription only
where the module holds the seat, because the holder's grant is composed from the holding. A
claimant that does not hold the seat here is refused the subscription and serves nothing. A
claimant missing a tool a seat promises is already refused at registration (design 33 §3).
- **The store seat's first verbs**, so the operator's question has an answer: `databases`, every
database the store holds with its owner and size, and `query`, one read-only statement against one
database. The database engine serves both as tools of those names and lists them in its definition.
Which verbs a seat serves is a decision per seat and binds every holder; these two are the smallest
set that makes the store askable.
## Consequences
- *List the databases of the store on the control node* is `mesh-store.databases` through the seat,
answered by its holder wherever it sits, or `postgres.databases@novox` through the module on one
named machine. Both say who answered.
- Every module's runtime serves one more subscription per tool and, for a claimant, one per promised
verb. No manifest changes for the per-machine half; the seat half needs each holder's definition to
list the seat's verbs among its tools, which registration already demands.
- The runtime change reaches a module when the module is rebuilt on the new runtime image; until
then that module answers only on its plain subject, and a call naming its machine is refused as
unserved, in words that say so.
- The credential gains `claims`; a module issued before this carries none and serves no seat verb
until it is issued again. `rollout mint` for the holders is the one-time cost.
## How this is checked
| Rule | Checked by |
|---|---|
| A call naming a machine reaches that machine's instance; an unnamed call reaches one and says which | mesh-tools, against a real bus: a module on two machines |
| A claimant serves a seat's verb on the seat's subject, and the answer names the machine | the same test |
| The console lists `node` on a module's tool and not on a seat's verb, and the answer carries *answered by* | mesh-tools, the MCP conformance test |
| The grant for a tool covers the plain and the machine-addressed subject | `TestInvokingAToolMayAddressTheMachineToo` (controller) |
| Live: the store's databases listed from the control node by name through the console, and through the store seat | done by hand after the roll-out and the catalogue's step |
## References
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — extended: the surface carries the machine
- [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md), [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) — the seat half, now for every holder
- [Design 33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §3, §4; [Design 34 — The console](../03-DESIGN/01-to-be/34-the-console.md) §3
- [Issue 182](../04-ISSUES/182-a-tool-call-reaches-whichever-instance-answers-first/00-report.md)
@@ -0,0 +1,139 @@
---
topic: the mesh
status: accepted
date: 2026-10-01
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
---
# 160. The mesh issues an assignment's subjects, and a runtime serves what it is issued
## Context
A module's code names no subject. It registers tools by name and emits events by name, and design 29
§1 says the rest: *the module names its event and the mesh decides where it lands*. What was built
decided it twice. The runtime derives `mesh.mod.<module>.tool.<name>` from the module's name by a rule
compiled into it; the controller derives the same subject by the same rule compiled into it, and grants
it. They agree because two binaries carry one convention, which is the failure design 33 §2 names for
seat protocols: *discovery that reads a binary disagrees with the mesh the moment the two are on
different versions*. [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)
extended the convention this morning — a second subject per tool with the machine as its last token, a
seat's verbs served from the credential's claims — and extending it made the shape plain: every such
change is written in the runtime and in the controller, and a module whose instances must not be
confused is told apart by a rule in a binary rather than by the mesh that assigned it.
The operator put it in one sentence: the mesh knows the subjects, the modules do not; a module should
ask what to listen on. This record decides exactly that.
## Considered Options
1. **Keep the convention, keep it in two places.** Cheap until the next change; every change is two
changes, and the mesh cannot vary a subject for one assignment without a rule for all.
2. **Keep the convention in one place by putting it in the SDK alone**, and have the controller call
the SDK's rule. The controller is Go and the SDK is TypeScript; one of them would still carry a copy.
3. **The mesh issues the subjects.** For every assignment the controller composes a membership: what
this instance serves, where, in which queue if any; the seat verbs it holds; where its events land;
what it may reach and at which subjects. It publishes it to a subject only that assignment may read,
kept last-per-subject so a runtime that connects late reads the current one. The runtime serves
exactly the list and nothing it did not receive. The grant is composed from the same membership, in
the same act, so the two cannot drift.
## Decision
**Option 3.**
- **A membership per assignment.** The controller composes, for a module on a machine, one document:
the tools the module serves with the subject each is served on and the queue group if any; the seat
verbs this instance serves and their subjects; the subject each of its events lands on; what it may
reach — the tools it invokes, resolved to the subjects the mesh issued to those modules' instances —
and what it consumes. The runtime registers tools and events by name; the membership says where.
- **Published, not written into the definition.** The membership is a message on
`mesh.assignment.<node>.<module>` in a stream that keeps the last per subject, like a node's
declaration. The controller publishes it whenever the assignment's facts change: a push, a seat
handover, an instance added elsewhere, an upgrade. A runtime reads the current one when it connects,
serves it, and keeps reading, so a change reaches a running instance as a re-subscription rather
than a restart.
- **One bootstrap rule, and only one.** The credential names the node and the module; the membership's
subject follows from those two names and nothing else, and the account may subscribe it. Every
other subject is data in the membership. This is the one convention the runtime keeps, the way a
resolver keeps the address of a root.
- **The grant is the membership, read the other way.** What an account may subscribe is what its
membership says it serves plus its own membership's subject; what it may publish is what its
membership says it emits and reaches. One composition yields both, so a subject the runtime serves
without a grant, or a grant for a subject nothing serves, cannot be written.
- **Whether an instance answers for the module, or only for its machine, is the mesh's to decide.**
A module on one machine is issued the module's plain subject and its machine's. A module on several
is issued only its machine's unless its definition says its instances are interchangeable, a fact
about the software and not about the bus; then every instance is issued the plain subject in one
queue group as well. The console lists what the memberships say: a stateful module on two machines
appears once per machine; a stateless one appears once.
- **A caller composes nothing.** The console's listing carries each tool's subject; the SDK's call by
name reads the subject from the caller's own membership, where the mesh wrote what it may reach. The
shape of a subject is the controller's business and may change without any module or runtime
changing.
- **Today's shape is the shape issued first.** `mesh.mod.<module>.tool.<name>`, with the machine as the
last token for an instance, and `mesh.seat.<seat>.tool.<verb>` with the machine for a node-scoped
seat, are what the controller composes on day one, so nothing on the mesh moves when the
membership arrives; only who decides it moves. [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)
stands for what it decided — a call names the machine, every answer names it, a holder serves its
seat — and is extended in how: those facts are now issued, not derived.
## Consequences
- The runtime loses its subject rule and its claims rule; it serves a list. The controller gains one
composition and one stream; design 25 §2 and §3 gain a line each. The console loses `toolSubject`
and reads subjects from the listing. The SDK's `invokeTool` reads the caller's membership.
- A subject scheme change is a controller release and a republish of every membership, with no module
rebuilt — the opposite of this morning's forty-three builds.
- A membership can differ per assignment on purpose: an instance that holds a seat serves more; an
instance the mesh wants quiet serves less; a module the mesh is retiring can be issued nothing and
told so.
- During the move, a runtime that finds no membership for its assignment falls back to the derived
shape and says so in its log, so the wave of this change is a controller release followed by one
push, and a runtime older than the change keeps working on the convention it carries.
## How this is checked
| Rule | Checked by |
|---|---|
| A membership composed for an assignment and the grant composed for its account name the same subjects, both ways | a controller test over a module on one machine, on two, holding a seat, and declared interchangeable |
| A runtime serves exactly the subjects its membership lists, and re-subscribes when the membership changes | a runtime test against a real bus: a membership published, served; republished with a subject removed and one added, followed |
| A runtime with no membership says so and serves the derived shape | the same test, before any membership is published |
| The console lists a stateful module on two machines once per machine, and composes no subject | the MCP conformance test |
| Live: the store's databases asked of one named machine and through the seat, after a controller release and one push, with no module rebuilt | by hand |
## Built, 2026-10-01
> **Progressive insight — 2026-10-01.** The decision stands; these are the facts of its building.
- The controller's half: mesh-controller 188 — the membership, its subject, the assignments stream
read directly, a module's account granted its own membership and nothing else of the stream, a
membership published after each push.
- The runtime's half: mesh-tools 23 — the one address derived, the membership read and followed,
exactly the issued subjects served and re-served, the derived shape with a log line until one is
issued, a seat's verbs implemented under the seat's name and never listed as the module's, the
listing carrying subjects and the console composing none. A claim may now name the verbs it
serves for its seat (mesh-controller 186), so a holder's own tools need not be the seat's.
- What the first roll-out taught: the controller's own grant did not name the assignments it issues,
so the first memberships were refused by the server and every runtime kept the derived shape —
which is exactly the fallback this record asked for, and exactly why nobody noticed
([issue 183](../04-ISSUES/183-the-controller-could-not-publish-the-memberships-it-issued/00-report.md)).
The SDK's `invokeTool` still composes a subject; it reaches a membership through the runtime's
broker, which does, so the caller-side rule is met there and not yet in the SDK's own words.
- Live, 14:55Z the same day, through the console: the console's runtime logged *was issued a new
membership; re-serving on it*; `mesh-store.databases` answered by the control node, the seat's
holder; `postgres.postgres_list_databases` with the machine named answered by that machine, on
both machines that run it; `mesh-controller.push {node}` reached the seat's verb with its own
argument intact. Three facts the proof taught: a runtime's first read of the stream must use the
subject-addressed direct get, the only form its account is granted (mesh-tools 25); a module's
bus credential is a minted secret written once, so a claim added to a definition reaches a running
module only after `module issue <module> --node <machine>` and a push (postgres, both machines);
and a registration under a seat the credential does not yet claim must be said and skipped, not
fatal (mesh-tools 26).
## References
- [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md) — extended: the same facts, issued rather than derived
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md), [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) — the surface and the seat's tools this applies to
- [Design 25 — The bus on NATS](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §2, §3; [Design 32 — What a module declares](../03-DESIGN/01-to-be/32-what-a-module-declares.md) §1; [Design 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md); [Design 34](../03-DESIGN/01-to-be/34-the-console.md)
+102
View File
@@ -0,0 +1,102 @@
---
topic: the mesh
status: accepted
date: 2026-10-01
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0126-a-module-declares-its-own-seats.md
---
# 161. What deserves a seat: a role of a module is a seat, a singular fact about machines is a placement with a capacity of one, and a holder's software is the machine's
## Context
Three issues asked the same question from three sides. The vault provides `secret` to the whole
mesh and claims no seat, so nothing refuses a second vault by name
([issue 106](../04-ISSUES/106-the-vault-claims-no-seat/00-report.md)). The hub of the private
network is a placement, `overlay place <node> --hub`, and the issue asked whether "there is exactly
one hub" is a seat's shape ([issue 105](../04-ISSUES/105-the-hub-of-the-private-network-is-not-a-seat/00-report.md)).
Three modules claim the one uplink seat, one per network manager a machine might run, and nothing
checks that the holder names the manager the machine actually runs
([issue 138](../04-ISSUES/138-two-modules-claim-one-seat-and-are-not-interchangeable/00-report.md)).
Read against the code on the day of deciding:
- The mesh's own seats are five by [design 26](../03-DESIGN/01-to-be/26-the-seats.md)'s table and
four in the controller's seed: `mesh-vault` is in the table and not in the seed, and the vault's
definition claims nothing. The design also says `secret` is reserved; no parser or resolution rule
reserves it. A second provider of `secret` would be a second candidate, settled by a pin.
- The store already keeps one hub: a unique index since the overlay's first migration, and the
placing command refuses a second hub naming the first. What 105 observed as silent is not.
[ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) decided that
the private network becomes a mesh-scoped seat held by a server module, with client modules —
the overlay is the host's own today, so that seat has nothing to be held by yet.
- A machine's capabilities are its profile, detected by the host at enrolment and never since, and
resolution refuses a module on a machine lacking one it declares, naming the capability. The uplink
holders declare `package-manager` and `service-manager`, which every machine has.
[ADR 0126](0126-a-module-declares-its-own-seats.md) gave the reason the mesh's own seats exist:
**the mesh's own code looks them up by name.** `mesh-store` is an identifier the controller
dereferences, not a convention. That reason decides the first question; the other two are decided
by what a seat is — a role held by a module assignment — and by what the mesh can check.
## Decision
**1. A provision the mesh itself dereferences is delivered by a mesh seat its provider claims.**
The vault's `secret` is one: the controller seals every minted credential with it. `mesh-vault` is
the fifth seat of the mesh's own, mesh-scoped, delivering `secret`, under
[ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md)'s convention; the vault's
definition claims it; a second provider of `secret` is a second claimant and refused by name. The
word *reserved* leaves design 26: the effect it described is the seat's. Every other mesh-scoped
provision — `smtp`, `oidc-client`, `s3-bucket`, `route`, `acme-ca` and the rest — may have several
providers, and a consumer with several and none local is a person's choice, as the glossary says.
The test for "deserves a seat" is the question 0126 asked: does the mesh's own code find it by name?
**2. A singular fact about machines is a placement with a capacity of one; a singular role of a
module is a seat.** A seat is held by a module assignment and points at it; the hub is a machine,
and the private network is the host's own until 0121's server and client modules exist. So the hub
stays a placement, and what a seat would have given — refusal of a second by name, and the one
named when asked — a placement of capacity one gives: the store keeps one (the unique index), the
placing command refuses a second naming the one that stands, and the overlay listing names it.
0121's seat for the private network stands, deferred with the split it needs. The rule generalises:
a fact of the shape *exactly one machine is X* is a placement checked by the store and said by name,
never a seat with no module to hold it.
**3. A holder of a seat whose role is "speak to what this machine runs" must be the dialect the
machine runs, and the machine says which.** The host's profile gains one capability per network
manager found active — `uplink-networkmanager`, `uplink-systemd-networkd`, `uplink-dhcpcd`, each
`systemctl is-active` of the manager's unit — and each uplink holder declares its own. Assignment
then refuses the wrong holder with the refusal that already exists, naming the capability; nothing
new is judged. The profile is detected again by every apply and travels in the report, and the
controller keeps the latest, so a machine that switches managers is, at its next push, a machine
whose holder lacks a capability: the plan refuses and names it, which is the one thing the machine
is the only one to know. `node-uplink` stays one seat: its three holders are three dialects of one
role, and the capability picks the dialect. One module speaking all three is allowed by this and
built by nobody.
## Consequences
- The controller's seed gains `mesh-vault`; the seat table takes it additively at the next start,
as every seed row does. The vault's definition claims it, one release after the controller.
- The uplink definitions declare their capability one release after the host reports it, or they
are refused on every machine in between; the order is controller (the report carries a profile),
host, then catalogue.
- Design 26 loses the word *reserved* for `secret` and states rules 2 and 3; the uplink row of the
seat table names the capability its holders declare.
- Issue 106 is resolved by rule 1, 105 by rule 2 with nothing to build, 138 by rule 3.
## How this is checked
| Rule | Checked by |
|---|---|
| `mesh-vault` is in the mesh's own set, mesh-scoped, delivering `secret`, and the vault claims it | a catalogue test on the default seats; registration refuses a second claimant by name (`CanHold`'s existing test, with the vault's seat) |
| A second hub is refused naming the first, and the listing names the hub | the overlay command's test; the store's unique index |
| A machine's profile names the network manager it runs, and is renewed by every report | a host detector test per manager; a controller test that a report carrying a profile replaces the stored one |
| An uplink holder on a machine running another manager is refused, naming the capability | the existing capability refusal, exercised by a resolution test with a networkmanager machine and the systemd-networkd holder |
| Live | `mesh-controller.seats` lists `mesh-vault` held by the vault on the control node; `plan` of a machine refuses the wrong uplink holder naming `uplink-<manager>` |
## References
- [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md), [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md), [ADR 0117](0117-a-machines-uplink-is-a-seat.md), [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0126](0126-a-module-declares-its-own-seats.md)
- [Design 26 — The seats](../03-DESIGN/01-to-be/26-the-seats.md)
- Issues [105](../04-ISSUES/105-the-hub-of-the-private-network-is-not-a-seat/00-report.md), [106](../04-ISSUES/106-the-vault-claims-no-seat/00-report.md), [138](../04-ISSUES/138-two-modules-claim-one-seat-and-are-not-interchangeable/00-report.md)
@@ -0,0 +1,128 @@
---
topic: the mesh
status: accepted
date: 2026-10-01
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
---
# 162. A merge produces a tiered plan the mesh keeps, and a module's dependencies are one relation in the catalogue
## Context
A merge on the forge reaches the controller as an event, and the controller asks the build
machine for what that merge changed. Until today that meant the modules whose recorded source is
that repository; since this afternoon it also means everything standing on what moved
([issue 186](../04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md)).
Both are done inside the handler that received the event: it asks one build, waits for it, asks the
next, and returns when the last is done. Three things followed from that shape on 2026-10-01:
- The controller hears nothing else for the length of the work — twenty-five minutes for the runtime
image and its forty-three dependents ([issue 184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md)).
- A controller replaced mid-merge loses the rest of the merge: the redelivered announcement reads as
history, and the dependents are asked by hand.
- Nothing is deployed between builds. A merge that changes the build machine and something the build
machine builds asks for both in order, but the second is built by whichever build machine is running
— the old one, unless somebody pushed in between. The order the dependents are sorted in exists for
the artifacts; it says nothing about what must be *running*.
And the knowledge the order is computed from is scattered: a manifest's `build.on`, the artifacts a
build was made against, the repositories a build read, and the fact that every source-built module is
built by the build machine, each read by a different function in the merge handler.
## Decision
**1. A module's dependencies are one relation in the catalogue.** `depends-on` edges, each with the
kind of dependency on it: `stands-on` (the module's artifact is built on the other's), `packages` (the
module's build reads the other's repository), `built-by` (the module is built by the holder of the
build-machine seat), and `declared` (a manifest's `build.on`). The relation is answered by one query
of the catalogue — the controller's inventory today, the catalogue seat's tool when something outside
the controller needs it — and nothing else computes an edge. The edges are derived from facts
recorded at two moments and written by nobody: registration records the manifest (`declared`, and
`built-by` for anything with a source), a build's take-in records what the image was built on and
which repositories it read (`stands-on`, `packages`). A module's first build places it by its
declared edges alone; from its second it is placed by what was true.
**The kinds are three dependencies, not one.** A *code* dependency — B packages A's source — means
B is rebuilt whenever A changes, in the same tier: B's build needs nothing of A's first. A *build*
dependency — B stands on A's artifact, or declares it — means B is rebuilt after A is *built*, the
next tier, and nothing need be deployed in between. A *runtime* dependency — B is built by A — means
B is rebuilt only after A is built *and running*, the next tier with a gate on the machines' reports.
One cycle is real and resolved by the kinds themselves: the runtime image is built by the build
machine, and the build machine stands on the runtime image; the image comes first, built by the
build machine that is running, which is the only one there could be — a `built-by` edge never orders
a module after a build machine that stands on it. A provision is not a dependency of this relation:
a consumer binds to its provider through what the push renders, and a change to the provider's image
changes nothing in the consumer's; a consumer whose build does read a provider's source declares it.
"A was deployed, so restart B" is the push's domain — B is replaced when what it reads changed — and
not the plan's.
**2. A merge produces a plan, and the plan is a record.** The controller takes the modules the merge
changed and everything reachable from them along `depends-on` edges, and sorts that set into tiers:
tier 0 depends on nothing else in the set, tier 1 only on tier 0, and so on. The plan — the merge it
answers, the tiers, and each module's state — is written to the store before any build is asked. The
handler asks tier 0 and returns. Every build's outcome, taken in by the same handler that takes every
outcome in, advances the plan it belongs to; a controller replaced mid-plan resumes it from the store.
**3. A tier is done when it is built, and when what the next tier needs from it is running.** A
module whose roll-out policy says *roll out* is sent to its machines when it moves, as today. The next
tier is asked only once every module in this tier is built and every rolled-out module of this tier
that a later tier is `built-by` has been applied by the machines running it — the machines' reports
say so. A module whose policy says *record* is built and not waited for. So a merge
touching the build machine and the controller builds the build machine, waits until it is the build
machine that is running, and only then asks for the controller's build.
**4. A plan is read where the mesh is read.** `status` lists every open plan: the merge, the tier it
is at of how many, what it is waiting for and since when; `builds` lists the asked beside the built.
A plan that has waited past a bound is named red there, which is the first fact of
[issue 187](../04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md)'s list.
## Consequences
- The merge handler returns in milliseconds; the receive loop is never held by a build again. Issue
184's remaining cause — a handler that waits for its own work — is removed rather than worked
around; the bus's heartbeats stop being dropped under a merge.
- A controller roll in the middle of a plan costs nothing: the plan is in the store and the asks are
in the queue (mesh-controller 194).
- A release across repositories is a plan whose edges cross repositories; the order a person kept in
a work-order file is the order the tiers give. Issue 186's third fault is answered by the plan,
not by a separate release record.
- The explicit `build --on <base>` stays as the way to ask for the same plan by hand.
- A module's `build.on` remains the one place a manifest states a dependency the store cannot see.
## How this is checked
| Rule | Checked by |
|---|---|
| Dependencies are one relation, each edge with its kind | an inventory test over a fixture catalogue: a runtime image, a module on it, a module packaging the controller's source, and the build machine; the four kinds come back from one call |
| A merge's set is sorted into tiers along the three kinds: a code dependency in the same tier, a build dependency after its base is built, a runtime dependency after the build machine; the build machine's own base first | a unit test on the tiering over the mesh's real shape: the runtime image, the build machine on it, modules built by it, a plugin declared on one, the proxy packaging the controller, an unrelated module left out; a cycle is one last tier and said |
| Only a runtime dependency gates on deployment, and only for a module that rolls out | the same test's gate cases |
| The plan is written before any build is asked, and the handler returns | a controller test: a merge announcement produces a plan row with its tiers and one asked build per tier-0 module, and the handler is back before any outcome |
| An outcome advances its plan; a complete tier asks the next; a tier with a rolled-out base waits for the machines' reports | store-backed tests over a two-tier plan: the first outcome marks built; the tier's roll-out gate holds until the report; the next tier is asked after |
| A controller restarted mid-plan resumes it | a test that opens a plan, drops the handler, and advances from the store alone |
| `status` lists open plans and names one that waits past the bound | the status JSON test with a fixture plan |
| Live | a catalogue merge touching a base and a dependent: the plan's tiers in `status`, the base rolled before the dependent is asked |
## Built and proven live, 2026-10-01
> **Progressive insight — 2026-10-01.** The decision stands; these are the facts of its building.
Built in mesh-controller 197 (the relation, the plan record, the driver, `status`), 198 (`plans`),
199 (a `built-by` edge orders and gates but never widens — the first live plan had taken the whole
catalogue along for a controller change; `plans stop`), 200. The first merge handled by the finished
machinery, at 18:56Z, was a controller change and produced the plan this record describes: tier 0
the build machine; tier 1 the controller and the proxy that packages its source. The handler
returned at once; the build machine was built, rolled, and the plan read *tier 0 built; waiting for
builder on novox to be applied* until the machine reported; then tier 1 was asked, both built, and
the plan read done — three minutes, read through the console with `plans`, the receive loop taking
reports throughout. What the day between decision and proof taught is in issues
[184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md),
[186](../04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md) and
[188](../04-ISSUES/188-a-refusal-inside-on-the-network-drops-a-machine-silently/00-report.md).
## References
- [ADR 0157](0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md), [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)
- [Design 30 — The mesh updates itself on a push](../03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md)
- Issues [184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md), [186](../04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md), [187](../04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md)
@@ -0,0 +1,164 @@
---
topic: the mesh
status: accepted
date: 2026-10-01
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
---
# 163. Taking a module over is a comparison: what it compares, what it refuses, and what it carries
## Context
On an adopted machine the mesh holds what it finds until the module is taken, and taking is the
cutover ([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)). The whole-node flip
is previewed and confirmed by digest; the per-module cutover, the step that actually replaces a
running service, previews nothing. `take` names the held things the next push will replace and
where each original is kept. It does not say how the module's version of each differs from what
runs. Ten issues from the first migrations are the same omission seen from ten sides:
- a port narrowed from everywhere to the private network, unannounced ([086](../04-ISSUES/086-taking-a-module-narrows-a-port-without-saying-so/00-report.md));
- a configuration file replaced whole, dropping the one line that was the installation's own ([098](../04-ISSUES/098-taking-a-module-replaces-a-configuration-nobody-compared/00-report.md));
- an image pin that had aged into a downgrade, discovered by three minutes of outage ([099](../04-ISSUES/099-a-modules-image-pin-ages-into-a-downgrade/00-report.md));
- a secret minted for a service that already had one, with no way to carry the existing value in because it was a required secret and not the module's own ([100](../04-ISSUES/100-a-minted-secret-cannot-be-the-one-the-service-already-uses/00-report.md));
- a container moved onto the module's own network, out of reach of the neighbour that called it by name ([101](../04-ISSUES/101-taking-a-service-reached-by-container-name-cuts-its-neighbours-off/00-report.md));
- a resource whose target changed, leaving the old container running with no record naming it ([097](../04-ISSUES/097-a-resource-that-changes-target-leaves-the-old-one-behind/00-report.md));
- a volume path that changed without the running container noticing, because the host does not compare that field ([126](../04-ISSUES/126-a-volume-path-is-not-in-the-spec-comparison/00-report.md));
- a build that deployed at once because the module's policy said so, racing a data move ([126](../04-ISSUES/126-a-volume-path-is-not-in-the-spec-comparison/00-report.md));
- a setting accepted where it was set and refusing the whole machine where it was read ([096](../04-ISSUES/096-a-setting-that-cannot-work-is-stored-and-stops-the-node/00-report.md));
- a module that could not take over what genesis raised, because the two differed in name, network, data and image ([090](../04-ISSUES/090-the-forge-module-does-not-take-over-the-forge-genesis-raised/00-report.md));
- a successor that could not stand beside its predecessor at all, answered by [ADR 0104](0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md)'s adapter ([093](../04-ISSUES/093-the-successor-proxy-cannot-serve-what-the-predecessor-still-serves/00-report.md)).
What the host records of a found thing is enough to compare from: a file's original, kept, with
its digest, mode and owner; a container's id and whether it ran; whether anything changed it
since. What it does not yet record is what a comparison needs most: the found container's image
and when that image was made, the networks it is on and who else is on them, what it mounts, what
it publishes. And the controller's rule that a machine is told everything or nothing turns one
impossible statement into a machine nobody can talk to.
## Decision
**1. A take is previewed, and the preview is a comparison.** For every held thing the module would
replace, `take` puts what runs beside what the module declares and says the difference:
- a **container**: its image against the module's, with each image's creation date so older and
newer have a meaning; its name; its networks, and the other containers on each found network
that is not the module's; its published ports and the reach of each, found firewall and guard
included; its mounts against the module's volumes and paths;
- a **file**: the kept original against the declared content, as a difference, not two digests;
- a **secret** the module takes that the mesh minted and nobody accepted, when the service's data
was found — a service that already runs already has a value;
- the module's **settings** on that machine, composed against its definition.
`take` without `--yes` prints the comparison and stops; `take --yes <digest>` cuts over exactly
what was previewed, the way the flip is confirmed, and a preview whose account of the machine is
older than the flip allows is refused the same way. The host supplies the facts in its report of
what it holds: the found container's image and its creation date, its networks and their members,
its mounts and published ports.
**2. Three differences refuse by default, each overridden by naming it.** An image **older** than
the one running, by creation date — `--downgrade`, said once and recorded. A declared file that
**differs** from the kept original — `--replace <path>`, or the module declares the file partially
and writes into it ([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md)), which is
the right answer wherever the file is the service's own and the format allows it. A **minted,
unaccepted secret** for a service whose data was found — accept the value first, or `--mint
<name>` to say the service shall take a new one. Two differences are said and not refused: a port
whose reach **narrows**, and a found network whose other members may reach the container **by
name**, each member named; both are the operator's to weigh, and the words are there to weigh them.
**3. A secret the mesh would mint may be accepted instead, own or required.** `secret accept`
reaches a module's required secrets, not only its own: the value is a fact about the machine, and
the mesh's job at a take is to learn it. The accepted value is sealed to the module as a minted one
would be, and the provider that would have minted it is told it has one. Whether one accepted
value should reach every consumer of a provider at once is [issue 165](../04-ISSUES/165-one-accepted-value-must-be-accepted-once-per-consumer/00-report.md)'s
question and the next group's.
**4. A taken container may keep a found network, for a while, by a setting.** A per-machine
setting names a found network the module's container also joins, so a neighbour that resolves it
by name keeps resolving it. It is migration scaffolding in the sense of
[ADR 0104](0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md): assigned only on an
adopted machine, reported while it stands, removed when the neighbours are taken, and the preview
names it. Taking a group of modules at once is not decided here; the setting makes the order free.
**5. The host compares every field it writes, and removes what it can no longer name.** A
container is current when every field the host would write agrees with the one running — volumes
and paths included; a field the host cannot compare recreates rather than passes. The host's
record keeps a resource's former targets: a container or file the host **wrote** under a name or
path the declaration no longer names is removed on the next apply and said; what was **found** is
never removed, as ADR 0100 says. And the host answers the question nothing answered on
2026-09-23: its report lists what runs on the machine that the mesh neither wrote nor holds —
containers and listeners — as *strays*, so a thing left behind is seen the day it is left.
**6. A setting is judged where it is stored, and an impossible one costs a module, not a machine.**
Storing a setting composes it against the module's current definition and refuses with the node,
module, layer and key when it cannot work. A definition that later moves under a stored setting
makes composition leave *that module* out of the machine's declaration — its held things kept, its
containers untouched — and say the statement by name; the machine is still told everything else.
A machine is told everything or nothing about what it *is* told; what it is not told is said.
**7. What genesis raises, it raises as the module that succeeds it declares** — name, network,
data directory and image — so the module adopts it by the found rule that already exists, and a
module meant to succeed a bootstrap service that it cannot adopt is a fault of genesis, found by a
test that raises and then assigns. **`build` says when a policy will act on its result**, so a
person choreographing a data move knows which module will not wait; under
[ADR 0162](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md) the roll-out is the plan's, and
the plan says it too.
## Consequences
- `take` becomes the per-module twin of the flip: preview, digest, confirm. The flip's own preview
gains the same comparisons for every module it takes.
- The host's report of what it holds grows by the found container's image and creation date,
networks and members, mounts and published ports; its store keeps former targets and strays.
- Issues 086, 098, 099, 100, 101 close on rule 1 and 2; 097 and 126 on rule 5; 096 on rule 6;
090 on rule 7; 093 is closed by ADR 0104's adapter, which runs.
- Nothing here changes what an adopted machine keeps or when: found stays held, held is never
removed, the original is kept before anything is written.
## How this is checked
| Rule | Checked by |
|---|---|
| The host reports a found container's image and creation date, networks and their members, mounts and published ports | host unit tests over a fake runtime; the adoption bed's report |
| `take` without `--yes` previews every held thing's difference and changes nothing; `--yes` with the digest cuts over; a stale account is refused | controller tests over a fixture report: a differing file, an older image, a narrowed port, a shared network, a minted secret |
| An older image, a differing file and a minted secret for found data refuse without their override | the same tests |
| A found network kept by a setting is joined, reported and named in the preview | a host test and a controller resolution test |
| `secret accept` takes a required secret | an inventory test; the provider is told |
| Every container field is compared; a former target the host wrote is removed and said; what was found is not | host tests: a volume path change recreates; a renamed container's predecessor is removed; a found one under the old name is kept |
| Strays are reported | a host test over a fake runtime with a container nobody declared |
| A setting that cannot compose is refused where stored, naming node, module, layer, key; a definition moving under one leaves that module out and says so | controller tests |
| Genesis raises the forge as its module declares it | a genesis test that raises, assigns, and finds the module holding rather than raising a second |
| Live | the next cutover on an adopted machine: `take` shows the comparison, refuses the downgrade if there is one, and the service keeps its configuration and its secret |
## Built, 2026-10-02
> **Progressive insight — 2026-10-02.** The decision stands; these are the facts of its building.
Built across mesh-host 63 and 64 and mesh-controller 201, 202 and the pull request that followed
them. Rule 1: `take` previews every held thing's comparison and ends with a digest; `take --yes
<digest>` acts on exactly that preview, and a changed preview or an account older than the flip
allows is refused, as the flip's are. A published port's reach is said as the machine reported it,
behind the found firewall whose rules are not read. Rule 2: an older image, a differing file and a
minted, unaccepted secret for found data refuse, overridden by `--downgrade`, `--replace <path>` and
`--mint <name>`; the secrets a module holds on a machine are read with where each came from. Rule 3:
`secret accept --provider` reaches a required secret. Rule 4: the per-machine setting is `networks`,
a container id to the found networks it keeps; judged for an adopted machine only, joined by the host
after the container runs, part of the container's spec, named in the preview. Rule 5: the host's
facts, former targets and strays. Rule 6: one judgement, run where a setting is stored and where a
machine is composed; a module whose stored setting its definition can no longer compose is left out
of the declaration, the envelope says so, the host keeps that module's things, and `plan` and `push`
say it by name. A key that reaches nothing is refused where stored and said by `plan`, and never
costs a module. Rule 7: genesis raises the forge under the module's container name, with its image
digest and its data directory; the network is the one difference left, said by the take, because the
bootstrap forge reaches the store on the machine's loopback.
**Not yet proven live.** Every machine of this mesh is converged, so the table's last row — a take
on an adopted machine — waits for the next adoption. What is live is what the rows above it check.
Issues 086, 098, 099, 100 and 101 stay located until that row is read.
## References
- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md), [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md), [ADR 0103](0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md), [ADR 0104](0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md), [ADR 0162](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md)
- [Design 05 — The node host](../03-DESIGN/01-to-be/05-the-node-host.md), [Design 09 — The node lifecycle](../03-DESIGN/01-to-be/09-the-node-lifecycle.md)
- Issues 086, 090, 093, 096, 097, 098, 099, 100, 101, 126
@@ -0,0 +1,99 @@
---
topic: the mesh
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
---
# 167. A membership carries what its module receives, and who the mesh is
## Context
A provider learns what it is given from a file. The controller composes every consumer's contribution
to a requirement, and the node's declaration writes them into the provider's received file. The route
proxy reads its routes that way: one JSON file, re-read every two seconds.
[Issue 191](../04-ISSUES/191-a-route-with-only-an-internal-name-is-dropped/00-report.md) showed what
that file leaves out. Since [ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md),
a route whose endpoint reaches only the private network carries an internal name and no public one.
The proxy dropped it. Serving it was not enough either: the proxy answers public and internal names on
the same listeners, so an internal name served to every request is public under a guessable name. To
serve it correctly the proxy needs a second fact, **who the mesh is**, and nothing gave it one.
The first attempt had the proxy work it out: the mesh's range from an environment variable written by
the catalogue, and the machine's container bridges read from its own interfaces. That is a second
definition of "the mesh", kept by one module, beside the one the packet filter already uses. The
controller resolves "from the mesh" to every machine's address on the private network, and the filter
is rendered from that list. Two definitions agree until one changes.
[ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
already gives every assignment one document on the bus, its membership, read once at connect and
followed. It says what the assignment serves and reaches. It does not yet say what it is given.
## Considered Options
1. **Keep the file, add the mesh to it.** The proxy keeps polling a file, and the controller writes the
mesh's addresses beside the routes. It fixes the definition, but delivery stays a file re-read on a
timer, written by a separate path from the one every other fact a module is told now takes.
2. **Have the proxy work it out** from a range in its environment and the machine's interfaces. Rejected:
it is the second definition this record exists to remove.
3. **The membership carries it.** What each module receives, from the same composition its received
file is written from, and the mesh's addresses, from the same list the filter is rendered from. The
proxy follows its membership and serves exactly that.
## Decision
**Option 3.**
- **A membership carries what its module receives**, by requirement: the contributions every consumer
made, exactly as composed for its received file. A requirement nobody contributed to is an empty
list, never absent, for the reason the file is written empty: "nothing asked" and "never told" want
different responses.
- **A membership carries who the mesh is**: every machine's address on the private network, the list
a rule saying "from the mesh" resolves to. One list, two readers: the filter and any module that
must tell the mesh from the world.
- **The route proxy reads its routes and the mesh from its membership**, with the bus account every
module that speaks on the bus is given. It serves an internal name only to the machines the mesh
names and to the machine itself, and answers anyone else as it answers a name it never routed: in
the request, in the handshake, and in the list of names it serves.
- **The file stays until the bus has spoken.** While a proxy has read no membership that carries routes,
it serves the file, and an internal name only to its own machine: refused, never opened. A
membership from a controller that issues no routes changes nothing.
## Consequences
- Every membership grows two fields. A machine joining or leaving republishes every membership, which
a push already does.
- A provider that receives something is told it twice for now, in its file and on the bus. The file
goes when every provider reads its membership; that is its own change.
- The route proxy needs a bus account. It is issued like any module's, so a machine running the proxy
cannot be composed between the catalogue declaring the account and the operator issuing it. The
machine keeps what it runs meanwhile.
- The internal name of a route that also has a public one is now served to the mesh only. Outsiders
have the public name.
- A container on the same machine that calls that machine's own internal name arrives from its
container network, not from a mesh address, and is refused. Calls between machines are unaffected:
they leave by the machine's mesh address. Whether the mesh should also issue each machine's container
networks is left open, because the mesh does not record them today.
## How this is checked
| Rule | Checked by |
|---|---|
| What a provider receives on the bus is what its received file says, same-node port fix included | a controller test composing a provider and a consumer on one machine and comparing the two |
| An internal name is served to the machines the membership names and to loopback, and to nobody else | the proxy's tests: served from a named address and from loopback; refused, unlisted and uncertified from any other |
| A membership that carries no routes, or a mesh that cannot be read, changes nothing | the proxy's tests |
| Until the mesh is issued, an internal name is served to the machine alone | the proxy's tests |
| Live: an internal-only route answers over the mesh and is refused from outside | by hand, after the release |
## References
- [ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md) —
the membership this extends
- [ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md) — reach, and the
insight of 2026-10-02 that the proxy is where internal reach is kept
- [ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md) — the machine itself is always inside
- [Issue 191](../04-ISSUES/191-a-route-with-only-an-internal-name-is-dropped/00-report.md) — what
found it
@@ -0,0 +1,136 @@
---
topic: the mesh
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
---
# 168. A converged machine is filtered by the mesh alone, and the host says what else refuses
## Context
[ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md) says what converging does to
the firewall a machine was found with: the mesh's derived filter is loaded in place of the
refusal-only guard, and the found firewall is retired — disabled, never flushed. Four issues from
the first two convergences are four ways that sentence was not the machine:
- the flip reported the found firewall retired and it was active two minutes later; fifty minutes
on, a reconcile found it disabled by hand and recorded that the mesh had done it
([143](../04-ISSUES/143-converging-does-not-retire-the-firewall-it-found/00-report.md));
- "the firewall found" named one front end, and what filtered the forwarded path on that machine
was a chain a predecessor had installed in the container runtime's user hook — invisible to the
mesh, refusing two ports the mesh declared open, and when it was removed, carrying an allowance
every module reaching another by the machine's own name had been relying on
([144](../04-ISSUES/144-the-predecessors-rules-outlive-the-firewall-it-was-found-as/00-report.md),
[145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md));
- the forward chain listed address ranges that followed neither the modules nor the machine
([141](../04-ISSUES/141-the-forward-chain-does-not-follow-the-modules/00-report.md)), answered by
[ADR 0140](0140-the-filter-constrains-what-arrives-from-outside.md) before this record;
- the networking module wrote two machine-wide files whole, so taking it restarted every
container ([084](../04-ISSUES/084-taking-networking-on-an-adopted-node-restarts-every-container/00-report.md)),
answered by [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md) and the hosts
file's marked region ([issue 128](../04-ISSUES/128-the-hosts-file-is-written-whole/00-report.md)).
Read on the four machines of this mesh on 2026-10-02, after every one had converged: on both
machines that had a front end it is inactive, and the host's record says the mesh retired it on
both — true of one, false of the other. On the home server the predecessor's chain is still in
force on the forwarded path, in the legacy packet filter the mesh's reader of rules does not
consult once a machine is converged, so that machine is filtered by two things and the mesh says
one. The host's reader already knows how to tell a table that refuses traffic from the runtime's
own plumbing and from a ban list; it is asked once, at adoption, and only to refuse a machine
whose firewall nobody speaks. Nothing asks it afterwards, and nothing reports what it saw.
The group's exit is one sentence: *a converged machine has exactly one thing filtering it, and
the mesh says truthfully which.* The first half the mesh can enforce only for what it owns; the
second half it can always do, and it is the half that was missing.
## Decision
**1. Convergence is a state the host keeps, not a step it takes once.** Every apply of a converged
declaration reads whether the found firewall is in force. Active — enabled again by a package, a
boot, a hand — it is retired again and said. The record distinguishes *the mesh disabled it* from
*it was found inactive*, and a reconcile that finds it inactive never records that the mesh did
it. When the step is skipped because the apply had failures, the report says the found firewall
was left in force and why; a step that does nothing is never silent.
**2. The host reports what filters the machine, with every apply, adopted or converged.** Every
table of the packet filter, and every chain of the legacy filter, that refuses traffic — a drop or
a reject, or a base chain whose policy drops — with an owner: the *mesh's*, the *found firewall's*,
the *container runtime's own*, a *ban* (a refusal that names the sources it refuses, in a chain
that accepts nothing), or *other*. The runtime's own is its plumbing — its chains, the forward
policy it sets when it turns forwarding on, its guard against reaching a container's address from
off its bridge. The user chain the runtime leaves for an administrator is not the runtime's:
anything refusing in it is *other*, which is where both predecessors' chains lived. Each entry
says in one line what it refuses. The mesh removes none of it: a rule it did not write is the
operator's to remove, now that they can see it.
**3. The mesh says which.** `node show` lists the filters with their owners. `status` names every
converged machine that something other than the mesh's table, the runtime's plumbing and a ban
list filters, the way it names strays and untaken modules, and such a machine is not "all well".
The converge preview lists the filters found and the fate of each: the found firewall retired, the
runtime's and the bans left, *other* left and named — so a person knows before the flip that the
machine will not be filtered by the mesh alone until they remove it, and what they would be
removing. *A converged machine is filtered by the mesh alone* when its list holds nothing but the
mesh's, the runtime's own and bans.
**4. Adoption's threshold does not move.** A machine whose front end nobody speaks is still refused
adoption; a refusing rule in the runtime's user chain still does not refuse it — on both machines
of this mesh it would have, and the migration would not have happened. It is reported instead,
from the first report on.
**5. Two of the group's issues are settled by records already accepted.** The forward chain follows
the machine's outward links and says nothing about networks ([ADR 0140](0140-the-filter-constrains-what-arrives-from-outside.md)),
which answers 141 whole. The runtime's file is written into and reloaded, and the hosts file's
region is the mesh's alone ([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md),
[issue 128](../04-ISSUES/128-the-hosts-file-is-written-whole/00-report.md)), which answers 084. One
machine-wide file the mesh still writes whole is its own filter, at the path the distribution's
packet filter reads; an operator's own rules at that path would be contested, and are held as
found until the filter module is taken ([ADR 0163](0163-taking-a-module-over-is-a-comparison.md)).
That is a difference a take shows, not a fault, and is decided when it bites.
## Consequences
- The host's report grows by the filters it found and, for a converged machine, the state of its
found firewall and who retired it; the controller keeps both on the node's record.
- `retireFirewall` runs on every converged apply and can disable the found firewall more than
once; the record's *disabled by the mesh* means exactly that.
- The reader of rules gains an owner per table and chain; what it refuses adoption for does not
change. A ban stays what it was: not a firewall.
- Issues 143 and 144 close on rules 1 to 3 once a machine's record names the predecessor's chain;
141 closes on ADR 0140 and 084 on ADR 0102, both by reading.
- Removing what is reported is the operator's act, by hand, with the preview's words in front of
them. The mesh never flushes and never deletes a rule it did not mark.
## How this is checked
| Rule | Checked by |
|---|---|
| Every refusing table and chain is classified, the user chain's refusals as *other* | host tests over rulesets captured from three machines of this mesh: a predecessor's chain in the legacy filter, a ban list and empty front-end chains beside the runtime's, a virtualisation host and an endpoint agent that refuse nothing |
| The found firewall active again on a converged machine is retired again and said; found inactive is recorded as found, not done; a skipped step is said | host tests over a fake front end |
| The report carries the filters and the found firewall's state for a converged machine | a host test reading the report |
| `node show` lists filters with owners; `status` names a converged machine something else filters and is not well; the preview lists filters and fates | controller tests over a fixture report |
| Live | the home server's record names the predecessor's chain in the runtime's user chain as *other*; `status` names the machine; after the operator removes the chain, the next report drops it and `status` is well |
## Built and proven live, 2026-10-02
> **Progressive insight — 2026-10-02.** The decision stands; these are the facts of its building.
Built in mesh-host 67 (every refusing table and legacy chain classified with an owner, reported with
every apply; the found firewall retired on every converged apply, *found inactive* kept apart from
*disabled by the mesh*, a skipped step said) and mesh-controller 211 (kept per node, shown on `node
show`, named by `status` and not well, previewed with fates). The live row was read at 10:10Z: the home
server's record named the predecessor's chain in the legacy filter's user chain as *other*, beside two
chains a retired front end left in the IPv6 legacy filter; the control node's record named the same two
leftovers; the laptop and the workstation read *the mesh alone*; `status` named both machines. The five
rule sets were removed at 12:46Z through the packet filter seat's `remove` verb
([ADR 0170](0170-the-firewall-seat-serves-its-verbs.md)), and the next report read *the mesh alone* on
all four machines. The control node's record still says the mesh retired its front end, which issue 143
records as a hand's work: the host trusts its record, and from this build on the distinction is kept.
## References
- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md), [ADR 0103](0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md), [ADR 0140](0140-the-filter-constrains-what-arrives-from-outside.md), [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md), [ADR 0163](0163-taking-a-module-over-is-a-comparison.md)
- [Design 08 — Connectivity](../03-DESIGN/01-to-be/08-connectivity.md), [Design 05 — The node host](../03-DESIGN/01-to-be/05-the-node-host.md)
- Issues 084, 141, 143, 144, 145
@@ -0,0 +1,89 @@
---
topic: the mesh
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0004-a-node-and-how-it-joins.md
---
# 169. A machine joins through the tunnel, and the bus is never public
## Context
The bus is the one channel every machine depends on: enrolment, every declaration, every tool. The
`nats` module declares it reachable from the mesh only. The controller still opens it to the whole
internet on the machine that runs it, as a *foundation* port that no module declares and nothing may
close ([issue 051](../04-ISSUES/051-the-mesh-cannot-update-what-it-depends-on/00-report.md)).
The reason is joining. [ADR 0004](0004-a-node-and-how-it-joins.md) has a new machine enrol over the bus
**before** it has a tunnel. [ADR 0007](0007-connectivity.md) states it as a requirement: the node
running the broker must be reachable from wherever nodes are, at a stable address.
So the bus listens on the internet permanently, for an event that happens a few times a year. A
sweep of every machine on 2026-10-02 found no client using the public path. Every connection arrives
over the tunnel or from the machine itself. The join token does not use it either: it carries the
controller's configured broker address, a mesh name with the old broker's port.
ADR 0004 already says what a joining machine needs: *an identity, an address, and one peer to reach*.
The tunnel can be that peer, if the hub knows the new machine's key before the machine first knocks.
WireGuard answers nothing to a key it does not know, which is why the tunnel's own port is safe to
leave open where the bus's is not.
## Considered Options
1. **Keep the bus public.** It is authenticated and encrypted, but every exposure of it, and of the
server behind it, is exposure of the one thing everything depends on.
2. **Open the bus publicly only while a join token is live.** Small, and the hub is open only during a
join window. But the window is real, the rule is about time rather than about who may reach the
bus, and the opening and closing are pushes that can fail between them.
3. **The controller makes the new machine's tunnel key and puts it in the token.** One step for the
operator, but the private half leaves a machine it does not belong to. ADR 0004 refuses that for
every key a node holds.
4. **The machine makes its key first, and the token is issued for it.** The machine prints the public
half of its tunnel key. The operator issues the token for that key. The controller gives the
machine its address and adds it as a peer on the hub. The token carries the hub's tunnel endpoint
and key, the machine's address, and the bus's address on the private network. The machine brings
up its tunnel and enrols over it.
## Decision
**Option 4.**
- **A machine makes its own tunnel key before it has a token**, and prints the public half. The private
half never leaves it, as ADR 0004 says of every key a node holds.
- **A token is issued for a tunnel key.** Issuing it assigns the machine's address on the private
network, records the key, and makes the machine a peer of the hub. The hub is sent that before the
token is shown, so the tunnel answers the moment the machine first uses it.
- **The token carries the one peer.** It adds the hub's tunnel endpoint and public key and the
machine's own address. **Where** becomes the bus's address on the private network, which needs no
name resolution.
- **The machine joins through the tunnel.** It brings the tunnel up from the token alone, then enrols
over it exactly as before. The enrolment checks that the key it is offered is the one the token was
issued for.
- **The bus is never public.** It is no longer a foundation port. Its reach is what the `nats` module
declares: the mesh. The tunnel's port stays open, as the one way in.
This changes three things earlier records say. ADR 0004's *where* is the bus's private address, and the
token carries the peer. ADR 0007's requirement that the broker be reachable from wherever nodes are
becomes: **the hub's tunnel is**. Issue 051's broker port stops being a foundation port.
## Consequences
- Joining is two commands on the new machine, with the token issued between them. A token issued for
the wrong key gives a tunnel that never answers, and the machine says so rather than timing out at
the bus.
- An unused token leaves a peer on the hub until it expires. Expiry removes it, the same way it voids
the secret.
- A machine already in the mesh is unaffected: it reaches the bus over its tunnel today.
- The genesis machine, the first one, raises the bus on itself and needs no tunnel to reach it.
## How this is checked
| Rule | Checked by |
|---|---|
| A token is refused without a tunnel key, and carries the hub's peer and the machine's address | a controller test |
| Issuing a token makes the machine a peer of the hub before the token is shown | a controller test over the hub's composed tunnel |
| An expired, unused token's peer is gone from the hub | a controller test |
| Enrolment refuses a tunnel key other than the one the token was issued for | a controller test |
| No machine's filter opens the bus to anywhere | a controller test over the composed filter, and the live sweep from outside the mesh |
| A new machine joins from outside the hub's network with the bus closed to it | the lab, then by hand |
@@ -0,0 +1,103 @@
---
topic: the mesh
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md
---
# 170. The firewall seat serves its verbs, and a foreign rule set is removed through one of them
## Context
[ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md) made the mesh say truthfully
what filters a converged machine, and left the removal of what it did not write to the operator's
hand. The first time that hand was needed — two machines, five rule sets a predecessor and a
retired front end had left — there was no mesh way to lend it: the packet filter is a seat
([ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)), a seat's
holder serves its verbs ([ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md),
[ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)), and the
firewall seat declared none. The only remaining path was a shell on the machine, which is the path
the mesh exists to replace, and which the operator's own tooling rightly refused to an agent.
A seat's verbs are the contract every holder implements, whatever filter it speaks. What a person
asks a machine's packet filter is the same whether nftables, a front end or a legacy filter answers:
what are the rules, reload the mesh's own, remove this thing the mesh did not write. What differs by
filter is the holder's own business and may be its own tools beside the seat's.
## Decision
**1. The `node-packet-filter` seat serves three verbs**, and a module that claims it serves all
three or is refused the claim, as with every seat:
- `rules` — the packet filter as the machine enforces it now: the nftables ruleset, and the legacy
filter's listings where that tool exists; narrowed to one table or chain when asked. Read-only.
- `reload` — load the mesh's own filter again from the file the mesh writes, and answer with the
mesh's table as loaded. The holder's own act on the holder's own rules.
- `remove` — remove one rule set the mesh did not write, named exactly as the host reports it under
ADR 0168 (`chain HAL-MESH-ONLY (iptables-legacy)`, `table ip6 filter, chain DOCKER-USER`), and
answer with what was done. It refuses the mesh's own tables, the container runtime's own chains,
a built-in chain other than the runtime's user chain, and any chain of a found firewall that is
in force. The runtime's user chain is emptied back to its one return; another chain loses the
jumps into it, is flushed and deleted; a table of the machine's own is deleted whole. Each is an
operator's act, by name, on one thing the mesh reported — never a flush, never a rule the mesh
itself marked.
**2. A holder may serve its own tools beside the seat's.** The nftables module keeps its reading of
the mesh's table as its own tool, and a holder speaking a filter with specifics of its own may add
tools for them; the seat's three are what every holder owes.
**3. A container may ask for a capability.** Serving `remove` and `reload` needs the machine's
network namespace and the right to change its packet filter; a holder's runtime declares
`capabilities: ["NET_ADMIN"]` on its container and runs on the machine's network. The host grants
exactly the capabilities declared, names them in the container's spec so a change recreates it, and
refuses a name that is not a capability's. A privileged container stays undeclarable.
**4. ADR 0168's "by hand" is read as "by the operator, through the seat".** Removing what the mesh
reports as *other* is still the operator's act and is still never the mesh's own doing; the verb is
how the act reaches the machine, recorded on the bus like every other, instead of a shell.
## Consequences
- The seat's row gains the three verbs; a mesh that already runs widens its row at the next
controller start. The nftables module claims them and gains a runtime — a tool server with the
packet filter's tools in its image, on the machine's network, with `NET_ADMIN`.
- The host's container vocabulary grows by `capabilities`; an older host refuses a declaration that
carries it, so the host rolls before the module.
- The two machines of this mesh that ADR 0168 found not filtered by the mesh alone are cleaned
through `remove`, and read *the mesh alone* afterwards; `status` returns to well without a hand on
either machine.
## How this is checked
| Rule | Checked by |
|---|---|
| The seat declares the three verbs; a claim that serves fewer is refused by name | the catalogue's seat tests |
| `remove` refuses the mesh's tables, the runtime's chains, a built-in chain and an active front end's chains, and removes a user chain with its jumps, empties the user chain, deletes an own table | the module's tests over a fake command runner, with the shapes the host reported live |
| A container's capabilities reach the runtime and its spec; an unknown name is refused | host tests |
| Live | `node-packet-filter.remove@<node>` on the home server and the control node; `node show` reads *the mesh alone* on both; `status` is well |
## Built and proven live, 2026-10-02
> **Progressive insight — 2026-10-02.** The decision stands; these are the facts of its building.
> Written as 0169 for three hours and renumbered to 0170: another record took 0169 on main first,
> and the check that refuses a shared number covered issues only (now records too).
Built in mesh-host 68 (`capabilities` on a container), mesh-controller 212 (the seat's three verbs)
and 213 (the filter file a module names under `filtering.into` counts as declared for a mount — the
module's first build was refused without it), mesh-catalog 216 (the nftables module's runtime and
verbs) and mesh-tools 27 (the console lists a node-scoped seat's verbs with their scope and carries the
machine; before it, the verbs were live on four machines and unreachable from the console —
[issue 199](../04-ISSUES/199-a-node-scoped-seats-verb-could-not-be-called-through-the-console/00-report.md)).
Each machine's holder was issued its bus account with `mesh-controller.issue`, the broker node pushed
first. At 12:46Z the five rule sets ADR 0168 had named were removed through
`node-packet-filter.remove`, three on the home server and two on the control node, each answering
with the commands it ran; the next report read *the mesh alone* on all four machines and `status`
listed nothing under `filtered`. The live row is read. What it cost on the way is
[issue 200](../04-ISSUES/200-the-controllers-answer-to-the-console-is-refused-by-the-bus/00-report.md).
## References
- [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md), [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md), [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)
- [Design 33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md), [Design 08 — Connectivity](../03-DESIGN/01-to-be/08-connectivity.md)
@@ -0,0 +1,59 @@
---
topic: the mesh
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0016-the-lab.md
---
# 172. The lab is a module, and runs a bed when the mesh asks
## Context
The lab raises virtual machines and runs the mesh on them, end to end, before a change reaches a real
machine ([ADR 0016](0016-the-lab.md)). It runs on one machine of the mesh, the one with the
virtualisation it needs. Until now the only way to start a bed there was to sign in to that machine and
run the lab's command line by hand, with a dozen environment variables pointing at sibling checkouts.
Nothing in the mesh could ask for it. An agent working through the mesh's own tools could build,
merge and push a change, and could not prove it in the lab first. The operator's direction on
2026-10-02: work on another machine goes through a mesh tool, not a shell on it.
## Considered Options
1. **Keep the lab a command line on one machine.** Every run is a person, or an agent with a shell on
that machine, outside the mesh.
2. **The lab is a module.** Assigned to the machine that can run it, serving tools that run a bed
against named branches and say how it went.
## Decision
**Option 2.**
- **A `lab` module, assigned where the lab can run**, serves five tools: whether this machine can run
beds, run beds against a branch per repository, a run's state, its log, and stopping it.
- **A run is the lab's own suite**, against fresh checkouts of the named branches from the mesh's forge,
side by side as the lab expects them. It builds what the beds place from those checkouts, as the suite
already does. It answers at once with an id, like a build: a bed takes minutes, and a call does not.
- **Only branches on the forge are run**, never code handed to the tool. What a run tested is what the
forge holds at the commit it names.
- **The lab is reached over the mesh only.** Its tools travel the bus, and the module opens no port.
- **No grant beyond the mesh's own.** Running a bed is root on the lab's machine, but anyone who can call
the mesh's tools can already do worse. The operator's judgement on 2026-10-02.
## Consequences
- An agent proves a change in the lab through the mesh, the same way it builds and pushes one.
- The lab's machine carries a module whose runtime holds the virtualisation's and the container
runtime's sockets, and a toolchain to build the mesh with.
- A run's checkouts are its own, so two runs never build from each other's tree. Old ones are removed
when their run ends.
## How this is checked
| Rule | Checked by |
|---|---|
| A run checks out exactly the named branches, and reports the commits it tested | the module's tests over a forge fixture, and each run's answer |
| A run answers at once, and its state and log follow it to the end | by hand, the first run |
| The module opens no port | the composed filter of the lab's machine |
@@ -0,0 +1,108 @@
---
topic: what runs on it
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0040-what-a-module-is.md
---
# 173. The operator's machine is the mesh's, and a module is whatever it declares
## Context
[ADR 0040](0040-what-a-module-is.md) says a module is *one self-contained piece of software the
mesh installs and manages*, and every example it gives is a service: a database, an analytics
server, a forge. The catalogue followed the examples. Of the predecessor's 34 modules on one
workstation, 28 are the operator's environment — a login manager, a window manager with 88 files
and four flavors, a shell, a terminal, a launcher, an audio setup, scripts — and the migration
scoped all 28 out as *the workstation's own environment*, to be managed by nobody
([research 018](../01-RESEARCH/018-the-operators-machine-as-modules/02-what-exists-and-what-is-missing.md)).
Since the predecessor retired, nobody is exactly who manages them: a fix is a hand edit that
nothing records and nothing regenerates.
[To-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) reached under the home for
one directory and drew a boundary inside it. The operator's statement is wider: *the mesh manages
my entire machine, all four of them, as far as it makes sense* — system folders and the home
alike, the servers and the workstations from the same catalogue. And the operator refused a
distinction this effort first drew between modules that ship code and modules that ship only
declarations: *a module can have some tools, a seat implementation, some containers, a unit, a
binary, some config files — one of these, or all, or two.*
## Considered Options
1. **Keep 0040's reading and manage the environment outside the catalogue** — dotfiles in a
repository, a script that places them. Rejected: that is the predecessor's first two days, the
origin of every inherited shape [as-is 10](../03-DESIGN/00-as-is/10-module-catalogue.md)
documents, and it puts the one thing a person looks at outside the one mechanism that is
checked.
2. **Add a second kind of module for configuration** — a "config module" with files and no
process. Rejected by the operator: a kind is a distinction the manifest already makes by what
it declares, and a second kind is a second set of rules to keep in step.
3. **One definition: a module is one managed thing, described by what it declares.** Chosen.
## Decision
**1. Everything configurable on a node is declared by a module.** Services, and equally the login
manager, the display server, the window manager, the shell, the terminal, the launcher, the
notifier, the audio setup, the boot images, the package manager's configuration, the agent at the
terminal, and a folder a person works in. The test is *can it be configured on a machine*; if it
can, some module owns it. What no module declares is found and left alone, as adoption already
says of a machine ([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)).
**2. A module is whatever it declares, and there are no kinds of module.** A package, files, a
container, a unit, a binary, a seat claim, tools — any one, or all. 0040's *one self-contained piece
of software* stands; its examples were services, and that was the whole of the bias. A downloads
folder with a process that tidies it, backs it up and answers questions about it is a piece of
software by 0040's own test, and so is a shell that is a package, three files and a seat.
**3. The home has no boundary of its own.** A file under the operator's home is placed and owned
the way [to-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) §2 built it: by a
module, resolved against the account's home, owned by the account. Which files are the mesh's is
decided by what modules declare, not by a line drawn through a directory. A person's documents,
projects and history are data under [ADR 0051](0051-shared-data-is-the-operators.md) and no module
declares them.
**4. One module ships one default configuration.** No flavors. What differed between the
predecessor's four flavors of one desktop module is what [ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)
is for.
**5. Servers and workstations take the same catalogue.** A module declares what it needs; a
machine reports what it has; assignment refuses by name
([ADR 0161](0161-what-deserves-a-seat.md) §3). The shell, the prompt, git and the agent are universal.
A display server needs a graphical session; a window manager needs the display server held. Nothing
in a manifest says *workstation*.
## Consequences
- The catalogue grows by a family of modules that run no service. Each is still built,
registered, assigned, pushed and reported like every other, and `status` says whether a
machine has applied them.
- The account fact becomes load-bearing for every node a person uses. Today it is empty on all
four node records of this mesh; stating it is the first step of the build.
- A module that *installs* a thing is distinct from a module that *holds its role*: zsh, fish and
bash may all be installed, and one holds the login shell
([ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)).
- The host's `package` shape drives the distribution's package manager only. A module whose
package is outside the distribution's repositories — the login manager in use is one — needs
either an official package or a shape the host does not have. Recorded as a gap, not decided.
- The predecessor's hooks go. What they did becomes declared state the host applies, or a verb a
seat serves ([ADR 0177](0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md)).
## How it is checked
| Rule | Checked by |
|---|---|
| A manifest with no container, no unit and no binary registers and resolves like any other | the catalogue's registration tests, with a package-and-files manifest |
| A file resource under the home resolves against the account and is owned by it | the controller's composition tests (to-be 29 §2, built) |
| A home-scoped module is refused on a node with no account, naming the fact | the same tests |
| A module needing a capability the machine lacks is refused by name | the resolver's tests (ADR 0161 §3) |
## References
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/00-overview.md), documents
01 and 02 — the behaviour wanted and the inventory measured.
- [ADR 0040](0040-what-a-module-is.md), [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md),
[ADR 0051](0051-shared-data-is-the-operators.md), [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)
- [To-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) — the account and the home
as a placement root, built; the records for them are proposed in an open change.
@@ -0,0 +1,92 @@
---
topic: building it
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0011-managed-files-are-generated-never-edited.md
---
# 174. A node varies a module through settings and kept regions, never through an edit
## Context
[ADR 0011](0011-managed-files-are-generated-never-edited.md) says a managed file is derived and an
edit to it is overwritten without warning. The predecessor said the same and then undid it twice:
a `merge` strategy that adopted disk drift back into its database, so a local edit became the
record; and a theming layer of about 90 environment variables substituted into templates at sync
time, with tools to list and set them, so that *nearly every value was a variable* — a second
configuration language laid over the first.
The operator wants both the variation and the rule. One window-manager module with one default
configuration, and each node tweaking it; and the file carrying the wanted value rather than a
variable the file reads. Two mechanisms already exist for exactly this: a **setting**, declared by
the module and set per mesh or per node, rendered at composition
(`${setting:…}` is live in the resolver's manifest); and a **kept region**, a block in a file the
mesh writes *into* where the operator's own lines survive every push
([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md), used by the ssh-client module
for the operator's own `Host` blocks).
What stands in the way is [issue 168](../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md):
a setting today reaches every mergeable file and every contribution of its module. Ninety theme
knobs on that mechanism would reach ninety files. The record that fixes it — a setting declared
with its type, meaning, default and the file it lands in — is proposed in an open change alongside
the container-runtime records.
## Considered Options
1. **Carry the predecessor's merge strategy.** A local edit is adopted into the node's layer.
Rejected: two writers and no arbiter, which is the option 0011 removed, and the reason a
`/model` choice was silently reverted on every node for weeks before anyone found the cause.
2. **Carry the environment-variable theming.** Rejected by the operator: the value belongs in
the file; a variable the file reads is a second place for the same fact.
3. **A per-node file override** — a whole file replaced for one node. Rejected: it is a flavor
under another name, and a module update then misses that node entirely.
4. **Settings rendered into the file, and kept regions, and nothing else.** Chosen.
## Decision
**A node varies a module in exactly two ways.**
- **A setting.** Declared by the module with a default, set for the mesh or for one node, rendered
into the file at composition. The value is in the file. Asked, the mesh lists every setting
with its effective value and where it came from.
- **A kept region.** A marked block in a file the mesh writes into, in which the operator's own
lines are kept across every push and given back when the module goes (ADR 0102).
**An edit outside a kept region is overwritten, as ADR 0011 says, and never adopted.** Nothing
reads a managed file back into the record.
**The predecessor's theme knobs become settings** of the modules whose files they render — the
window manager's colours are the window manager's settings, the bar's are the bar's — each
landing in the file that reads it and no other.
**Issue 168 is fixed before any environment module declares a setting.** A setting must name the
file it lands in; until that ships, the environment modules carry their defaults in their files
and no settings.
## Consequences
- No flavors, no per-node file copies, no environment layer. A module's definition is one set of
files; a node's difference is data in its layer, visible by asking.
- The settings record proposed alongside the container-runtime records is on the critical path
of every module with a knob, and this record depends on it shipping as proposed.
- A kept region is the only place a person edits a managed file, and the file says where it is.
The operator's own prompt customisations, aliases and window rules live there.
- What got harder: a change that is neither a setting the module declared nor the operator's own
lines has no home, and is refused by the mechanism rather than silently kept. That is the point.
## How it is checked
| Rule | Checked by |
|---|---|
| A setting reaches only the file its declaration names | the controller's settings tests, once the proposed record ships; issue 168 closes on it |
| A kept region survives a push with its content and is given back on undeclare | the host's write-into tests (ADR 0102), with a region declared by an environment module |
| An edit outside a region does not survive a push | the same tests, asserting the file equals the composed content outside the region |
| Every effective value names its source | `mesh-controller.settings` and the module's own `show-config` tool |
## References
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/01-the-intended-behaviour.md) §"One default, varied by settings, never by edits"
- [ADR 0011](0011-managed-files-are-generated-never-edited.md), [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md),
[issue 168](../04-ISSUES/168-a-setting-reaches-every-file-and-contribution/00-report.md)
@@ -0,0 +1,122 @@
---
topic: what runs on it
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 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](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)).
What *runs* that code is [ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md):
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](../01-RESEARCH/018-the-operators-machine-as-modules/03-one-tool-executor-per-node.md)):
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](0005-the-node-host.md)): 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](0152-the-operators-surface-is-a-module-the-console.md)
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](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)); a mesh-scoped seat's verbs
run on the node that holds it ([ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)).
**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](0170-the-firewall-seat-serves-its-verbs.md) §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
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/03-one-tool-executor-per-node.md)
- [ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md), [ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md),
[ADR 0152](0152-the-operators-surface-is-a-module-the-console.md), [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md),
[ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md), [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)
- [To-be 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md), [to-be 34](../03-DESIGN/01-to-be/34-the-console.md)
@@ -0,0 +1,81 @@
---
topic: what runs on it
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
---
# 176. The login shell is a node seat held by one shell module, and `execute` is its contract
## Context
[ADR 0040](0040-what-a-module-is.md) names the shell as its example of a *shared* seat: bash, zsh
and fish all join `shell`, and one may be default. The operator's reading is sharper, and it
matches [ADR 0126](0126-a-module-declares-its-own-seats.md) better: *installing* a shell is
installing software, and several may be installed; *holding* the seat is being the login shell,
which a node has exactly one of. A definition says which seats a module can hold; the assignment
says which it does.
A seat carries the tools its holder must serve ([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)),
and [to-be 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) leaves which verbs each
seat serves as a decision per seat, taken slowly. This is the first seat of the operator's
environment, and the one every node has.
## Considered Options
1. **A shared `shell` seat with a default**, as 0040's example reads. Rejected: *default* is a
second concept beside *holder* for the same fact, and the `user` shape already makes the
login shell declared state ([to-be 05](../03-DESIGN/01-to-be/05-the-node-host.md)).
2. **No seat; each shell module sets the login shell for itself.** Rejected: two assigned shell
modules would fight over `chsh`, and nothing would say which won.
3. **An exclusive node-scoped seat, `login-shell`, declared by the shell modules, held by one
per node.** Chosen.
## Decision
**1. `login-shell` is a node-scoped seat declared by the shell modules.** zsh, fish and bash each
declare that they can hold it; a node's assignment says which does; the controller refuses a
second holder by name as for every seat. A shell module that is assigned without holding the seat
is installed and nothing more.
**2. Holding the seat sets the account's login shell.** The holder's declaration carries the
`user` shape with the shell it provides, so the login shell is declared state the host applies and
gives back when the holding moves — `chsh` stops being a hook.
**3. The seat's contract is `execute`.** One verb, one argument, the command, run on the node the
seat is scoped to as the operator account, answering with what it printed and how it exited.
Every holder serves it; a holder may serve its own tools beside it
([ADR 0170](0170-the-firewall-seat-serves-its-verbs.md) §2) — show the rendered configuration, list
the plugins, set a prompt value.
**4. Any node may call it on any node.** The grant is the node tools runtime's
([ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md) §5):
*run `uptime` on every node* is five calls to one verb.
## Consequences
- The first environment module is a shell: a package, files under the home owned by the account,
a seat declaration and claim, a `user` shape, and one tool. It proves the whole pattern on every
node, servers included, before anything graphical is written.
- ADR 0040's shell example is read as *installed is not holding*; a dated note in that record says
so. Its decision is untouched.
- `execute` is a shell on every machine, addressed over the bus. That is the point, and it is
the widest verb the mesh serves; it exists because the operator decided every node may call
every tool, and this record does not narrow that.
## How it is checked
| Rule | Checked by |
|---|---|
| Two shell modules assigned to one node, one holding: one `user` shape in the declaration, naming the holder's shell | the controller's composition tests |
| A second claimant is refused by name | the catalogue's seat tests |
| `execute` runs as the account and answers output and exit status | the module's tool tests over a fake runner, and live on every node |
| The seat's verb appears with its scope and machine in the node tools listing | the runtime's tests (to-be 33 §4) |
## References
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/04-the-seats-of-the-environment.md)
- [ADR 0040](0040-what-a-module-is.md), [ADR 0126](0126-a-module-declares-its-own-seats.md),
[ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md), [ADR 0170](0170-the-firewall-seat-serves-its-verbs.md)
@@ -0,0 +1,80 @@
---
topic: what runs on it
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md
---
# 177. A unit may be user-scoped, and the service manager is a node seat whose holder answers for the units
## Context
The host's `service` shape puts a system unit into a state. It has no user scope.
[To-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) states the gap: *a
workstation's per-user daemons have no form the mesh can send.* Four of the predecessor's
environment modules ship user units — the desktop's reload watcher and bar watchdog, the audio
module's masks, the power module's memory guard, the thermal daemon's profile switcher — and the
predecessor needed a hook to enable them because *shipping a unit file does not run it*; one unit
was deployed for months and ran on one machine only.
[ADR 0040](0040-what-a-module-is.md) says the host hardcodes no supervisor, and a swappable
machine mechanism is a module implementing a capability — which is what the nftables module is for
the packet filter ([ADR 0170](0170-the-firewall-seat-serves-its-verbs.md)). The service manager is
reported today as a capability, `service-manager`, and held by nobody. The operator's proposal: a
systemd module that holds the seat and serves the tools about units, system and user.
## Considered Options
1. **Keep user units as a module concern** — each module runs `systemctl --user` in a hook.
Rejected: that is the hook that silently never ran, and an action over the link is refused.
2. **The service-manager module applies units** on behalf of others, as a provision. Rejected by
the operator: provisioning is for resources a provider creates for a consumer; a unit is
declared state the host applies, as every resource is.
3. **The host's `service` shape gains a user scope; a systemd module holds the service-manager
seat and serves the verbs about units.** Chosen.
## Decision
**1. The `service` shape gains `scope`: `system` (the default) or `user`.** A user-scoped unit
is applied as the operator account through the account's own service manager: enabled, started,
stopped, reloaded on its triggers, exactly as a system unit is, and refused on a node with no
account, naming the fact. The host applies it; no module does.
**2. `node-service-manager` is a seat of the mesh's own, node-scoped**, seeded by the controller
under this record, as ADR 0121 requires of a `node-*` name. The `systemd` module claims it and is
assigned to every machine whose profile reports `service-manager`.
**3. The seat's verbs answer for every unit on the machine**, each taking an optional `scope`:
`units`, `status`, `start`, `stop`, `restart`, `enable`, `disable`, `journal`. The host applies what
is declared; the holder answers questions and operator acts about it, and says, for a mesh-held
unit, that the host will restore what its declaration says.
## Consequences
- The host's vocabulary grows by one field on one shape, asserted by its count test
([to-be 05](../03-DESIGN/01-to-be/05-the-node-host.md)); an older host refuses a declaration
carrying it, so the host rolls before the first module that uses it.
- The predecessor's four user-unit modules become declarable without a hook.
- The seat's holder is the first system seat held by a module that runs nothing of its own: its
verbs are served by the node tools runtime ([ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)).
- What got harder: `journal` and `status` on a user unit need the account's manager reachable
from the runtime's process, which runs as the node's account; the holder's tool escalates or
switches user as it needs, which is ADR 0175 §4 applied.
## How it is checked
| Rule | Checked by |
|---|---|
| A `service` with `scope: user` is enabled and started under the account, and refused with no account | the host's tests with a fake service manager |
| The seat declares its verbs; a claim serving fewer is refused by name | the catalogue's seat tests |
| The verbs act on a named unit in the named scope and name the unit's holder when the mesh declares it | the module's tests over a fake runner |
| Live | the desktop's reload watcher declared `scope: user` on a workstation; `node-service-manager.status@<node>` reports it active |
## References
- [Research 018](../01-RESEARCH/018-the-operators-machine-as-modules/04-the-seats-of-the-environment.md)
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md),
[ADR 0040](0040-what-a-module-is.md), [ADR 0170](0170-the-firewall-seat-serves-its-verbs.md)
- [To-be 05](../03-DESIGN/01-to-be/05-the-node-host.md), [to-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md)
@@ -0,0 +1,132 @@
---
topic: the mesh
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md
---
# 179. The intrusion seat serves its verbs, a container may log to the journal, and every door declares its jail
## Context
Read on the control node on 2026-10-02, the day the machines were confirmed filtered by the mesh
alone ([ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md)): the intrusion
prevention watched one door. Its two jails read the ssh daemon's journal and its own log, banned
five failures in ten minutes for ten minutes, and in a day had seen twelve thousand failed logins
from three hundred addresses and banned none of the busiest, which paced themselves at one try every
ten minutes. The mail submission port took a hundred and sixty password guesses in the same day from
thirty-eight addresses with no jail reading it at all; the forge and the public proxy had no jail
either, and the proxy logged nothing a jail could read. Nobody could see the jails without a shell:
the module's three tools existed in code and were served by nothing, and the seat it holds declared
no verbs.
Three things were missing and they are three shapes the mesh already has. The packet filter's seat
serves verbs every holder owes ([ADR 0170](0170-the-firewall-seat-serves-its-verbs.md)); the
intrusion seat serves none. A module's `listens` compose into the machine's filter, and [to-be 31](../03-DESIGN/01-to-be/31-a-module-declares-its-fail2ban-jail.md)
says a module's `jails` compose into the machine's intrusion prevention the same way — the controller
composes them, and no module declares one. And a jail reads a log; a container's output goes to a
file of the runtime's own under a path that changes when the container is recreated, which is why
no jail could read the mail front end, the forge or the proxy, however they logged.
## Decision
**1. The `node-intrusion-prevention` seat serves four verbs**, and a module that claims it serves
all four or is refused the claim, as with every seat:
- `status` — every jail with what it watches, how many addresses it is counting failures against and
holding now, and the totals since it started; one jail's detail when named. Read-only.
- `banned` — every address banned now, with the jail holding it, when it was banned and when the ban
ends. Read-only.
- `ban` — ban one address in one jail now, for that jail's ban time. An operator's act on the live
ban list, which the mesh composes the rules for and never writes itself.
- `unban` — let one address go, from one jail or from every jail.
A holder may serve its own tools beside these; the fail2ban module reads one jail's effective
settings as its own.
**2. A container may log to the journal.** `logging: journald` on a container has the host run it
with the journal as its log driver; the journal keeps the container's name on every line, and
`docker logs` keeps working. Where a container logs is part of its spec, so moving it recreates the
container, and the only place besides the runtime's own file is the journal: a machine's intrusion
prevention reads the journal already, for the ssh daemon, and a container that logs there is read
the same way, by the container's name, whatever the container is called by the runtime this time.
**3. A module with a door declares its jail, and the holder composes them.** What to-be 31 designed
is now the rule: a module whose service authenticates from outside — the mail front end, the forge,
the public proxy — declares in its manifest what a failed attempt looks like in its log and how to
ban on it, naming no node and no path; the module that holds the intrusion seat declares where the
composed jails and filters land, and the mesh writes them on every machine that runs both. A machine
not running the module has no such jail. The holder restarts its daemon on the composed file.
**4. The base is strict, and the mesh's own range is never banned.** Three failures in a day ban for
a day, on every jail unless the jail says otherwise; banned twice in two weeks, by any jail, is
banned for four. The attackers this mesh sees pace themselves under any ten-minute window; a day's
window counts them. A person who mistypes three times from one address is out for a day from that
address, and never from a machine of the mesh, whose range stays in the never-banned list the module
has carried since [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md). The operator
chose this knowing it.
**5. The proxy says a refused name in its log.** A request for a name this mesh does not serve, from
outside, is what a scanner does; the proxy already logged a certificate refused for such a name, and
now logs the plain request too, with the asking address last, as its own jail's filter expects it.
## Consequences
- The seat's row gains four verbs; a mesh that already runs widens its row at the next controller
start. The fail2ban module claims them and gains a runtime — a tool server whose image carries the
fail2ban client, with the daemon's socket shared in from the machine, and nothing else of the
machine. The daemon stays the machine's; what runs in the container is only the client.
- **That runtime is the shape the catalogue has today, and it is on its way out.**
[ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md),
accepted the same day as this record, replaces a tool container per module with one tool runtime
per node on the host side, taking each module's tools as a bundle. Nothing here depends on the
container: the verbs, the client that speaks to the daemon over its socket, and the jails are the
same code under either. This module converts with the packet filter's, whose runtime that record
names, and the socket it needs becomes the node runtime's to reach rather than a mount of its own.
- The host's container vocabulary grows by `logging`; an older host refuses a declaration that carries
it, so the host rolls before the modules. Three containers are recreated once, when their modules
are pushed with the field: the mail front end, the forge and the proxy — each a moment's outage.
- The fail2ban module declares where jails compose (`jailing`) and the directory the filters go in;
the mail, forge and proxy modules each declare one jail reading the journal by their container's
name. The composed jail file is the one resource the daemon restarts on when a module arrives or
leaves a machine.
- The two base jails and the composed ones take the day's window; the ssh jail's ten minutes are
gone. An address banned on the first day of this record stays banned for the day.
- The module's three old tools, served by nothing, are replaced by the seat's four verbs and one
own tool; `fail2ban_status` as a name is gone.
## How this is checked
| Rule | Checked by |
|---|---|
| The seat declares the four verbs; a claim that serves fewer is refused by name | the catalogue's seat tests |
| `status`, `banned`, `ban` and `unban` read and steer the daemon through its client, with the shapes fail2ban 1.1.0 printed live; a non-address and a non-name are refused before anything runs | the module's tests over a fake command runner |
| A container's `logging` reaches the runtime's arguments and its spec; a place other than the journal is refused | host tests |
| A module's jails compose into the holder's file and a filter per jail, and the file is written empty when none is declared | the controller's composition tests (to-be 31) |
| The proxy logs a refused name with the address last | the proxy's tests |
| A jail's pattern names `<HOST>` once per shape, since two is a duplicate capture group and costs the machine every ban | the catalogue's manifest tests |
| Live | done 2026-10-02: `status` and `banned` answered on both servers through the console; the proxy's jail counted seven refusals on the home server; a documentation address banned in the ssh jail came back with its end time and was released |
## Built and proven live, 2026-10-02
All five rules are in the mesh. The host carries `logging`; the controller's seat row carries the four
verbs and the proxy says a refused name in its log; the fail2ban module holds the seat from a runtime
with the daemon's socket shared in, composes the jails, and the mail front end, the forge and the
proxy each declare one. Through the console on the control node: `status` listed five jails with what
each watches, `banned` listed the nine the long jail holds, and a documentation address banned in the
ssh jail came back with its ban's end time and was released again. On the home server the proxy's jail
had counted seven refusals within minutes of starting.
**One fault, found by the machine and not by a test.** The proxy's pattern matched two shapes of
refusal in one expression and so named `<HOST>` twice. fail2ban expands that placeholder into a named
capture group; two of them is a duplicate group name, and the daemon refuses *its whole configuration*
and exits — both servers kept no bans at all for about ten minutes, every jail and not the one at
fault. The pattern is now one per shape. A manifest check refuses the mistake at merge time, naming
what it would cost, which is the only reason this record can claim the rule rather than the instance.
## References
- [ADR 0170](0170-the-firewall-seat-serves-its-verbs.md), [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md), [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)
- [Design 31 — A module declares its fail2ban jail](../03-DESIGN/01-to-be/31-a-module-declares-its-fail2ban-jail.md), [Design 33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md), [Design 08 — Connectivity](../03-DESIGN/01-to-be/08-connectivity.md)
@@ -0,0 +1,68 @@
---
topic: the mesh
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md
---
# 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
[ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md) retires the firewall a machine
was found with by disabling it, never flushing it, and keeps its configuration on disk so that
returning the node to adopted can enable it again. [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md)
made the host keep it retired and say so. Both machines of this mesh that had a front end have been
converged for days; neither is going back. What remained of the front end on each — its package,
its unit enabled for boot on one, its empty chains still wired into the kernel's hooks, a chain of
its container integration still dropping traffic on the IPv6 path until the day before this record
— was not a rollback path. It was software nobody runs, left where a reader finds it and asks
whether the machine has two firewalls.
The operator asked on 2026-10-02 that it be disabled and uninstalled. Disabled it already was. For
uninstalled, the host had no word: a package could be declared present and not absent.
## Decision
**1. A package may be declared absent.** `absent: true` on a package resource has the host remove
the package when it is installed and leave alone a machine that never had it, through the machine's
own package manager, dependencies untouched. A declaration that stops saying a package is absent
installs nothing: there is nothing to undo.
**2. The module that holds the packet filter seat declares the front end it replaced absent**, after
its own filter is loaded, so the mesh's table is in force before the front end's package goes. On a
converged machine the front end is therefore gone, not merely off; on an adopted machine nothing of
this runs, because the filter module is assigned by the flip and not before.
**3. Returning such a machine to adopted enables nothing.** A machine with no firewall needs no
openings ([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)); the host records the
front end as *removed*, says so once, and asks nothing of a command that is not there. What
ADR 0100 kept on disk for a return is kept only as far as the package manager keeps a changed
configuration file; the rollback path it described is given up on purpose.
## Consequences
- The host's vocabulary grows by `absent` on a package; an older host refuses a declaration
carrying it, so the host rolls before the module.
- The nftables module's declaration gains one resource; on the two machines of this mesh that were
found with ufw, the next push removes it.
- `node show` reads *found firewall: ufw, removed* on those machines from then on.
- ADR 0100's sentence about a return to adopted restoring the found firewall holds only while the
front end is installed, which after this record it is not on a converged machine.
## How this is checked
| Rule | Checked by |
|---|---|
| An absent package is removed when present, left when not, and read back | host tests over a fake package manager |
| An uninstalled front end is recorded as removed and nothing is asked of it | a host test with ufw missing on a converged apply |
| Live | done 2026-10-02: both machines report ufw gone — `pacman -Q ufw` has no answer, `node show` says *removed*, `status` is well. The home server said *retired* for six hours after the package went, because this record's step runs only after a clean apply ([ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md)) and one dead tracker was failing its applies ([ADR 0187](0187-a-dead-tracker-is-not-the-machines-failure.md)) |
## References
- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md), [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), [ADR 0170](0170-the-firewall-seat-serves-its-verbs.md)
- [Design 08 — Connectivity](../03-DESIGN/01-to-be/08-connectivity.md)
@@ -0,0 +1,118 @@
---
topic: what runs on it
status: accepted
date: 2026-09-27
deciders: jochen
reconstructed: true
extends: 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
---
# 181. The operator account is a node fact, and a home is a placement root
*Reconstructed. The controller shipped this on 2026-09-27 and
[to-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) recorded it as built without a
decision behind it. This record states what was decided, from the code and the design, and adds the
two rules the code left implicit — what an empty account means for a module, and that the account is
stated rather than discovered. Written 2026-10-02.*
## Context
[ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) took every host path out of a
module definition and gave a module's *system* data a place: a directory the mesh resolves under the
node's root, owned by the module. It said nothing about the other half of a filesystem — the files
that belong under a person's home and are owned by that person. The predecessor wrote several of
those: the ssh client configuration, the shell's configuration, an agent's instruction files. It knew
whose home it was writing into because each of its node records carried a login name. The mesh took
the machine facts over and dropped the human one.
The loss was found the ordinary way: `ssh <node>` logged into the home-server under the workstation's
own login name, because nothing in the mesh said the home-server's account was a different one
([to-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md),
[issue 172](../04-ISSUES/172-the-ssh-client-block-matches-one-spelling-of-a-machine/00-report.md)).
What the controller does since 2026-09-27: a node record carries an operator account and, optionally,
its home; the account and its home are machine facts a definition may name in a resource's path, owner
and content; a roster file may say it lives under the home, and is then rendered per node, placed under
that node's account's home, owned by the account, and left out on a node with no account. On
2026-10-02 **all four nodes of the live mesh carry an empty account**: the fact exists and nobody has
stated it, so no home-scoped resource can land anywhere yet.
## Considered Options
1. **The definition names the login.** `owner: <name>` in the module. Rejected: it is the installation
written into a definition, which ADR 0112 forbids and
[ADR 0155](0155-a-definition-names-no-installation-and-how-that-is-checked.md) checks for, and it is
wrong on the first machine whose login differs — which is exactly the machine that surfaced this.
2. **The host discovers the account.** The first non-system user, or whoever ran the enrolment.
Rejected: a guess. A shared machine has several people on it, a server may have none, and a host
deciding whose files these are is a decision the mesh then cannot see, state or correct.
3. **The account is a fact the operator states on the node record, and the home is derived from it
unless stated.** Chosen.
## Decision
**A node has an operator account: the login name of the person who works on it.** It is stated by the
operator on the node record, the way a node's address or mode is held there, and it is empty for a
machine nobody logs into. Empty is a real state, not a missing value. The mesh holds the fact because
everything below derives from it, and because it is precisely the fact that was lost when the
predecessor's records were not carried over.
**The account's home is derived unless stated.** The superuser's home for the superuser, the
distribution's conventional per-user home otherwise; a node whose account lives elsewhere states its
home. One place computes the default, so a fact and the record cannot disagree about it.
**A resource may be placed under the home, owned by the account.** This is ADR 0112's move one level
over: as a module's system directory is resolved under the node's root, a file under a person's home is
resolved against the account's home, and owned by the account rather than by root or a module's own
account. A definition names the account and its home as machine facts, never as a path; a roster fact
may say it is a home file and is then placed and owned the same way. The controller resolves both at
composition, and the host chowns what it creates.
**A node with no account cannot carry a home-scoped resource, and says so.** A roster fact that lives
under the home is left out of that node's declaration rather than written to nowhere. A resource naming
the account fact on such a node is refused at composition, naming the fact the machine does not have.
A module that writes a person's files is thereby unassignable to a machine with no person on it, which
is the right refusal.
**One account per node is what this record decides.** Several people on one machine is left open, with
the constraint that allowing it must not force the common case — one workstation, one person — to name
anything.
## Consequences
- **The operator states the account before any home-scoped module lands.** Today none is stated, so the
first assignment of such a module begins with four node records.
- The roster carries each node's account, so a composed ssh configuration logs in as the right person
on every machine — the gap that surfaced this, closed by the same fact.
- A family of modules becomes writable: everything the predecessor placed under a home — ssh client,
shell, the agent's instruction files — is now a module naming a fact rather than a path
([to-be 29 §2](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md)).
- **What got harder:** a definition cannot say "my user's home" without the mesh knowing who the user
is, so a module of this family is refused on a freshly enrolled machine until a person is named on
it. That is a prompt, not an obstacle.
- **Not decided here:** several accounts per node; a service unit running as the account rather than
as root or a module; a one-off step run as the account. Each is a record of its own.
## How it is checked
| Rule | Checked by |
|---|---|
| A resource's path and owner resolve the account and its home | controller tests on machine-fact resolution: a file naming the account facts lands under the account's home, owned by the account |
| The home is derived unless stated | a controller test: the superuser's home for the superuser, the conventional home otherwise, the stated home when one is stored |
| A home roster fact is left out on a node with no account | a controller test on roster composition: the file is absent from that node's declaration and present on a node with an account |
| A resource naming the account on a node with no account is refused by name | a controller test on machine-fact resolution: the refusal names `account` and lists the facts the machine does have |
| No definition names a home path | ADR 0112's catalogue test on host paths, which a `/home` or `/root` literal fails |
## References
- [to-be 29](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) — the design this record
gives a foundation to, and its "what has shipped" section
- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) — the system-path placement
this mirrors; [ADR 0155](0155-a-definition-names-no-installation-and-how-that-is-checked.md) — why
a login name may not be in a definition
- [ADR 0120](0120-a-roster-fact-carries-its-format-as-a-template.md) — the roster fact a home file
may be
- [ADR 0182](0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md) — what
the mesh may and may not do inside the home this record lets it reach
- mesh-controller `internal/inventory/nodes.go` (the account and its home on the node record),
`internal/catalogue/machine_into_files.go` and `roster.go` (resolution and the home fact)
@@ -0,0 +1,117 @@
---
topic: what runs on it
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md
---
# 182. 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
## Context
[ADR 0181](0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md) lets a module
place files under a person's home. A home is unlike any directory the mesh has written into so far:
it is shared with the person, and with every program the person runs. The agent's configuration
directory on the laptop makes the point. On 2026-10-02 it holds thirty entries. The predecessor placed
five of them (an instruction file, a conventions rule, a settings file it merged into, two skills); a
sibling module placed a sixth (the node's identity rule). The agent itself writes the other
twenty-four: its settings, its credentials, its history, the memory of every project it has worked in,
its plugins, its session logs. Several of those are what [to-be 15](../03-DESIGN/01-to-be/15-the-agent-session.md)
calls memory *written by the session itself and declared by nobody*: a mechanism that regenerated the
directory would erase a season of it, silently, while reporting success.
The predecessor's own module recorded the hazard in the other direction. Its settings file was first
shipped as *replace*, and every `/model` choice a person made inside a session was reverted to the
template's value on the next synchronisation — on every node, indefinitely, with no indication why. It
was changed to *merge*, and the comment explaining why is still in its manifest.
**And the generator is gone while its output stayed.** The predecessor was retired from the laptop on
2026-10-01. Its six files are still on both workstations, with their content telling every session to
use tools that no longer exist. Nothing owns them; nothing will ever rewrite or remove them.
[To-be 29 §3](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) drew the line for one directory,
`~/.ssh`: the mesh owns the directory and the files it places; it holds the person's private keys and
personal drop-ins as found. That was argued from the lockout `~/.ssh` can cause. The argument here is
the same shape with a different stake — the person's work rather than the person's way in — and it has
to hold for every directory the family of home-scoped modules will touch, so it is a rule, not a
section.
## Considered Options
1. **The module owns the directory whole**, regenerating it from the definition. Rejected: it destroys
the memory, history and local settings the agent writes for itself, which is the failure to-be 15
names and the predecessor's settings file demonstrated at small scale.
2. **The module owns only the files it names, and nothing about the directory.** Rejected: *owning one
file beside foreign ones is not owning anything* (to-be 29). The directory must exist, with the right
owner and mode, before the tool first runs on a fresh machine; and a credentials file in a
world-readable directory is a credentials file in the wrong directory.
3. **The module owns the directory and the files it places; a file the tool writes for itself is
written into, never over; everything else is held as found.** Chosen.
## Decision
**A home-scoped module owns the directory it declares: its existence, owner and mode.** The host creates
it if absent, owned by the account, and never removes it while it holds anything
([ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md)). Inside it, every path the module touches
is in exactly one of four classes, and **the class is visible in the definition from the shape
declared**, not inferred from what happened to be on disk:
| class | declared as | the host's rule |
|---|---|---|
| **owned** | a file with content, or a roster fact | written whole, regenerated, removed when undeclared; a file found there with no record of the mesh making it is kept once before it is written over ([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md)) |
| **written into** | a file written *into* a structured document | only the keys the definition names are set, every other key is kept, and each set key is given back when undeclared (ADR 0102). The key list is the module's and is short |
| **written by the module's own process** | nothing the host applies: the module's code writes it from what it was handed | the file's content is never a declared file's content, because a declaration travels in the clear on the bus and the host records it; the module's code writes it, owned by the account, atomically. [ADR 0183](0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md) says how for a credential |
| **found** | nothing | never read, never rewritten, never removed. The person's memory, history, projects, local settings, their own rules and skills |
**A file the tool writes for itself is written into, never over.** The agent's settings file and its
own state file are the tool's; the mesh has one or two facts to state in each. Setting those keys and
nothing else is what lets a person's `/model` choice survive a push, and what lets the mesh's keys be
taken back cleanly when the module goes.
**A predecessor's output is found.** A file placed by a generator that no longer exists is, to the
mesh, a file it has no record of making. Where the successor module keeps the path, declaring it
*adopts* it: the host keeps the original once and writes the mesh's. Where the successor does not keep
the path, the mesh does not remove the file, because it removes nothing it did not make; **the operator
removes it, once**, and the module's definition names those paths in its own documentation so the step
is not forgotten. This is the first instance of the one-off setup step to-be 29 leaves open, and the
rule chosen for it is that it is a person's act, listed, not a module's.
**The rule is the family's.** An ssh client module, a shell module, an agent module each declare their
directory and classify their paths this way. A module that cannot say which class a path is in has not
finished its definition.
## Consequences
- A person's work under their home survives every push and every unassign. The mesh's own files come
and go with the module; the mesh's keys in the tool's files come and go with it; the directory stays.
- **Stale files survive too.** Two workstations keep three predecessor files each until a person removes
them — a visible cost, accepted over a mesh that deletes under a person's home. A module author who
renames one of the mesh's own files has the ordinary path: the old resource id is undeclared and the
host removes what it made ([ADR 0118](0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md)).
- A module's definition is longer by a classification, and a reviewer has one more question per path.
That is the point: *which parts are managed must be explicit rather than inferred* (to-be 15).
- **What got harder:** a module cannot seed a person's preference once and leave it. A seeded file
([ADR 0087](0087-a-seeded-file-is-created-once.md)) is the shape for that, and it is available to
this family unchanged; what is refused is a seed the module later wants to change, because what grew
in it is the person's.
## How it is checked
| Rule | Checked by |
|---|---|
| The directory is created owned by the account and kept when the module goes | host tests of a directory resource with an owner (ADR 0051's and 0118's), and the family's lab check below |
| An owned file found with no record is kept once, then written | host tests of ADR 0102's kept-original rule |
| Only the declared keys of a written-into file change, and are given back | host tests of ADR 0102: declared keys set, the rest kept, restored when undeclared |
| Nothing found is touched | the family's lab check: a machine with a seeded home holding a person's file beside a predecessor's; after apply the person's file is byte-identical, the predecessor's is kept as the original, the mesh's keys are set and the person's keys in the same file remain; after unassign the mesh's files are gone, the keys are restored, the person's files are untouched and the directory stands |
| Every path a home-scoped module touches is classified | a catalogue review rule for this family: each path is a directory, a file, a file written into, a secret-and-step, or absent — the first module written to it is [to-be 36](../03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md) |
## References
- [to-be 29 §3](../03-DESIGN/01-to-be/29-a-node-has-operator-accounts.md) — the same boundary drawn for `~/.ssh`
- [to-be 15](../03-DESIGN/01-to-be/15-the-agent-session.md) — a session's memory is declared by nobody
- [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md), [ADR 0087](0087-a-seeded-file-is-created-once.md),
[ADR 0118](0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md), [ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md) — the mechanics each class rests on
- [ADR 0051](0051-shared-data-is-the-operators.md) — the third case the host had no word for: what it neither made nor configured
- the predecessor's `claude-code` module manifest, whose comment on `strategy: merge` records the reverted `/model` choice
@@ -0,0 +1,163 @@
---
topic: what runs on it
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0024-model-access-is-a-provision.md
---
# 183. 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
## Context
**The operator's stance, set on 2026-10-02 and sharpened during the day.** The controller has no part in
the agent module. The host is module-agnostic: it knows no vendor, no agent, no path under a home. The
agent module owns its own files. And there must be a *real* licence manager — a module that doles out
the correct licence in every situation the mesh has: two subscription accounts and one API key today,
used by a person's interactive agent on each workstation, by the mesh's own sessions, and by workers.
**What the predecessor built, read from its code the same day.** Two modules, split after an incident.
A *manager* on exactly one node held every account's full OAuth grant encrypted, rotated each grant
under a per-licence lease on a cadence and an expiry floor, published each rotation over its bus with
the tokens encrypted, collected the vendor's usage figures per licence, and alerted once a day on
repeated failure or on a refresh token within three days of its own expiry. A *consumer* on every node
was the single writer of the agent's credentials file: it applied a published rotation, stripped the
refresh token so a node could never rotate, pulled when stale, refused a stale grant by comparing
expiries within one lineage, and mirrored a local login back to the manager only after checking the
account's identity against the licence's record — because an unchecked mirror had once written one
account's grant into another's row and published it mesh-wide. Three **touchpoints** with fallbacks: the
node's interactive agent; the mesh's own sessions on the node, falling back to the node's licence; a
worker's own account, falling back to the node's, and refusing to spawn when assigned a licence that
could not be served. The split exists because four nodes refreshing one grant destroyed it: an OAuth
refresh rotates the refresh token, and the predecessor's own code records both that a reused token
killed a licence and that a malformed client id was once misdiagnosed as the same fault. **Whether a
refresh token is single-use is not documented by the vendor**; the predecessor treated it as so, and
this record keeps one rotation source for that reason while leaving the fact to be measured.
**What the mesh has.** [ADR 0050](0050-model-access-is-vendor-agnostic.md) put a per-vendor adapter
inside the controller's licences context, with the carve-out that the manager node holds the refresh
token readably; the catalogue has a manager and a consumer module built on it, assigned to nothing. The
controller's licence commands are not seat verbs and cannot be asked for through the console
([to-be 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md)). `model-access` is a vendor-blind
provision ([ADR 0024](0024-model-access-is-a-provision.md)), and the operator's judgement is that the
agent is not a vendor-blind consumer: it is coupled to an Anthropic subscription grant and nothing else,
so a name that hides the vendor misdescribes the coupling
([ADR 0027](0027-a-provision-names-what-the-consumer-is-coupled-to.md)).
**The bus's rule for a secret** ([to-be 32 §10](../03-DESIGN/01-to-be/32-what-a-module-declares.md)): the
bus is not trusted with one; a secret travels sealed to its recipient, on core request/reply, never
through a stream that persists it.
## Considered Options
1. **Keep the lifecycle in the controller** ([ADR 0050](0050-model-access-is-vendor-agnostic.md) as
built), and make the agent module a consumer of `model-access` delivered by the host as a sealed
file. Rejected by the operator: the controller and the host would both carry a part of an
Anthropic-specific mechanism, and the agent's coupling is misnamed.
2. **The manager delivers each short-lived token through the vault**, as a backend-issued secret the
vault provides to each consumer ([ADR 0113](0113-the-vault-makes-every-secret.md)). Rejected: every
hourly rotation becomes a vault delivery, a composition and a push to every node, and the host
ends up writing a vendor's credential as a file — the module-agnostic host, carrying a vendor's
traffic.
3. **A seat-holding manager module that talks to the agent module on every node over the bus.**
Chosen.
## Decision
**The Anthropic licence manager is a module, `claude-licence-manager`, holding the mesh-scoped seat
`anthropic-licence-manager`.** The seat's contract is the licence verbs: list the licences and their
health, list the bindings, bind or switch a consumer, release one, refresh now, read usage, adopt a
grant, register a node's key, answer a consumer's current token. One holder, on a node the operator
assigns, is what makes rotation happen once ([ADR 0126](0126-a-module-declares-its-own-seats.md),
[ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)). The seat is named for the vendor,
because what it manages is one vendor's grants and nothing else is coupled to it. The vendor-blind
`model-access` provision stands for the consumers that do not care which vendor answers; the agent is
not among them.
**The manager owns the licences.** The records, the grants, the bindings per touchpoint, the usage
readings and the audit of every switch live in the manager's own store, not in the controller's
licences context, which keeps only what it already serves to vendor-blind consumers. The manager is the
one rotation source: it alone calls the vendor's token endpoint, under a lease per licence, on an expiry
floor and a cadence it declares as a setting.
**The long-lived grants are encrypted at rest with a key the vault made for the manager.** The vault
keeps custody of that one key as the manager's own secret ([ADR 0113](0113-the-vault-makes-every-secret.md));
the grants themselves — a refresh token per subscription account, the API key — are the manager's
rows, readable only by it. This is [ADR 0050](0050-model-access-is-vendor-agnostic.md)'s carve-out,
moved with the manager: *one module, one node, the long-lived grants only.*
**The short-lived tokens travel module to module, sealed, on request/reply.** The agent module on each
node makes a keypair of its own when it first runs — a private key made where it is used, never leaving
([ADR 0113](0113-the-vault-makes-every-secret.md)) — and registers its public half with the seat. The
manager hands a node its token by calling that node's agent module (`<module>.<tool>@<node>`,
[ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)) with the token
sealed to that key, and the module answers *applied* or *refused* and why. An agent module that starts,
or finds its token near expiry, asks the seat for its current token the same way. **A token is never
published as an event**: what the manager emits — rotated, switched, failing, usage read — names the
licence and nothing secret, and the audit logger records it. This is a second channel for a secret
beside the vault's, and it is bounded as 0050's carve-out is: this vendor, tokens that live hours, sealed
to one recipient, request/reply only.
**The agent module alone writes what the agent reads.** For a subscription licence it writes the
agent's credentials file under the operator's home, as the operator, access-token-only, atomically. For
the API-key licence it serves the key through the agent's own key-helper setting, so nothing is written
under the home at all. For the mesh's own sessions and workers on that node, it is the local source of
their token. **The host delivers the module's package and its state directory and knows nothing else**:
no path under the home, no vendor, no file shape.
**A binding is explicit, and a switch is a reaction.** Every consumer — a node's interactive agent, the
mesh's session on a node, a worker — is bound to a licence by the operator through the seat's verb, with
the predecessor's fallbacks: a session inherits its node's licence, a worker inherits its node's, and a
worker assigned a licence that cannot be served is refused rather than lent another. Exhaustion is
observed and warned about once per crossing of a declared threshold; moving a consumer to another
licence is a person's act through the seat's verb, as [ADR 0024](0024-model-access-is-a-provision.md)
says, and the declaration language grows no conditional. An automated policy is not decided here.
**A login is attributed only to the account it belongs to.** When a person logs in on a node, the
agent module reads the account's identity from the agent's own state and offers the grant to the
manager sealed to the manager's key; the manager adopts it only when the identity matches the licence
the node is bound to, and refuses with a notification otherwise.
## Consequences
- One module decides which licence every consumer gets, one module writes what each agent reads, and
neither the controller nor the host carries a word of the vendor.
- **A second sealed channel exists** beside the vault's, bounded as stated. A record that widens it to
another vendor or a longer-lived secret is a new decision, not an application of this one.
- The catalogue's `anthropic-manager` and `anthropic-consumer` modules, built on ADR 0050's placement,
are retired once the manager runs; the controller's licences context stops holding Anthropic licences.
- The console lists the seat's verbs, so a person switches a licence in a sentence, and the controller
gains no `licence` verb.
- **What got harder:** a manager that is down leaves every node on its last token until it expires;
the agent module keeps the last token and says so. And a node whose agent module has not registered
its key cannot be handed a token, which the manager reports by name.
- Every interactive session on a machine shares the node's one agent directory, and so its licence;
twenty sessions share it as one does. A consumer with a licence of its own on the same machine is a
worker running from a home of its own with its own agent directory — the worker touchpoint above, for
when workers exist ([ADR 0003](0003-agents-are-persistent-employees.md)); the predecessor ran its
agents that way.
- **Not decided here:** an automated switch on exhaustion; whether a refresh token is single-use, to be
measured in the lab.
## How it is checked
| Rule | Checked by |
|---|---|
| Only the seat's holder calls the vendor's token endpoint | a catalogue test: no module but the manager names it; the manager's refresh runs under a lease per licence, tested with two concurrent runs |
| A token crosses the bus only sealed, only on request/reply | a bus test: every message the manager publishes as an event carries no token; the hand-over is a request whose payload opens only with the receiving module's key |
| The agent module's private key never leaves the node | the per-key test of ADR 0113, extended to this module's key |
| The host writes nothing under a home and names no vendor | a catalogue test on the agent module's definition: no file resource under a home, no vendor word in anything the host applies |
| A grant is attributed only to a matching identity | a manager test: a grant whose account identity differs from the bound licence's is refused and a notification emitted |
| An unservable binding refuses rather than lends | a manager test: a worker bound to a dead licence is answered with a refusal, never another licence's token |
| A switch through the console changes the token on the node and nothing in the answer is a token | a live check on one workstation |
## References
- [ADR 0024](0024-model-access-is-a-provision.md), [ADR 0050](0050-model-access-is-vendor-agnostic.md) — the licence as a named thing, the carve-out this moves with the manager
- [ADR 0027](0027-a-provision-names-what-the-consumer-is-coupled-to.md) — why the seat is named for the vendor
- [ADR 0113](0113-the-vault-makes-every-secret.md) — the vault's custody of the manager's key, and the exception stated here
- [ADR 0126](0126-a-module-declares-its-own-seats.md), [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md), [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md) — a module's seat, its verbs, a call addressed to one machine
- [to-be 32 §10](../03-DESIGN/01-to-be/32-what-a-module-declares.md) — a secret on the bus
- [to-be 36](../03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md), [to-be 39](../03-DESIGN/01-to-be/39-the-anthropic-licence-manager.md) — the two modules
- the predecessor's `claude-licences` and `claude-code` modules, read 2026-10-02: the lease, the floor, the lineage comparison, the identity guard, the touchpoints
@@ -0,0 +1,75 @@
---
topic: the mesh
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0005-the-node-host.md
---
# 184. A service the mesh asked to run is still running a moment later
## Context
The host already refuses to take a service manager's word for it. Three places in one function read
a unit back after acting on it, each with a comment saying why: *a service manager accepting a
command says the transaction was accepted, not that the unit is running — one that starts and
immediately dies satisfies it.* The intent was right and the implementation did not reach it.
On 2026-10-02 the mesh composed a fail2ban jail whose pattern the daemon refused. The host wrote the
files, restarted the service, read the unit back and reported *restarted*. The unit was `active` at
that instant and `failed` 221 milliseconds later, which the unit's own record states. Both public
machines then kept no bans at all — every jail, not the one at fault — and nothing in the mesh said
so. The fault was found by calling a tool that needed the daemon, not by the mesh noticing.
The read-back races the failure. A service manager returns when it has started the process; a daemon
that reads its configuration, refuses it and exits does so a fraction of a second afterwards. One
look sees `activating` or `active` whatever the process is about to do, and *the host reports success
for a machine that is already wrong* — the one shape of failure this host exists to refuse
([ADR 0005](0005-the-node-host.md)).
A command the module declares — *test the configuration before restarting* — was considered and
rejected. The link carries no actions ([ADR 0005](0005-the-node-host.md)), and a verification
command is a command: a declaration that carried one would be remote execution over the bus,
arriving as root on every machine, which is a far larger door than the fault it closes. The host
does not need one. It already knows what it asked for.
## Decision
**1. A unit the host has just asked to run is read twice**, with a pause between the reads long
enough for a daemon that refuses its configuration to have exited. Not running at the second look is
a failure of that resource, named with the unit and the state it is in — the same failure the single
read was always meant to catch.
**2. It is never a wait for a unit to come up.** A unit still starting reads as running at both
looks and is accepted, exactly as before. What the second look catches is a unit that *was* running
and is not any more. A service asked to be stopped is not waited on at all.
**3. The host tests nothing and runs nothing of a module's.** The second look is the host checking
the state it was told to establish, which is its whole job; the declaration gains no vocabulary, and
no command reaches a machine that did not already come from a built artifact.
## Consequences
- Every apply that starts, restarts or reloads a service spends a moment confirming it. The cost is
bounded by the number of services that changed in that apply, which is usually none.
- A module whose configuration the mesh composes — the packet filter, the intrusion prevention, the
resolver — now fails its apply when the composition is bad, instead of reporting success onto a
dead daemon. `status` names the machine, which is how the operator finds out.
- It does not prevent the bad composition. [ADR 0179](0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md)'s
manifest check is what refuses the one that caused this, at merge time; this record is what makes
the *next* one visible within a minute rather than invisible until something asks the daemon a
question.
## How this is checked
| Rule | Checked by |
|---|---|
| A unit that is running at the first look and dead at the second fails the apply, naming the unit and its state | a host test over a service manager that answers as systemd does |
| A unit still starting is accepted at both looks | a host test |
| A service asked to be stopped is not waited on | a host test |
## References
- [ADR 0005](0005-the-node-host.md), [ADR 0179](0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md)
- [Design 05 — The node host](../03-DESIGN/01-to-be/05-the-node-host.md)
@@ -0,0 +1,73 @@
---
topic: the mesh
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md
---
# 185. A control plane behind its seat's row serves what it can
## Context
The mesh's own verbs are the controller seat's tools, and the seat's row is the store's
([ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)). A control plane reads the
row at start and installs a handler per verb; a verb the row carries that the binary cannot run was
refused at start rather than at the first call, so that a disagreement between the row and the
binary was said early. The refusal aborted the start.
On 2026-10-02 a merge added one verb. The new control plane started, widened the row, and ran. A
push a few seconds later recreated its container at the previous image — a stale declaration from
an overlapping wave, [issue 201](../04-ISSUES/201-a-push-recreated-the-controller-behind-the-row-its-successor-wrote/00-report.md) —
and the older binary read a row naming a word it had never heard. It refused to start, and kept
refusing. The mesh had no voice for ten minutes: no verb answered, no node could be pushed, no build
was dispatched, and `status` said nothing because `status` is one of the verbs that had stopped
being served. The way back was a person running the binary by hand outside its service, because the
push that would have replaced it is itself a verb of the control plane that was down.
The check was right about the fact and wrong about the cost. A row ahead of a binary is the ordinary
state of a roll-out: the row is widened by whichever control plane starts first, and a mesh with one
control plane sees that gap on every merge that adds a verb. Making it fatal turned a transient into
an outage with no path out that did not need a human.
## Decision
**1. A control plane serves the verbs it can run and does not refuse to start for the ones it
cannot.** The row remains the authority on what the seat serves; this is only about what this binary
does when it is behind the row.
**2. A verb it cannot run answers the reason.** Not silence and not a missing subject: a caller gets
a sentence naming the verb, saying this control plane cannot run it and that it is a verb of a newer
build. A verb that is simply absent from the row is still not served at all — that is the row
deciding, which is unchanged.
**3. It says so once at start**, naming every verb of the row it cannot run, so the gap is visible
in the log of the thing that has it rather than only at the moment somebody calls one.
**4. A mesh with no controller seat at all is still a refusal.** That is not a version gap, it is a
mesh that has not been seeded, and nothing this control plane does would be meaningful.
## Consequences
- An overlapping roll-out costs the verbs the newer build added, for as long as the older binary is
in place. Everything else — every push, every build, every read — keeps working, and the ordinary
machinery that notices a machine is behind is what puts the newer binary back.
- The log gains one line on a control plane that is behind, and nothing on one that is not.
- Issue 201's other half remains: the push that sent a stale declaration is a race worth closing on
its own terms. This record makes that race survivable rather than fatal, which is the difference
between a transient and an outage, and is deliberately the cheaper half.
## How this is checked
| Rule | Checked by |
|---|---|
| A row carrying a verb this build cannot run still serves every verb it can, and names the one it cannot | a controller test over a widened row |
| The unknown verb answers a sentence naming itself and saying this build is behind | the same test |
| A mesh with no controller seat is refused | the existing start-up path |
## References
- [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md), [ADR 0162](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md)
- [Issue 201](../04-ISSUES/201-a-push-recreated-the-controller-behind-the-row-its-successor-wrote/00-report.md)
- [Design 33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md)
@@ -0,0 +1,81 @@
---
topic: the mesh
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md
---
# 186. A ban list never holds a neighbour, and the mesh's own bans are its own wherever they hang
## Context
[ADR 0179](0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md) gave the
public proxy a jail. Within the hour the home server's ban list held `192.168.1.1` — the house's own
router. The router reflects local traffic, so every client in the building reaches that machine as
the gateway's address; one local request for a name the mesh does not serve, three times in a day,
and the whole house is refused by the machine it was asking. The jails inherited an `ignoreip` of
the loopback and the mesh's own range, which was right when the only jail read the ssh daemon and
the only clients were the mesh's; a jail on a public front door sees the neighbours too.
The same jail broke the other half of [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md).
The home server began reading *NOT the mesh alone: 1 rule set the mesh did not write refuses traffic
here*, and the rule set named was the mesh's own ban chain, written by the mesh's own intrusion
prevention minutes earlier. The host's reader of the legacy filter required every path into a chain
of refusals to come from a built-in chain whose policy accepts, before it would call that chain a
ban. On that machine the chain hangs off the container runtime's user chain as well as the input
chain, and the runtime had set the forward policy to DROP — so the mesh reported its own work as a
foreigner's, on the one machine where the group's exit condition was supposed to hold.
Both faults are one mistake in two places: a rule written about the public internet, applied to
everything that arrives.
## Decision
**1. A ban list never holds a neighbour.** The jails the mesh composes never ban a source on a
private range — the mesh's own range, which was already named rather than written
([ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md)), and every address space
reserved for private use beside it, in both families. A machine behind a router that reflects local
traffic sees its whole building as one address; a ban there is a self-inflicted outage, and the
sources worth banning are not on those ranges in the first place.
**2. The mesh's own bans are its own wherever they hang.** A chain of refusals is a ban list when
every refusal names the sources it refuses and the chain accepts nothing — the rule the host already
applied to the packet filter's own tables, now applied to the legacy filter too, and nothing more.
The policy of the chains that jump into it says nothing about what it is: that policy is already
classified where it belongs, as the container runtime's, and requiring it here counted it twice.
**3. A chain that accepts anything is still not a ban.** That is what keeps a predecessor's
allow-these-and-drop-the-rest chain classified as something an operator must look at, which is the
distinction [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md) exists to draw.
## Consequences
- The composed jails gain the private ranges in their never-ban list. An address already banned
stays banned until it is released; the house's router was released by hand the moment it was found.
- The home server reads *the mesh alone* again, which is group 7's exit condition and was false for
about an hour.
- A machine whose apply fails for an unrelated reason does not revisit its found firewall's record
at all — the step runs only after a clean apply ([ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md)).
The home server's record therefore still reads *retired by the mesh* although the front end is
uninstalled, and will correct itself once that machine's own stuck module is fixed. It is a stale
record, not a wrong machine.
- The record number the front end's removal was given moved under it: another session took 0175
while that record was in review, and it is now
[ADR 0180](0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md). The citations
the host and the control plane print were pointing at an unrelated record and are corrected here.
## How this is checked
| Rule | Checked by |
|---|---|
| A private source is never banned | the module's jail configuration, read back by `fail2ban.fail2ban_settings` on a machine |
| The mesh's own ban chain reads as a ban behind a dropping forward policy | a host test over the home server's own captured rule set |
| A chain that accepts anything is not a ban | a host test |
| Live | done 2026-10-02: all four machines read *the mesh alone*, the home server counting its own ban chain as a ban; no ban held anywhere is a private address |
## References
- [ADR 0179](0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md), [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0180](0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md)
- [Design 08 — Connectivity](../03-DESIGN/01-to-be/08-connectivity.md), [Design 31 — A module declares its fail2ban jail](../03-DESIGN/01-to-be/31-a-module-declares-its-fail2ban-jail.md)
@@ -0,0 +1,73 @@
---
topic: the mesh
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0136-a-step-gates-its-module-not-the-machine.md
---
# 187. A dead tracker is not the machine's failure
## Context
The home server had not applied a declaration cleanly since midday. One run-once step — the one
that writes a media app's download clients and indexers through the app's own API — exited
non-zero, forty-nine times over six hours, for one public tracker that had stopped answering. The
step's own words: the entry was *written*, and the app's test of it then failed with a 400 from the
indexer proxy. The machine reported *not doing what it was told* for the rest of the day.
What that gated matters more than the step. A converged machine retires the firewall it was found
with only after a clean apply ([ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md)),
so that machine went on recording its found front end as merely *retired* long after the package
had been uninstalled ([ADR 0180](0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md)).
A dead public tracker was holding a firewall record hostage, which is not a connection anybody
would design.
The step already knew this was not its business. It had a rule for exactly this: an entry the mesh
only *found and re-pointed*, rather than one it was told to make, whose feed is gone, is said and
left as found — *failing the node's apply on every heartbeat for it reports the mesh as wrong about
a tracker*. The rule was there and matched one shape of the fault. An app can refuse to save such an
entry, and it can save it and then fail its own test; saving validates settings, and the test runs a
live search. The rule caught the first and let the second through.
## Decision
**1. An entry the mesh only found is never the machine's failure.** Whatever shape the app's
refusal takes — it would not save it, or it saved it and its own test fails — an indexer the mesh
found and re-pointed is reported as a notice and left as found. What decides is whose entry it is,
not which sentence the app returned.
**2. What the mesh is answerable for is the plumbing.** That the entry exists, points at this
mesh's indexer proxy, and carries the credential the mesh delivered — which was checked against the
proxy before anything was written. Whether a public tracker answers today is not the mesh's to
promise, and a machine that reports itself broken because one did is lying about itself.
**3. An entry the operator listed is theirs to insist on.** An indexer named in the step's settings
is one the mesh was told to make, and it still fails the step when it cannot be made to work. The
notice says so, and says that listing the indexer is how to turn it back into a failure.
## Consequences
- The home server applies cleanly again, and everything a clean apply gates — its found firewall's
record among it — follows.
- A tracker that dies is a line in a report rather than a machine that reads as broken. An operator
who wants it gone removes the entry or repairs the feed; the mesh says which, every time it runs.
- The four Servarr modules carry one byte-identical copy of this step each
([ADR 0069](0069-a-module-is-a-repository-and-a-path.md)), so the change lands in four places and
a test refuses any drift between them.
- It does not widen to a download client: one the mesh was told to write and cannot is still a
failure, because the mesh chose it and nothing else will fix it.
## How this is checked
| Rule | Checked by |
|---|---|
| A found feed whose tracker answers an error after the entry was written is a notice | the step's tests, with the home server's own message and the app's two validations modelled apart |
| An indexer the settings list is still a failure | the same test |
| The four copies of the step do not drift | the step's own sameness test |
| Live | done 2026-10-02: the home server applies cleanly after six hours of failing, `status` holds no machine wrong or behind, and its found firewall reads *removed* |
## References
- [ADR 0136](0136-a-step-gates-its-module-not-the-machine.md), [ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), [ADR 0180](0180-the-found-front-end-is-uninstalled-once-a-machine-is-converged.md), [ADR 0069](0069-a-module-is-a-repository-and-a-path.md)
@@ -0,0 +1,130 @@
---
topic: the mesh
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md
---
# 188. A provider declares what it derives for each consumer, and the mesh tells both ends
## Context
An arrangement between a consumer and a provider is delivered entirely by the mesh. Where the
provider is, which port it answers on, what name the consumer must present, where its password
is — each arrives as a fact the consumer reads from its binding, or as `${bound:…}` filled into a
file before the declaration leaves the control plane. The provider invents none of it and hands
none of it back ([ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)).
One kind of value escapes that. Where the **provider names the resource** — a bucket, a database,
a vhost — the name is derived from the consumer, per consumer, and the mesh has no way to carry
it. `serves` is a literal block in the provider's definition: the same values for every consumer.
A provisioner's contract takes a provision and returns nothing. So a value the mesh's own rule
produced reaches neither end as a statement; it is recomputed at one end and transcribed at the
other.
The object store is the instance ([issue 124](../04-ISSUES/124-a-consumer-cannot-be-told-what-its-provider-derived/00-report.md)).
Its provisioner normalises the login the mesh minted into a bucket name and creates, checks and
removes exactly that; the rule lives in twenty lines of the module's own TypeScript. Its three
consumers each write the answer into their own definition by hand. Two transcribed it correctly;
one named a predecessor's bucket, and would have authenticated successfully and been refused on
every object, which reads like a credential fault and is not one.
Even corrected, the transcriptions are wrong in a second way. Each is `mesh-<node>-<slug>`, so
each **names the machine the module happens to run on today** — a definition stating a fact about
one installation, which [ADR 0155](0155-a-definition-names-no-installation-and-how-that-is-checked.md)
forbids and whose check does not catch because the name is not a domain. Move any of the three to
another machine and its configuration points at a bucket its key cannot open.
The shape is not the object store's. A database provisioner that prefixed names, a queue provider
that scoped vhosts, any provider that derives a resource from who is asking: each forces the
consumer to reproduce somebody else's rule and keep it in agreement by hand.
## Decision
**1. A served value may name the consumer the mesh is serving.** A `serves` block, which is
literal today, may interpolate the mesh's own statement of who the consumer is:
- `${consumer:as}` — the identity the mesh minted for this consumer, exactly as the login it is
told to present ([ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md));
- `${consumer:as:dns}` — the same identity written as a DNS label.
Nothing else. **The mesh learns no protocol here; it spells its own name in an alphabet it already
knows.** The identity is the mesh's, minted by the mesh, already capped at twenty characters
because of what an S3 access key accepts; `dns` is that same name with its separator written `-`
instead of `_`, which is the whole of the difference between the mesh's identifier alphabet and
the one buckets, vhosts and hostnames use. A provider that needs a prefix or a suffix writes it
around the placeholder, because a served value is a string.
The rejected alternative is **the provider returning values from provisioning** — the natural
channel, since the provider is what derived them. It is rejected for three reasons, in order of
weight. It inverts the delivery the mesh is built on: a grant would carry data the provider wrote
rather than only data the mesh minted, and [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)
removed exactly that second path once already. It makes a consumer's declaration incomplete until
its provider's reconcile loop has run, so a consumer could not be composed before a provider
answered — a bootstrap order the mesh does not have and does not want. And it puts the rule where
nothing can check it: a value that arrives from a running process cannot be refused at resolution,
only discovered wrong later, which is the failure this record exists to end.
**2. The mesh resolves it once, per consumer, and tells both ends from the one resolution.** At the
moment a consumer's declaration is composed, the mesh knows exactly who the consumer is. There, and
only there, the placeholders are filled. The result reaches:
- the **consumer**, as the served facts in its binding file and as `${bound:<provision>:<key>}` in
any file it writes — unchanged mechanisms, carrying one more key;
- the **provider**, as `serves` on that consumer's entry in its contributions file, so the
provisioner is *told* the name rather than recomputing it.
**The provider stops deriving in code and starts declaring.** One statement, filled once, delivered
to both ends: the two cannot disagree, because there is no second computation to disagree with.
**3. A served value stays settled before it is per-consumer.** Settings still compose into `serves`
([ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)), and the consumer
placeholders are filled after that, so an operator may set a prefix and the mesh still derives the
rest. A `${consumer:…}` naming a fact or an alphabet the mesh does not have is refused when the
definition is parsed, with what it may say.
**4. A consumer may no longer name the resource its provider derives.** With the value delivered,
a literal in a consumer's definition is not merely redundant — it is the one thing that can
disagree with what the provider will actually create. The three object-store consumers lose their
hand-written bucket names in this change.
## Consequences
- One more thing a definition may say, and one less thing a module may be wrong about. The
vocabulary grows by a placeholder; the catalogue loses three literals that named this
installation's control node.
- A provider's naming rule becomes readable in its definition instead of in its source. `minio`'s
`bucketFor` goes; the manifest says `"bucket": "${consumer:as:dns}"` and the provisioner uses
what it is given.
- A provider that already serves consumers keeps serving them: the derived value equals what the
code derived, so no bucket, database or login changes name. This is a change of **who says it**,
not of **what is said**.
- The mesh now holds a rule in another system's alphabet — one rule, `dns`, stated once. A second
alphabet is a decision, not an addition: the cost of each is that the mesh must be right about
somebody else's naming, and that cost is only worth paying where the mesh already mints the name.
## How this is checked
- A served value naming an unknown fact or alphabet is refused at parse, with the list of what it
may say — tested on both halves of the message.
- Resolving a consumer whose provider derives a value puts that value in the consumer's binding
file, in its `${bound:…}` substitutions, and in the provider's contributions entry for that
consumer — one test asserting the three agree, because agreeing is the whole point.
- Two consumers of one provider on one machine get two different derived values, and neither gets
the other's.
- A catalogue-wide test refuses a consumer definition that writes a literal where its provider
derives: the provider's `serves` names the key, so the catalogue can say which definitions
transcribe one.
- `dns` is checked against the identity the mesh actually mints, not against an invented string:
the test derives an identity with `ConsumerIdentity` and asserts the label it becomes.
## References
- [issue 124 — a consumer cannot be told a value its provider derived for it](../04-ISSUES/124-a-consumer-cannot-be-told-what-its-provider-derived/00-report.md)
- [ADR 0048 — a provider creates the credential the mesh minted, and seals nothing](0048-a-provider-creates-the-credential-the-mesh-minted.md)
- [ADR 0049 — a consumer's identity fits the tightest backend](0049-a-consumers-identity-fits-the-tightest-backend.md)
- [ADR 0174 — a node varies a module through settings and kept regions, never through an edit](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)
- [ADR 0155 — a definition names no installation, and how that is checked](0155-a-definition-names-no-installation-and-how-that-is-checked.md)
- [design 27 — a module requires, the mesh resolves](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md)
@@ -0,0 +1,131 @@
---
topic: the mesh
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md
---
# 189. The store keeps what the records name, and a maintenance step holds its writers still
## Context
The mesh's artifact store has never collected anything
([issue 108](../04-ISSUES/108-the-registry-has-no-garbage-collection-once-it-has-two-doors/00-report.md)).
Every build pushes another layer set; nothing has ever removed one. The predecessor ran a routine
on a timer — stop the registry, collect, start it — and the conversion carried the settings that
routine depends on without the routine, because the routine was a script beside the module and not
a resource in it. The store now holds fifty-three repositories on the machine that serves
everything else, and the only outcome of leaving it is a full disk reported as somebody else's
failure.
Three things stood in the way, and the issue names all three.
**Nothing in the mesh's vocabulary expresses a maintenance window.** The collector requires every
writer stopped while it runs. A `run-once` step runs *beside* containers, not instead of them, and
a scheduled step is the same container on a cadence. There is no way for a module to say *hold this
container of mine still while this runs*.
**Deletion is not enabled, and the door it would be enabled on has no accounts.** The store is
internal, reached by name over the overlay, trusted because being on that network is the permission
([ADR 0082](0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md)). The predecessor
kept deletion behind an authenticated door, which it could, having one.
**Nothing says what may be removed.** The registry's own answer — collect everything no tag names —
is wrong here. The mesh pushes each artifact under one moving tag and pins machines by digest, so
every build but the newest is untagged and some machine may still be running it.
## Decision
**1. Deletion is enabled on the store's one door, and the overlay stays the permission.** The
objection dissolves on inspection: that door **already accepts a push**, and a writer who can push
can replace any tag in the store with anything it likes. Delete takes nothing a push did not
already have, and the machines that can reach the door are the ones the mesh's own filter admits
([ADR 0168](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md)). Putting an authenticated
door in front of deletion while leaving push open would be a lock on the window beside an open
door, and it would cost the thing ADR 0082 bought: a store every machine can reach without a
credential to distribute first.
**2. The mesh deletes what it made and no longer keeps; the store reclaims the bytes.** Two halves,
each doing what only it can.
The **mesh** decides. It does not need to enumerate the store to do it — it has never put anything
there it did not record, so **every digest it could remove is already in its own build records**.
It deletes those manifests through the store's door, by digest, and remembers that it did.
The **store** reclaims. A deleted manifest frees no bytes until the registry's own collector walks
the storage with nothing writing to it, so the module declares that collector as a scheduled step
with the server held still for its duration. Plain collection, not `--delete-untagged`: what the
mesh keeps is still a manifest in the store, so it is still referenced, so its blobs stay — the
dangerous flag is not needed at all once the mesh is the one deciding.
**3. What the mesh keeps, stated as three reasons rather than a number.** A digest is kept because:
- **a definition names it** — every artifact reference in any module's current recorded manifest,
which is what the mesh would hand a machine now. No age limit: this is the floor;
- **the mesh can still go back to it** — every artifact of the **five most recent successful
builds** of each module, so a release that turns out wrong has somewhere to return to;
- **nothing else.** An artifact older than that, which no definition names, is what the store is
carrying for no stated reason.
A digest the mesh did not record making is never touched. That is not a safety margin, it is the
whole rule restated: the mesh removes what it put there and can account for, and the images genesis
pushed before any record existed are exactly what this must not reach
([04-ISSUES/102](../04-ISSUES/102-an-address-recorded-at-genesis-or-build-does-not-follow-the-nodes-ports/00-report.md), F4).
**4. A scheduled step may hold its module's own containers still while it runs** —
`while-stopped`, naming resource ids in the same module. The host stops each, runs the step, and
starts them again **whatever the step did**, including when it failed or the host was interrupted.
Three boundaries:
- **Its own module's containers only.** A module that could quiesce a neighbour could stop the
mesh; a maintenance window is a statement about one service's own insides.
- **Scheduled steps only, not `run-once`.** At apply time the host already has a window: the
declaration is applied in order and a step gates what follows, so a one-time offline migration
says *before* rather than *instead of*. A recurring window is the case order cannot express.
- **Restoring is not conditional.** A step that fails must leave the service running; the whole
risk of this field is a window that never closes.
**5. The sweep runs where the records change — after a build the mesh recorded.** That is the
moment new bytes landed and the moment the keep set moved, and it needs no new timer. The
store's collection runs nightly, because reclaiming is slow and the thing it reclaims is already
unreferenced.
## Consequences
- Disk stops growing without bound on the machine that serves the mesh. That is the whole point
and it has no other way to be true.
- A machine behind by more than five builds of a module, which recreates a container, cannot pull
what it was running. It is already a machine the mesh reports as behind, and the answer is the
one the mesh already gives it: the current declaration. Stated here rather than discovered.
- The store is a little less of a museum. A digest in an old build record may no longer be
fetchable, and the record still says what that build made — the record is history, not an
index of what is on disk. The collected mark is kept beside it so the two can be told apart.
- `while-stopped` is a second thing the host does to a container it did not start this pass. It is
deliberately the narrowest form: the module's own, by id, restored unconditionally.
- The store is briefly unavailable each night, for as long as collection takes. Everything that
pulls from it retries; nothing in the mesh treats a momentary store as a failure
([ADR 0185](0185-a-control-plane-behind-its-seats-row-serves-what-it-can.md)).
## How this is checked
- The host: a scheduled step with `while-stopped` stops the named containers before the run and
starts them after; it starts them again **when the step fails**; it refuses an id that is not a
container of the same module, its own id, and `while-stopped` on a `run-once` step. Each refusal
is tested for what it says, not only that it says something.
- The controller: given build records and current manifests, the keep set holds every reference a
manifest names and every reference of the five most recent builds per module, and nothing else;
a reference the mesh never recorded is never in the delete set; a delete that answers 404 is
recorded as collected rather than retried forever.
- The sweep is tested against a fake store that records what it was asked to delete, so what is
asserted is the decision and not the registry's behaviour.
- Live: the store's size before and after the first nightly collection, read from the machine.
## References
- [issue 108 — the registry has no garbage collection](../04-ISSUES/108-the-registry-has-no-garbage-collection-once-it-has-two-doors/00-report.md)
- [ADR 0082 — the registry is reached by name and trusted by the overlay](0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md)
- [ADR 0053 — a step that runs on a schedule](0053-a-step-that-runs-on-a-schedule.md)
- [ADR 0156 — an artifact is what a build produces, and the store is named for its scope](0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)
- [design 32 — what a module declares](../03-DESIGN/01-to-be/32-what-a-module-declares.md)
+34
View File
@@ -168,6 +168,28 @@ python3 00-META/checks/index.py fail if stale
- **0132** — [A seat carries the tools its holder must serve](0132-a-seat-carries-the-tools-its-holder-must-serve.md)
- **0134** — [The mesh says what it applied](0134-the-mesh-says-what-it-applied.md)
- **0142** — [The mesh delivers its own components as binaries, not as container images](0142-the-mesh-delivers-its-own-components-as-binaries.md)
- **0154** — [The mesh's own verbs are the mesh-controller seat's tools, and which verbs those are](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)
- **0156** — [An artifact is what a build produces, the artifact store serves every kind, and its seat is named for its scope](0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)
- **0157** — [A build says what it does on the bus, as it happens](0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md)
- **0158** — [A provider with one credential shares it with every consumer, and the vault remakes it for all of them at once](0158-a-provider-with-one-credential-shares-it-with-every-consumer.md)
- **0159** — [A tool call names the machine it is for, every answer says which machine answered, and a holder's runtime serves its seat's verbs](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)
- **0160** — [The mesh issues an assignment's subjects, and a runtime serves what it is issued](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
- **0161** — [What deserves a seat: a role of a module is a seat, a singular fact about machines is a placement with a capacity of one, and a holder's software is the machine's](0161-what-deserves-a-seat.md)
- **0162** — [A merge produces a tiered plan the mesh keeps, and a module's dependencies are one relation in the catalogue](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md)
- **0163** — [Taking a module over is a comparison: what it compares, what it refuses, and what it carries](0163-taking-a-module-over-is-a-comparison.md)
- **0167** — [A membership carries what its module receives, and who the mesh is](0167-a-membership-carries-what-its-module-receives-and-who-the-mesh-is.md)
- **0168** — [A converged machine is filtered by the mesh alone, and the host says what else refuses](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md)
- **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)
- **0179** — [The intrusion seat serves its verbs, a container may log to the journal, and every door declares its jail](0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.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)
- **0184** — [A service the mesh asked to run is still running a moment later](0184-a-service-the-mesh-asked-to-run-is-still-running-a-moment-later.md)
- **0185** — [A control plane behind its seat's row serves what it can](0185-a-control-plane-behind-its-seats-row-serves-what-it-can.md)
- **0186** — [A ban list never holds a neighbour, and the mesh's own bans are its own wherever they hang](0186-a-ban-list-never-holds-a-neighbour.md)
- **0187** — [A dead tracker is not the machine's failure](0187-a-dead-tracker-is-not-the-machines-failure.md)
- **0188** — [A provider declares what it derives for each consumer, and the mesh tells both ends](0188-a-provider-declares-what-it-derives-for-each-consumer.md)
- **0189** — [The store keeps what the records name, and a maintenance step holds its writers still](0189-the-store-keeps-what-the-records-name.md)
### Its tiers, from the bottom up
@@ -200,6 +222,7 @@ python3 00-META/checks/index.py fail if stale
- **0109** — [A package registry seat is one per ecosystem, not one for all of them](0109-a-package-registry-seat-is-one-per-ecosystem.md)
- **0126** — [A module declares its own seats; the mesh reserves its own](0126-a-module-declares-its-own-seats.md)
- **0148** — [The mesh's names are resolved, not copied into every container](0148-the-meshs-names-are-resolved-not-copied-into-containers.md)
- **0151** — [A route's internal name is composed under the node that serves it](0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md)
### What runs on them, and how it gets there
@@ -255,6 +278,15 @@ python3 00-META/checks/index.py fail if stale
- **0146** — [Connectivity is checked by name, per hosting form, with a valid certificate](0146-connectivity-is-checked-by-name-per-hosting-form.md)
- **0147** — [A module anchors the mesh's authority on a machine, and takes it away again](0147-a-module-anchors-the-meshs-authority.md)
- **0150** — [A module's own code runs as supervised processes under the module's one account](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md)
- **0152** — [The operator's surface is a module the mesh assigns: the console](0152-the-operators-surface-is-a-module-the-console.md)
- **0155** — [A definition names no installation: how that is checked, and the three ways a value that did gets out](0155-a-definition-names-no-installation-and-how-that-is-checked.md)
- **0173** — [The operator's machine is the mesh's, and a module is whatever it declares](0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md)
- **0175** — [One tool runtime per node serves every module's tools, on the host side](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)
- **0176** — [The login shell is a node seat held by one shell module, and `execute` is its contract](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)
- **0177** — [A unit may be user-scoped, and the service manager is a node seat whose holder answers for the units](0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md)
- **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)
### How it is built
@@ -276,6 +308,7 @@ python3 00-META/checks/index.py fail if stale
- **0107** — [Persistent data is a directory bind, never a named volume](0107-persistent-data-is-a-directory-bind-never-a-named-volume.md)
- **0111** — [A build source is on the mesh's git seat, or it is an external repository](0111-a-build-source-is-on-the-git-seat-or-external.md)
- **0149** — [The live mesh is the test bed](0149-the-live-mesh-is-the-test-bed.md)
- **0174** — [A node varies a module through settings and kept regions, never through an edit](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)
### How it is checked
@@ -296,5 +329,6 @@ python3 00-META/checks/index.py fail if stale
- **0034** — [The local account owns the mesh, and a web application's login is not that](0034-the-local-account-owns-the-mesh.md)
- **0080** — [The development cycle is checked, not trusted](0080-the-development-cycle-is-checked.md)
- **0081** — [A decision nothing cites is not yet in the chain](0081-a-decision-nothing-cites-is-not-yet-in-the-chain.md)
- **0153** — [The record is read by a module the mesh assigns, and the console lists it](0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)
<!-- index:end -->
+40 -60
View File
@@ -1,78 +1,58 @@
---
layer: as-is
status: implemented
code: [hal]
updated: 2026-08-23
decisions: []
code: [mesh-catalog modules/records, mesh-catalog modules/mesh-console]
updated: 2026-09-30
decisions:
- 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
- 02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
---
# Knowledge
The mesh keeps two knowledge stores. They are not redundant, and knowing which is which is the
difference between finding an answer in one search and rediscovering it over several hours.
**The mesh keeps no knowledge store.** What it knows is what its modules answer, and the way a person
or an agent asks is the console's tool list on the machine they sit at
([13 — The console](13-the-console.md)). This document used to describe two stores; it is rewritten
because neither exists from the mesh's side, and an as-is document that describes what is gone is a
brochure.
## The operational memory
## What was here, and where it went
A store of operational notes, written and read by whoever — human or agent — is working. Each
note is a slug and a body: how something works, what went wrong, what the fix was, what
assumption turned out to be false.
Until the cut-over of 2026-09-28 the predecessor ran two stores: an operational memory of notes
indexed on symptoms, and a structured archive of governed documents with a librarian approving
promotion. Both were reached through the predecessor's tool server over the bus the mesh removed
([issue 147](../../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md)).
Nothing in the mesh reaches them now, and nothing in the mesh has replaced them: there is no note
store, no archive, no librarian, and the lessons of the last days were written into this repository by
hand. That is a gap, and it is stated here rather than papered over. What replaces a symptom-indexed
memory, if anything does, is undecided.
It is indexed on **symptoms**. The entry someone needs is usually titled after the error they
are staring at, which is why the standing instruction is to search the literal error text
before forming a hypothesis rather than after one fails.
## The record
Its content is overwhelmingly the record of previous debugging: a large body of
troubleshooting entries, module conventions, and standing notes about work that is open. It is
the mesh's institutional memory of *what has already gone wrong*.
**The design record is read where it is written.** Since 2026-09-30 a module, `records`, keeps a
checkout of this repository from the forge — cloned from the `git` seat, reset to the origin on every
merge the forge announces and every ten minutes — and answers over the bus: where a phrase appears as
written, one document whole, what a folder holds, and where the checkout stands, each naming the
commit it read. Which repository it reads is a setting on its assignment; the module names no mesh.
The cost of skipping it is documented in the mesh's own record: entries have been rediscovered
from scratch, over hours, in sessions where the search was skipped because the trail felt
confident. It fires hardest on familiar ground, not unfamiliar ground.
It is listed by the console beside every other tool, with a description that says to search the
literal words of a symptom before forming a hypothesis. That is what
[ADR 0025](../../02-DECISIONS/0025-the-design-record-is-read-not-copied.md) meant by *beside
everything else*, in a mesh with no store to be beside
([ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)).
## The structured archive
Nothing is copied. A checkout lags the source by seconds after an announced merge and by minutes
otherwise, and says so.
A second store, structured rather than flat: spaces, pages, revisions, tiers, and full-text
search. Where the operational memory is a note, this is a document with an owner and a
lifecycle.
## The constitution
Content is promoted through tiers — private, then team, then platform — with a librarian agent
owning approval and promotion at the boundary. Proposals to edit are reviewed rather than
applied.
This is where the mesh's **governed** documents live, including the constitution injected into
design sessions ([ADR 0020](../../02-DECISIONS/0020-the-mesh-is-governed-by-a-constitution.md)).
## Why both
The distinction is by lifecycle, not by subject.
| Operational memory | Structured archive |
|---|---|
| Written the moment something is learned | Written deliberately, reviewed |
| Flat, symptom-indexed | Structured, tiered, owned |
| Anyone writes; nothing approves | Promotion is approved |
| Truth is "this happened" | Truth is "this is agreed" |
Collapsing them would cost one of the two properties: either every hard-won note waits for
review, or governed documents can be changed by anyone mid-incident.
[`00-META/how-we-build.md`](../../00-META/how-we-build.md) is the source of the mesh constitution
([ADR 0021](../../02-DECISIONS/0021-hq-is-the-source-of-the-constitution.md)). The page it used to be
synchronised into lived in the predecessor's archive and is unreachable; the constitution today is
read from this repository, through the same module, and playbook 05's sync has nothing to write to.
## Where this repository sits
This repository is a third thing, and the objection was raised when it was created: a fourth
knowledge system repeats the mistake the split was made to fix.
The answer given was **indexing, not location** — that these documents are indexed into the
knowledge base so that a symptom search returns them alongside everything else. One source,
many surfaces.
**That indexing does not currently exist.** A search for this repository's content returns
nothing. The claim is load-bearing for the decision to separate the repository at all, and
until it is true, this repository is exactly the fourth knowledge system the objection
described. Recorded here because it is a statement about how the mesh's knowledge actually
works today, and as [`04-ISSUES/006`](../../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md).
## The librarian
A single agent owns the archive's approvals and promotions. Its approval capabilities have at
times not been reachable as tools, which does not affect the operational memory but does mean
promotion stops silently — the store keeps accepting proposals that nothing can approve.
A third thing beside two that are gone, which makes it the first: the one governed record the mesh
has, public, read by a module the mesh assigns, and edited nowhere else.
+16
View File
@@ -82,6 +82,22 @@ to clone.
The schema column added for this defaults to empty rather than null, because "not on a seat" is a
real answer, so every row recorded before the change keeps exactly the meaning it had.
## A seat's protocol is on its row, and the controller serves its own
*Since 2026-09-30 ([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)).*
The `seat` table carries `accepts`, `emits` and `serves`; `serves` holds each verb with its description
and input schema. The rows were seeded from the compiled defaults the first time a controller with the
columns migrated, and each later migration adds any verb the defaults name that a row lacks, never
removing one. `UseSeats` still falls back to the compiled protocol for a row with none, which after the
first seeding is no row.
The `mesh-controller` seat serves twelve verbs — `tools`, `status`, `nodes`, `node`, `modules`, `seats`,
`builds`, `plan`, `assign`, `unassign`, `push`, `build` — on `mesh.seat.mesh-controller.tool.<verb>`,
each answered by the controller running that command in its own binary and returning what it printed.
A module claiming a mesh seat with verbs must list them under `tools` or registration refuses it by
name. A node-scoped seat's tool is `mesh.seat.<seat>.tool.<verb>.<node>`; no node-scoped seat declares
one yet.
## Where this differs from the design
**Capacity is not implemented.** The design's vocabulary has a seat with a capacity, and a
+64
View File
@@ -0,0 +1,64 @@
---
layer: as-is
status: implemented
code: [mesh-catalog modules/mesh-console, mesh-tools src/mesh.ts, mesh-tools src/http.ts, mesh-tools src/runtime.ts, mesh-controller internal/broker, mesh-controller cmd/mesh-controller/check.go, mesh-controller cmd/mesh-controller/seatverbs.go]
updated: 2026-09-30
decisions:
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
- 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
- 02-DECISIONS/0037-where-a-module-lives.md
- 02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md
---
# The console, as it runs
**The mesh's tools reach a person through a module the mesh assigned to their machine.** Since
2026-09-30 a workstation that is a node can be assigned `mesh-console`; the mesh mints a bus account
`<node>.mesh-console`, seals its credential to the machine, and the container binds
`127.0.0.1:<port>` with the port the mesh assigned for the manifest's declared one. An agent on the
machine is pointed at `http://127.0.0.1:<port>/mcp` and sees the mesh's tools; a person uses the same
endpoint. Nothing on the machine holds a credential a person had to carry.
## What it answers
`initialize`, `tools/list`, `tools/call`, over HTTP, one JSON body per request, no session and no event
stream. `tools/list` is what the running modules answered: every tool runtime built on or after that day
serves a `tools` verb for its module, and the console asks the catalogue for the roster and each module
for its tools. A module that did not answer is named in the list's `_meta.notAnswering`. On the day it
shipped that was 36 of 51 modules — those that serve no tools at all, and those whose rebuilt runtime the
mesh records rather than rolls out — and 62 tools from the rest.
`tools/call` reaches any tool by `<module>.<tool>`, listed or not. The console's grant is `*`, so what it
may call is every tool on the mesh; its account may publish nothing else and subscribes nothing.
## The mesh's own verbs
*Since 2026-09-30 evening ([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)).*
The console asks the `mesh-controller` seat's `tools` verb beside the modules and lists every role's
tools as `<seat>.<verb>` — `mesh-controller.status`, `mesh-controller.push` and the other ten. A call
to `<prefix>.<name>` reaches the seat when the prefix is a seat declaring that verb, and the module
otherwise; `seat:<seat>.<verb>` says so outright. When the control plane does not answer, the list
names `mesh-controller (seat)` as not answering and carries the modules' tools regardless. The
`mesh-controller` *module* is always named as not answering: it serves no module tools, only its seat's.
## Around it
- **`invokes`** in a manifest is the grant. It is composed into the bus's user list exactly as a
person's account is; the console is the only module that declares it.
- **`module check <file|dir>…`** on the controller's binary judges a manifest with no mesh: the strict
parse, every per-manifest problem, and the rules between the manifests given. It prints what it cannot
judge without a store rather than refusing. The console's own manifest was the first thing checked
with it, and the whole catalogue passes.
- **The person's client remains.** `operator issue` and `mesh tools|call|mcp` with a credential file
still work, for a machine that is not a node and for a mesh not yet able to assign anything.
`mesh tools --console <url>` goes through a running console with no credential; it is covered by the
runtime repository's tests and was not exercised on the live mesh.
## What shipped bent
- A module registered by hand from the catalogue with `--source <url> --path modules/<m>` records a URL,
not a place on the git seat: `--self` takes the forge path form (`<owner>/<repository>`), which the
operator did not pass. The rebuild-on-merge matched the URL anyway.
- Modules whose upgrade policy is *record* — the forge among them — answered `tools` only once
something pushed their rebuilt runtime; until then they are listed as not answering while still
callable. That is the policy doing what it says, not a fault of the console.
+2 -1
View File
@@ -15,12 +15,13 @@ Where the two disagree, the implementation wins and the disagreement is stated.
| [`04-delivery.md`](04-delivery.md) | Push to running: the three silos, levels, and what a green pipeline proves |
| [`05-runtime-and-installation.md`](05-runtime-and-installation.md) | The node runtime, its modes, and how a node comes into being |
| [`06-configuration-and-secrets.md`](06-configuration-and-secrets.md) | Managed files, value resolution, and where secrets live |
| [`07-knowledge.md`](07-knowledge.md) | The two knowledge stores, and what each is for |
| [`07-knowledge.md`](07-knowledge.md) | The mesh keeps no store: what it knows is what modules answer, and the record is read by one |
| [`08-agents-and-work.md`](08-agents-and-work.md) | Agents as employees, tasks, workflows, and the meeting model |
| [`09-interfaces-and-observability.md`](09-interfaces-and-observability.md) | How the mesh is reached and watched — tools, board, proxy, health, thoughts |
| [`10-module-catalogue.md`](10-module-catalogue.md) | The catalogue's shape, and what its shape says |
| [`11-the-lab.md`](11-the-lab.md) | The lab — the first piece of the new shape that exists, and what it does not yet do |
| [`12-the-seats.md`](12-the-seats.md) | The seats the mesh defines, who holds one, and where a seat changes resolution |
| [`13-the-console.md`](13-the-console.md) | The mesh's tools on the machine a person sits at, served by a module the mesh assigned there |
## What these documents are not
+22 -3
View File
@@ -1,9 +1,10 @@
---
layer: to-be
status: in-progress
code: [mesh-lab]
updated: 2026-09-11
code: [mesh-lab, mesh-catalog modules/lab]
updated: 2026-10-02
decisions:
- 02-DECISIONS/0172-the-lab-is-a-module-and-runs-a-bed-when-the-mesh-asks.md
- 02-DECISIONS/0016-the-lab.md
- 02-DECISIONS/0010-delivery.md
---
@@ -119,7 +120,25 @@ In order, on a machine with nothing:
6. **Verification**, as above, before anything is raised.
## Open
## The lab answers the mesh
*2026-10-02* ([ADR 0172](../../02-DECISIONS/0172-the-lab-is-a-module-and-runs-a-bed-when-the-mesh-asks.md)).
Once installed, the lab is also a module: `lab`, assigned to the machine that passed `check`. Its
tools run there and nowhere else:
| tool | does |
|---|---|
| `lab_check` | the lab's `check`, on this machine |
| `lab_run` | fresh checkouts of the named branches from the forge, side by side, then the suite on the named beds; answers with an id |
| `lab_status` | where a run is, and how it ended: the commits it tested, passed and failed |
| `lab_log` | the run's output so far |
| `lab_stop` | ends a run |
The runtime is a container holding the toolchain the suite builds with. It reaches the
virtualisation daemon and the container runtime through their sockets on the machine, so what it
raises is what a hand run raises. The prerequisites above stay installed by hand. The module uses
them, and never installs them.
- **Whether the lab's bootstrap may install packages at all**, given that the mesh's rules
forbid installing by hand. The resolution is probably that the lab's bootstrap *is* the
+48 -1
View File
@@ -2,8 +2,11 @@
layer: to-be
status: in-progress
code: [mesh-host]
updated: 2026-09-29
updated: 2026-10-02
decisions:
- 02-DECISIONS/0184-a-service-the-mesh-asked-to-run-is-still-running-a-moment-later.md
- 02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md
- 02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md
- 02-DECISIONS/0141-the-host-delivers-its-own-successor.md
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
- 02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md
@@ -153,6 +156,36 @@ checked:* unit tests hold the host to keeping a found file and container, conver
taken, never removing a held file and reporting one that changed; the adoption bed asserts a found
file byte for byte unchanged until its module is taken.
**What the host says of a found container, and what it removes** — revision, 2026-10-01
([ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md)). Its report of a held
container carries the image and the image's creation date, the networks it is on and the other
containers on each, its mounts and its published ports — the facts a take compares. The host compares
every field it writes before calling a container current, volumes and paths included; its record keeps
a resource's former targets, removes a container or file it wrote under a name the declaration no
longer names, never removes what was found, and reports what runs on the machine that it neither
wrote nor holds. *How it is checked:* ADR 0163's table.
**What the host joins, keeps and raises for a take** — revision, 2026-10-02
([ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 4, 6 and 7). A
container may name networks it also joins once it runs — the found network a per-machine setting keeps
for a taken container while a neighbour still resolves it there; joined after the run, part of the
container's spec, refused when it cannot be joined. A declaration may name the modules the mesh left
out of it because a stored setting cannot compose with the module's definition: the host keeps what it
wrote and holds for a left-out module and says so, where absence used to read as removal. And genesis
raises the bootstrap forge under the forge module's container name, with the module's image digest and
its data directory, so the module holds it by the found rule; the network is the one difference a take
has left to say. *How it is checked:* a host test joins a kept network and refuses one it cannot; a
host test keeps a left-out module's record and hold and removes an absent module's; a bootstrap test
holds the installer's constants to the module's manifest where the catalogue is checked out beside it.
**What filters the machine, and the found firewall kept retired** — revision, 2026-10-02
([ADR 0168](../../02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md)). The host
reports, with every apply, every table and legacy chain that refuses traffic and whose it reads it as —
the mesh's, the found firewall's, the container runtime's own, a ban, or other — and, converged, whether
the firewall it was found with is in force and who retired it. It retires that firewall on every
converged apply, not once, records *found inactive* apart from *disabled by the mesh*, and says when the
step was skipped. *How it is checked:* ADR 0168's table.
**Found reaches every kind that can touch what the machine has**
([ADR 0103](../../02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md)). For a module not yet taken, a directory present with no record
keeps its mode and owner, a unit present with no record keeps its state and boot setting, a
@@ -466,3 +499,17 @@ run and reported; the exit follows an in-flight apply rather than interrupting i
not start is rolled back once and the second failure halts; a completed reconcile retires what is older
than the predecessor and never the predecessor; and the newest of two delivered versions is the one
that runs.
## A service is still running a moment later, 2026-10-02
[ADR 0184](../../02-DECISIONS/0184-a-service-the-mesh-asked-to-run-is-still-running-a-moment-later.md).
The host has always read a unit back after acting on it, because a service manager accepting a
command says the transaction was accepted and nothing about the process. The read raced the failure:
a daemon that refuses the configuration the mesh just wrote exits a fraction of a second after the
manager returns, and one look sees it alive. So the host looks twice, with a pause between, and a
unit that was running and is not any more fails its resource by name. A unit still coming up reads
as running at both looks and is accepted; a service asked to stop is not waited on.
No command for this reaches a machine. A module declaring *how to test my configuration* was weighed
and refused: the link carries no actions, and a verification command is one. The host is checking
the state it was told to establish, which is what it is for. *How it is checked:* ADR 0184's table.
+99 -6
View File
@@ -7,9 +7,15 @@ code:
- mesh-controller internal/identity/authority.go
- mesh-host internal/identity/serving.go
- mesh-host internal/apply (the service that reflects a rule set)
updated: 2026-09-30
updated: 2026-10-02
decisions:
- 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
- 02-DECISIONS/0167-a-membership-carries-what-its-module-receives-and-who-the-mesh-is.md
- 02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md
- 02-DECISIONS/0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md
- 02-DECISIONS/0147-a-module-anchors-the-meshs-authority.md
- 02-DECISIONS/0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md
- 02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md
@@ -170,6 +176,28 @@ the broker's node must be dialable by every node, at a stable address, and so mu
reachable; on one network it does not. A mesh whose nodes are all behind NAT cannot be raised, and
a broker node whose address moves invalidates every token issued for it.
*2026-10-02.* **The order changes at step 1: the tunnel comes first, from the token**
([ADR 0169](../../02-DECISIONS/0169-a-machine-joins-through-the-tunnel-and-the-bus-is-never-public.md)).
The circularity above is real, and it is broken differently. The overlay is configured by the mesh,
except for the one peer a joining machine needs, and the token carries that peer. So the sequence
becomes:
```
0 the node has an underlay address the machine's own
1 the node makes its tunnel key before any token; it prints the public half
2 a token is issued for that key its address assigned, and the hub sent it as a peer
3 the tunnel comes up to the hub from the token alone: the hub's endpoint and key, its address
4 the node dials the bus OVER THE TUNNEL, at the bus's private address
5 it proves itself, and is proved to enrolment, checking the key is the one the token named
6 the rest of the overlay the whole peer set, delivered as files
7 names, filtering, routes as before
```
The link no longer stays on the underlay. The bus is reached over the tunnel by every machine,
including one that is joining, so it is never opened to the internet. The precondition becomes: **the
hub's tunnel must be dialable by every node, at a stable address.** That port answers nothing to a
key it does not know.
**Whether the link should later move onto the overlay, with the underlay as fallback, is
[open](../../02-DECISIONS/0007-connectivity.md).** It is a decision rather than a derivation: the
gain is which network carries bytes, not what an attacker can reach, since the link is already
@@ -305,10 +333,11 @@ and [135](../../04-ISSUES/135-a-containers-mesh-names-are-not-compared/00-report
**A container resolves the mesh's names through its machine's resolver, at the moment it asks, and
nothing is copied.** The resolver is a machine-level process rather than a container, so nothing
circular is being asked for. This is gated on a container being able to reach the resolver from any of
the runtime's networks, which it cannot today
([issue 110](../../04-ISSUES/110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/00-report.md)) —
until that lands the mesh keeps copying and keeps comparing, and the order is stated in the record.
circular is being asked for. It was gated on a container being able to reach the resolver from any of
the runtime's networks
([issue 110](../../04-ISSUES/110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/00-report.md)),
and landed the day that did, 2026-09-30: the controller writes no mesh name into a container and the
host's digest carries only what the module declared for itself.
The paragraph below states the old boundary, and 0148 deliberately gives it up: a container somebody
started by hand resolves the same names as everything else, because the resolver answers the machine,
@@ -324,6 +353,14 @@ machine — declared or not — is what a nameserver in `resolv.conf` would be f
service, the rest is the node — so what resolves is *anything under a node's name*, going to that
node. What routes it once it arrives is a proxy's, and stays separate.
*2026-09-30.* **So the node in a route's internal name is the one whose proxy answers it**
([ADR 0151](../../02-DECISIONS/0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md)).
Composed from the node the module ran on, the name sent a client to a machine with nothing listening
whenever the proxy ran elsewhere
([issue 139](../../04-ISSUES/139-an-internal-route-name-resolves-to-the-consumers-node/00-report.md));
composed from the serving node, the rule above holds without exception. The public name stays the
module's node's, which is where the operator put it.
**The mesh writes the data and runs no daemon.** One wildcard per machine, from the same set that
writes the hosts file. A resolver is third-party software and runs *on* the mesh rather than being
*of* it: the mesh has no business shipping one, choosing which one, or knowing its configuration
@@ -399,7 +436,9 @@ can reach from the outside but cannot resolve from the inside is a name it canno
authority of its own.
**So a granted route is published into internal resolution as well** — the routed name to the node
that serves it, mesh-wide, by the same mechanism that writes the node names. It is *given by the
that serves it, mesh-wide, by the same mechanism that writes the node names — and as itself: a routed
name has no mesh form, and the suffixed alias the roster once added beside it resolved to a refusal
([issue 157](../../04-ISSUES/157-a-routed-names-internal-alias-is-served-by-nothing/00-report.md)). It is *given by the
mesh, not chosen by a module*, for the same reason the node names are: a module listing the routes
would go stale the day one changes. The mesh propagates the names it was told to serve and still
knows nothing about what they mean
@@ -697,6 +736,48 @@ needs no new filter; a declared port is reachable from off the private network a
not; no address of a machine's own networks appears in a rendered filter, asserted on the text; and a
machine reporting no outward link is refused in the control plane with its existing filter left alone.
### A converged machine is filtered by the mesh alone, and the host says what else refuses
*2026-10-02, [ADR 0168](../../02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md),
from [issues 143](../../04-ISSUES/143-converging-does-not-retire-the-firewall-it-found/00-report.md) and
[144](../../04-ISSUES/144-the-predecessors-rules-outlive-the-firewall-it-was-found-as/00-report.md).*
Retiring the found firewall was a step the flip took once, and said it had taken whatever happened;
on the first machine with one it did not take, and a hand's work fifty minutes later was recorded as
the mesh's. And "the firewall found" named one front end while a predecessor's chain in the container
runtime's user hook — legacy iptables on one machine, invisible to a reader of nftables — filtered the
forwarded path, refused ports the mesh declared open, and carried an allowance every module reaching
another by the machine's own name relied on.
**Convergence is a state the host keeps.** Every converged apply reads whether the found firewall is in
force; enabled again, it is retired again and said; the record says whether the mesh disabled it or
found it inactive, and a skipped step is said. **The host reports what filters the machine**, every
apply, adopted or converged: every table and legacy chain that refuses, with an owner — the mesh's,
the found firewall's, the runtime's own plumbing, a ban, or *other*, which is where the runtime's user
chain's refusals go. **The mesh says which:** `node show` lists them; `status` names a converged machine
anything *other* filters and is not well; the converge preview lists what filters the machine and the
fate of each — retired with the front end, left as the runtime's, left as a ban, or *left in force and
not the mesh's*. The mesh removes none of it; adoption's threshold does not move.
*How it is checked:* host tests over rulesets captured from three machines of this mesh classify every
refusing chain (a predecessor's chain in the legacy filter as *other*, a ban list reached through the
user chain as a ban, a leftover front-end chain as *other*); a fake front end enabled again on a
converged machine is retired again and said, found inactive is recorded as found; the report carries
the filters and the found firewall's state and a change in them is worth an unasked report; controller
tests over a fixture report check the recording, the preview's fates, the status JSON and the well
predicate. Live: the home server's record names the predecessor's chain as *other* and `status`
names the machine until the chain is removed by hand.
*2026-10-02, [ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md):* removing what
the host reports as *other* is reached through the packet filter seat's `remove` verb, an operator's act
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 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.
## 5 — Certificates
**Two authorities, kept separate on purpose.**
@@ -830,6 +911,18 @@ One value, three readers:
| `public` | the machine port, to anywhere | the public name | the public authority |
| `both` | the machine port, to anywhere | both names | each name's own authority |
*2026-10-02.* **The proxy serves an internal name to the private network only**
([ADR 0138](../../02-DECISIONS/0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md),
its insight of this date). It answers public and internal names on the same listeners, so the name a
request carries is the request's own claim, not where the request came from. An internal name is
served to the machines of the mesh and to the machine itself; to anyone else it is answered as a name
never routed, in the handshake as well as the request. Without this, an endpoint with reach `internal`
would be public under a name that is easy to guess
([issue 191](../../04-ISSUES/191-a-route-with-only-an-internal-name-is-dropped/00-report.md)).
The proxy is told who the mesh is, and its routes, in its membership on the bus — the same machine
addresses the filter's "from the mesh" is rendered from
([ADR 0167](../../02-DECISIONS/0167-a-membership-carries-what-its-module-receives-and-who-the-mesh-is.md)).
**An endpoint that is not routed is reached and never named.** No route contribution means no name is
composed and no certificate requested, while the filter still acts on it. That is the case the model
could not express at all, and it is the ordinary case for anything that is not HTTP.
+28 -1
View File
@@ -8,8 +8,9 @@ code:
- mesh-host packaging/nox-mesh-host-network.sh
- mesh-controller internal/token
- mesh-controller internal/inventory/nodes.go
updated: 2026-09-23
updated: 2026-10-02
decisions:
- 02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
- 02-DECISIONS/0004-a-node-and-how-it-joins.md
- 02-DECISIONS/0005-the-node-host.md
@@ -311,6 +312,32 @@ found firewall again and converges the openings through it; what was taken stays
a predecessor leaves one and asserts nothing that serves changes until a module is taken or the
node is converged, and that the flip closes exactly what the preview said.
**Taking a module is previewed, and the preview is a comparison** — revision, 2026-10-01
([ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md)). For every held thing a
module would replace, `take` puts what runs beside what the module declares: a container's image and
its age, name, networks and their other members, published ports and their reach, mounts; a file's
kept original against the declared content, as a difference; a secret the mesh minted for a service
that already has one; the module's settings composed against its definition. An older image, a
differing file and a minted secret for found data refuse unless named; a narrowed port and a shared
network are said. `take --yes <digest>` cuts over what was previewed, as the flip does. A taken
container may keep a found network by a per-machine setting while its neighbours are not yet taken.
*How it is checked:* ADR 0163's table.
**A setting is judged where it is stored, and the take's words** — revision, 2026-10-02
([ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1, 2, 4 and 6).
The preview ends with a digest of what it said; `take --yes <digest>` acts on that preview and nothing
else, and a preview that has changed since, or an account of the machine older than the flip allows, is
refused as the flip's is. A module the machine holds nothing for has nothing to compare, and `--yes`
suffices. The overrides are `--downgrade`, `--replace <path>` and `--mint <name>`; the per-machine
setting that keeps a found network is `networks`, a container id to the networks it keeps, accepted
for an adopted machine only. Storing a setting composes it against the module's current definition and
refuses, naming node, module, layer and key, what cannot compose or reaches nothing. A definition that
later moves under a stored setting costs that module its place in the machine's declaration, said by
name in `plan`, `push` and the declaration itself, and the machine is told everything else; a stray
setting no longer refuses the machine where it is read. *How it is checked:* controller tests over the
one judgement — refused where stored, a module left out where composed, the envelope naming it — and
over a take's digest, staleness and secrets.
A candidate machine is not empty. It has a package manager, probably a container runtime,
configuration somebody chose. [ADR 0005](../../02-DECISIONS/0005-the-node-host.md)
says the host never touches what it did not create — adoption is the deliberate act of taking
+28 -1
View File
@@ -7,9 +7,10 @@ code:
- mesh-controller internal/catalogue/build.go
- mesh-controller internal/inventory/secrets.go
- mesh-controller cmd/mesh-builder
updated: 2026-09-12
updated: 2026-09-30
decisions:
- 02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md
- 02-DECISIONS/0037-where-a-module-lives.md
- 02-DECISIONS/0009-modules-and-the-graph.md
- 02-DECISIONS/0010-delivery.md
- 02-DECISIONS/0005-the-node-host.md
@@ -60,6 +61,32 @@ module from a repository and a path, and the root-only reading left every existi
unbuildable — pointed at the catalogue the builder finds no manifest, pointed at a module's source
it finds no manifest either.*
## A manifest is checked where it is written
*Added 2026-09-30, from [issue 148](../../04-ISSUES/148-a-manifest-outside-this-catalogue-has-no-check/00-report.md).*
The check the mesh applies at registration — the manifest parses strictly, every name in it is a
usable one, its routes and events and seats are well formed, and no two manifests given together
declare one seat — is a verb on the controller's binary, `module check <manifest>…`, and it needs no
mesh. It reads the files it is given, runs the same functions registration runs, prints every problem
in the manifest's own words, and exits non-zero if there was one. Somebody describing their own
application in their own repository — the case [ADR 0037](../../02-DECISIONS/0037-where-a-module-lives.md)
calls the one that matters most — runs it before pushing, and finds out there rather than when a
running mesh refuses the registration, or later, when a machine applies something that resolved and
should not have.
**What it cannot know, it says.** A seat another module declares elsewhere is unknown to a check that
was not handed that module's manifest, and the output says so rather than refusing: pass the other
manifest too. The mesh's own seats it knows from the binary, which is the one place that set may be
read without a store ([ADR 0122](../../02-DECISIONS/0122-a-seat-is-data-a-rename-is-a-database-update.md)
keeps the store authoritative, so a claim on a mesh seat is judged fully only at registration, and the
check says that too).
*How it is checked:* the controller's test runs the check over the catalogue checkout beside it and
over a manifest with a known fault, and asserts the first passes and the second names the fault; the
test that used to be the only check, `TestEveryCatalogueManifestParses`, now stands beside a command
anybody can run.
## The manifest in the repository is not the manifest the mesh holds
A resource names an artifact:
@@ -5,7 +5,7 @@ code:
- mesh-controller internal/inventory/secrets.go
- mesh-controller cmd/mesh-controller/rotate.go
- mesh-controller examples/postgres-provisioner
updated: 2026-09-21
updated: 2026-10-01
decisions:
- 02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md
- 02-DECISIONS/0009-modules-and-the-graph.md
@@ -136,3 +136,16 @@ rotates, and is queried for who holds it, through exactly the machinery describe
The rotation a module's own secret lacks is not a second mechanism; it is this one, pointed at a
secret the vault provides. What this page proves for a database password holds, by construction,
for a secret from the vault.
*Built 2026-10-01, the read-at-start half ([issue 180](../../04-ISSUES/180-a-modules-own-secret-cannot-be-rotated/00-report.md),
[ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md)).* An own secret
says how the module takes it — `taken: at-start` or `taken: applied` on its entry — and the mesh
rotates only the first: `secret rotate <node> <module> <name>` makes it anew, seals it to the machine
and the operator, and sends the machine, so the module starts again on it. A secret that says neither
is refused with the word to write, because a credential rotated under software that never reads it
again is the fault of issue 179 made deliberately; an applied one is refused until the staged form is
built; an accepted one is refused as ADR 0113 says. `rotate` is a verb on the controller's seat with
both shapes, so the console asks for either. A provider that shares its one credential with every
consumer ([ADR 0158](../../02-DECISIONS/0158-a-provider-with-one-credential-shares-it-with-every-consumer.md))
rotates the same way, with every holder's copy remade and every holding machine sent together. *How it is checked:* the tests named in issue 180, and a
live rotation through the console of a secret a module reads at start.
+10 -1
View File
@@ -4,9 +4,10 @@ status: in-progress
code:
- mesh-controller internal/licences
- mesh-controller cmd/mesh-controller/licence.go
updated: 2026-09-05
updated: 2026-10-02
decisions:
- 02-DECISIONS/0024-model-access-is-a-provision.md
- 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
- 02-DECISIONS/0009-modules-and-the-graph.md
- 02-DECISIONS/0054-model-usage-is-recorded-at-two-grains.md
- 02-DECISIONS/0055-model-access-is-answered-by-a-licence-or-a-node.md
@@ -96,6 +97,14 @@ So `(node, module)` tells them apart, and asking for a licence per session neede
identity. Checked rather than argued: two sessions on one machine hold different licences, each is
given its own key, and releasing one leaves the other.
*2026-10-02:* the operator's own agent at a terminal is **not** a consumer of this provision: it is
coupled to an Anthropic grant and nothing else, so it uses the `anthropic-licence-manager` seat, whose
holder owns the Anthropic licences, their bindings and their rotation, and hands each node's agent its
token over the bus
([ADR 0183](../../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md),
[36](36-the-operators-agent-on-a-machine.md), [39](39-the-anthropic-licence-manager.md)). This
provision stays for the consumers that do not care which vendor answers.
**What is still open is the rest of the gap, and it is the harder half.** A *worker* is not one
per machine — many can run on one, from one module — so `(node, module)` cannot name them apart
and this reasoning does not extend to them. That belongs with
@@ -148,6 +148,10 @@ than reproduced from a declaration — because there is nothing to reproduce it
([ADR 0025](../../02-DECISIONS/0025-the-design-record-is-read-not-copied.md)), not by holding a
copy. It is that reader; there is not a second agent for it.
*2026-09-30:* the reading is a module's — `records`, [35 — Reading the record](35-reading-the-record.md),
[ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md). The
session, when built, asks it rather than reading for itself; what it adds is judgement, not text.
**It answers into a symptom search**, so what it knows appears beside ordinary results rather than
only when it is asked. **And when it cannot be reached, the search says so.** A result set that
silently omits this material looks identical to one where nothing matched — the same rule as the
+71 -5
View File
@@ -5,8 +5,10 @@ code:
- mesh-controller cmd/mesh-builder
- mesh-controller internal/builder
- mesh-catalog modules/builder
updated: 2026-09-30
updated: 2026-10-02
decisions:
- 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
- 02-DECISIONS/0189-the-store-keeps-what-the-records-name.md
- 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
- 02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
@@ -186,7 +188,8 @@ disagrees with it.
| `grants` | credentials it must create for its consumers |
| `filtering` | rules beyond its own ports |
| `computed` | marks a module the controller generates rather than an author writing |
| `build.artifacts` | what it produces |
| `build.artifacts` | what it produces; an artifact's `context` may be a URL or a path on the git seat (`seat: git`), composed by the mesh that builds it |
| `names-on-purpose` | on a resource: each name it means to name — the world's federation server, a registry that built an application the mesh does not — with its reason. A definition names no installation, and a test says so ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)) |
**A container mounts only what the manifest declares**
([ADR 0091](../../02-DECISIONS/0091-a-mount-is-declared-three-ways.md)). A bind mount the module
@@ -233,10 +236,10 @@ counts as a copy and what as a base.
| resource | is | a module may |
|---|---|---|
| `directory` | a directory with a mode and an owner | ✅ |
| `file` | literal content, with `${bound:…}` and `${secret:…}` filled in | ✅ |
| `directory` | a directory with a mode and an owner, **placed by the mesh** under the node's root: `place: "."` is the assignment's own root, `place: "mesh"` the mesh's directory for the module, a pathless one sits beneath the root by its id; a stated path is the placement for data that must stay where it is, and may itself sit beneath a placed one (`${dir:<id>}/…`). Everything else names it as `${dir:<id>}` ([issue 119](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md), [174](../../04-ISSUES/174-the-meshs-own-files-for-a-module-are-placed-by-the-definition/00-report.md)) | ✅ |
| `file` | literal content, with `${bound:…}`, `${secret:…}`, `${dir:…}`, `${port:…}`, `${machine:…}` and `${setting:…}` filled in — the last an operator's value from the assignment's settings, refused by name when unset ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)) | ✅ |
| `user` | a login | ✅ |
| `access` | a pre-existing path it may use and must not own | ✅ |
| `access` | a pre-existing path it may use and must not own, **named by id** and placed by the assignment (`accesses: {<id>: <path>}` on its settings); mounts say `${access:<id>}`; a path in the definition is the default an assignment replaces, tolerated while the catalogue converts ([issue 153](../../04-ISSUES/153-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md)) | ✅ |
| `archive` | files fetched by digest and unpacked | ✅ |
| `package` | a package that must be present | ✅ |
| `network` | a named container network | ✅ |
@@ -266,6 +269,69 @@ ships one and wrong for code the mesh built, which has no unit until the mesh wr
**Tools, hooks and consumers are not further modes**, which is the test of whether three is the
right number: they are loaded by a tool host, and a tool host is a process that stays up.
## A build says what it does, as it happens
*2026-10-01 — [ADR 0157](../../02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md).*
A build machine narrates every build on the bus as the role it holds: `started` when it takes the
work, one `log.<build id>` event per line — every command it runs with its duration, every step of
the recipe, and on failure the command's own output, line by line — and `built` for the outcome as
before. The same lines still go to the machine's standard error, so a build machine with nobody
listening is as readable as it was; a listener reads the same lines, live, from anywhere on the mesh.
One build is one subject. A reader follows it by subscribing that subject and nothing else, and the
events stream keeps it for a week, so `builds --log <id>` — on the command line and as the
controller's seat verb through the console — reads it back afterwards. `builds` lists every build's
id beside it, and `build` says the id it asked with. The mesh keeps no second copy: the stream is the
log. The console's `build` tool asks and answers at once with the id; the outcome is taken in — the
build recorded, the module registered with its source — by whoever hears it, the waiting command or
the daemon following the role's event, so a build nobody waited for still reaches the catalogue
([issue 176](../../04-ISSUES/176-the-consoles-build-tool-neither-waits-nor-registers/00-report.md)). A viewer of builds, when one is built, is a subscriber over these subjects and the outcome; the
builder needs nothing more for it.
*How it is checked:* the holder's grant is exactly `started`, `built` and `log.*` (broker test); a
build's lines reach a reader of its subject in order and the stream holds them afterwards (link test
against a real server); the seat verb with an id reads the log (controller test); and, live, a build
after the roll-out read line by line through the console.
## The store keeps what the records name
*2026-10-02 — [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md),
[issue 108](../../04-ISSUES/108-the-registry-has-no-garbage-collection-once-it-has-two-doors/00-report.md).*
Every build pushes another layer set and, until this, nothing ever removed one. The registry's own
answer — collect what no tag names — is wrong for this mesh: each artifact is pushed under one
moving tag and machines are pinned by digest, so every build but the newest is untagged and some
machine may still be running it.
**The mesh decides and the store reclaims.** Deletion is enabled on the store's one door — that
door already accepts a push, and a writer who can push can replace any tag, so delete takes
nothing a push did not already have, and ADR 0082's bargain (a store every machine reaches with no
credential to distribute first) is kept. The mesh then removes what it put there and no longer
keeps, **naming it from its own build records** rather than enumerating the store: it has never
put anything there it did not record, so a digest it did not record making is never named, which
is what keeps the sweep away from the images genesis pushed before any record existed.
An artifact stays for one of two reasons and otherwise goes: a definition the mesh holds names it
(no age limit — this is the floor), or it belongs to one of the five most recent successful builds
of its module (somewhere for a wrong release to return to). The sweep runs after a build the mesh
recorded, which is the moment new bytes landed and the moment the keep set moved; it needs no
timer. Deleting a manifest frees no bytes, so the store's own collector runs nightly as a
scheduled step with the server held still — which is what `while-stopped` exists for
([design 20](20-writing-a-module.md)). Plain collection, not `--delete-untagged`: what the mesh
keeps is still a manifest and so still referenced, and the dangerous flag is not needed once the
mesh is the one deciding.
A machine behind by more than five builds of a module, recreating a container, cannot pull what it
was running. It is already a machine the mesh reports as behind, and the answer is the current
declaration.
*How it is checked:* the keep set, against records, holds what a manifest names and the five most
recent builds and nothing else; a reference the mesh never recorded is never in the delete set; an
image and an archive are asked for at their own endpoints; a store with deletion off names the
remedy rather than the status code; a store that does not have it is recorded collected rather
than retried for ever. Live: the store's size before and after the first nightly collection.
## The builder compiles the languages the mesh is written in
*2026-09-29 —
+26 -1
View File
@@ -5,11 +5,12 @@ code:
- mesh-catalog modules/showcase
- mesh-controller internal/builder
- mesh-sdk src
updated: 2026-09-30
updated: 2026-10-02
decisions:
- 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
- 02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md
- 02-DECISIONS/0053-a-step-that-runs-on-a-schedule.md
- 02-DECISIONS/0189-the-store-keeps-what-the-records-name.md
- 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
- 02-DECISIONS/0040-what-a-module-is.md
- 02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md
@@ -208,3 +209,27 @@ is recreated with the new fact
([ADR 0099](../../02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md)). *How it is
checked:* the host's unit tests run a step again when its named file changed and not otherwise,
and recreate a container naming a step after the step ran.
## A recurring step may hold its own module's containers still
*Written 2026-10-02, from [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md)
and [issue 108](../../04-ISSUES/108-the-registry-has-no-garbage-collection-once-it-has-two-doors/00-report.md).*
Some work cannot be done underneath a running service: an artifact store's collector walks the
storage and requires every writer stopped. A `run-once` step runs *beside* containers and a
scheduled one is the same container again, so until this a module had no way to say it — and the
mesh inherited a store that has never collected anything, because the predecessor said it with a
shell script and a script beside a module is not a resource in it.
A scheduled step may name `while-stopped`: resource ids of **its own module's** containers, which
the host stops before the run and starts again after it, in the reverse order, **whatever the step
did**. Three boundaries, each refused where it can be seen earliest — its own module's containers
only, because a module that could quiesce a neighbour could stop the mesh; scheduled steps only,
because at apply the declaration is applied in order and a step already gates what follows, so a
one-time offline job says *before* rather than *instead of*; and restoring that is not conditional
on anything, because the only real risk of the field is a window that never closes.
*How it is checked:* the host's unit tests assert stop–run–start in that order, the restart after a
step that **failed**, the reverse order for several containers, and a service left down said
loudly. The controller refuses, from the definition alone, a window with no schedule, one on a
run-once step, one naming a container the module does not declare, and one naming itself.
+18 -1
View File
@@ -2,8 +2,9 @@
layer: to-be
status: implemented
code: [mesh-catalog, mesh-controller, mesh-host]
updated: 2026-09-21
updated: 2026-10-01
decisions:
- 02-DECISIONS/0158-a-provider-with-one-credential-shares-it-with-every-consumer.md
- 02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md
- 02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md
- 02-DECISIONS/0085-a-secret-is-a-provision.md
@@ -127,6 +128,22 @@ credential a provider grants; the export names each entry by the node and module
the name they know it by, and says whether it is a module's own secret or a pair credential, so
recovery addresses both alike.
### A provider with one credential
*Decided 2026-10-01 ([ADR 0158](../../02-DECISIONS/0158-a-provider-with-one-credential-shares-it-with-every-consumer.md)); the controller's half built the same day (mesh-controller PR 184): the offer's word, the need carrying the shared secret's name, the vault's one value under one generation stamp, remade for every holder on a later binding or a rotation, the rotate command sending every holder. What remains is each provider's definition saying `credential` and `taken`, with a start that applies the file — the media catalogue's work.*
Software that holds one credential — a download client's web password, an indexer's one API key —
cannot give each consumer a login, so ADR 0048's form does not fit it and its values were accepted
by hand. An offer may now say `"credential": {"own": "<secret>"}`: the provider's own secret *is* the
credential every consumer of that provision receives, in the shape of an ordinary pair credential,
under the provider's one user name. The vault keeps one value per provider assignment and provision,
sealed to the provider's machine, each consumer's machine and the operator; because it holds no
plaintext it remakes the value for every holder at once when a consumer binds or unbinds or a
rotation is asked, and the mesh sends every holding machine together. The provider takes it as it
says it takes its own secret (`taken`, issue 180); consumers read it at start. An accepted value is
sealed to the consumers of the moment and not remade; a consumer that binds later waits for the next
acceptance. *How it is checked:* the rows of ADR 0158's table; the controller's rows pass, the live row waits for the first provider.
## Beyond generate and hold
Owning a secret means owning more than its creation. The mesh being migrated onto has a working
+28 -2
View File
@@ -7,8 +7,11 @@ code:
- mesh-tools src/broker-amqp.ts (to be replaced)
- mesh-catalog modules/nats (to be written)
- mesh-sdk src (the protocol's NATS binding, step 3)
updated: 2026-09-27
updated: 2026-10-02
decisions:
- 02-DECISIONS/0167-a-membership-carries-what-its-module-receives-and-who-the-mesh-is.md
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
- 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
- 02-DECISIONS/0106-the-bus-is-nats.md
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
@@ -80,8 +83,24 @@ mesh.seat.<seat>.accept.<verb> work submitted to a role (JetStream: per-s
mesh.seat.<seat>.event.<verb> a role's own event (JetStream: EVENTS)
mesh.seat.<seat>.tool.<verb> a role's tool (core request/reply)
mesh.ask.<node>.<command> the controller's command api (core request/reply)
mesh.assignment.<node>.<module> an assignment's membership (JetStream: ASSIGNMENTS, last-per-subject)
```
**Revised 2026-10-01** ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)): the rows above
for a module's and a seat's tools are the shapes the controller *issues*, not rules a runtime carries.
Every assignment is published a membership — what it serves and where, in which queue, its seat verbs,
where its events land, what it may reach — on `mesh.assignment.<node>.<module>`, kept last per subject
like a declaration, republished when the assignment's facts change. The runtime serves exactly that
list; the account's grant is the same membership read the other way; the console's listing carries each
tool's subject. The one rule a runtime keeps is the membership's own subject, from the two names in its
credential.
**Revised 2026-10-02** ([ADR 0167](../../02-DECISIONS/0167-a-membership-carries-what-its-module-receives-and-who-the-mesh-is.md)):
a membership also carries **what its module receives**, by requirement — the contributions its
received file is written from, from the same composition — and **who the mesh is**, every machine's
address on the private network, the list the filter's "from the mesh" is rendered from. The route
proxy is the first reader of both.
**Revised 2026-09-27** ([ADR 0129](../../02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md)):
**`mesh.build.request`, `mesh.control.built` and the BUILDS stream are gone.** A build is work submitted to a role, and the
mesh already has a shape for that — a seat's `accept` subjects, on a work queue with a queue group of
@@ -135,7 +154,7 @@ Core NATS is at-most-once. Everything the mesh must not lose lives in a JetStrea
|---|---|---|---|
| CONTROL | `mesh.control.>` except `alive` (a build's outcome moved to its seat, ADR 0121) | work queue, one consumer (the controller), explicit ack | the store-window guarantee ([ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)): the controller `nak`s with a delay while its store is away and the message is redelivered; nothing is dropped |
| NODES | `mesh.node.>` | last per subject | one declaration per node, always the newest |
| EVENTS | `mesh.mod.*.event.>` | limits (age, size), durable consumer per subscribing module | a subscriber that was down catches up; after `max-deliver` attempts the advisory feeds `mesh.events.dead` (its own small stream) |
| EVENTS | `mesh.mod.*.event.>` and `mesh.seat.*.event.>` | limits (age, size), durable consumer per subscribing module | a subscriber that was down catches up; after `max-deliver` attempts the advisory feeds `mesh.events.dead` (its own small stream). *2026-10-01:* a build's whole log is here too, as the build-machine seat's `log.<build id>` events ([ADR 0157](../../02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md)) — one subject per build, a week of retention, read back by `builds --log <id>` with a consumer that is gone when the reading is done |
Tool calls and heartbeats stay on core NATS: a lost heartbeat is the next heartbeat; a lost tool
call is a timeout the caller already handles.
@@ -361,6 +380,13 @@ bridged. It is three things:
Nothing is built of this before §10's bed passes; the MCP surface is a thin adapter over (2).
*Built, and then made a module — 2026-09-30.* (1) and (2) exist: `operator issue` and the `mesh`
client. What (2) describes as a program on the workstation is now the recovery path; the surface an
operator uses is a module the mesh assigns to the machine, holding a credential the mesh minted —
[ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md),
[34 — The console](34-the-console.md). The tool list it asks for is no longer
`catalog_tools`, which nothing served: each runtime answers `tools` for its own module.
## 8. What a module sees, and what the wire does
**The contract a module is written against does not change.** `publish` on an envelope becomes a
+18 -4
View File
@@ -10,8 +10,9 @@ code:
- mesh-controller cmd/mesh-controller/source.go
- mesh-controller internal/inventory/migrations/0032-a-source-may-live-on-a-seat.sql
- mesh-catalog modules/gitea/module.json
updated: 2026-09-27
updated: 2026-10-01
decisions:
- 02-DECISIONS/0161-what-deserves-a-seat.md
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
- 02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md
@@ -75,6 +76,17 @@ named at the wrong scope. Adding a seat is a decision, recorded, for the reason
host's vocabulary is one: the set is what a person reads to learn what a mesh can have, and an entry
nobody argued for is an entry nobody can explain.
**What deserves one** ([ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md)). A provision the
mesh's own code dereferences by name is delivered by a mesh seat its provider claims — the store, the
bus, the vault, the artifact store, the catalogue. A provision a module merely offers may have several
providers, and a consumer with several is a person's choice. A fact of the shape *exactly one machine
is X* — the hub — is not a seat, because a seat is held by a module assignment and points at it; it is
a placement with a capacity of one, kept by the store, refused by name when a second is placed, and
named in the listing. And a seat whose role is to speak to what the machine runs — the uplink — is
held only by the dialect the machine runs: the machine says which in its profile, renewed with every
report, and the holder declares the capability, so the wrong one is refused the way any missing
capability is.
## The set
**The set is derived, and only the mesh's half is written here.** Revision, 2026-09-26
@@ -108,8 +120,8 @@ convention, which later seats departed from.
| `mesh-controller` | — | mesh | — | the controller |
| `mesh-store` | — | mesh | — | the store the mesh's own records live in |
| `mesh-broker` | — | mesh | `mesh-bus` | the broker carrying the mesh's own bus |
| `mesh-vault` | — | mesh | `secret`, reserved | the vault |
| `mesh-artifact-store` | `the-artifact-store` | mesh | `artifact-store` | the artifact registry |
| `mesh-vault` | — | mesh | `secret` | the vault (in the seed since [ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md); the earlier *reserved* named an effect no rule produced) |
| `mesh-artifact-store` | `the-artifact-store` (renamed 2026-09-30, [ADR 0156](../../02-DECISIONS/0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)) | mesh | `artifact-store` | the artifact registry |
| `mesh-catalog` | `the-catalogue` | mesh | — | the catalogue |
| `mesh-npm-package-registry` | `npm-package-registry` | mesh | `npm-package-registry` | the forge |
| `mesh-git` | `git` | mesh | `git` | the forge |
@@ -240,6 +252,8 @@ checked as their tables say:
| A handover replaces the holder as one write, needs an assignment to point at, and goes with it | 0131: store tests — a second handover leaves one row; a handover to a module not assigned where named is refused; unassigning the holder removes the row. |
| A requirement naming a seat is answered by its holder; a foundation seat cannot be named | 0118: resolution tests with a second provider on the consumer's node, with the seat unheld, and naming `mesh-store`. |
| Several providers and none local is a person's choice | 0118: an assignment test listing candidates with the seat's holder first and recording the pin. |
| `secret` is reserved | 0118: the parser and resolution refusals for another provider and a pin. |
| `secret` has one provider, the holder of `mesh-vault` | 0161: a second claimant of the seat is refused by name (`CanHold`); *correction of fact, 2026-10-01: no parser rule ever reserved the word, the seat does the work*. |
| A singular fact about machines is a placement of capacity one, refused by name | 0161: the overlay command's test for a second hub; the store's unique index. |
| A holder of `node-uplink` is the dialect the machine runs | 0161: the host reports `uplink-<manager>` in its profile with every report; a resolution test refuses the other holder naming the capability. |
| Holdings are derived, and the overview lists every seat | 0118: the `seats` command test, including an unheld seat. |
| A build source on the seat records no address; an unheld seat refuses only self-hosted builds | 0111's tests. |
@@ -1,10 +1,11 @@
---
layer: to-be
status: proposed
code: []
updated: 2026-09-26
status: in-progress
code: [mesh-controller internal/catalogue]
updated: 2026-10-02
decisions:
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
- 02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md
- 02-DECISIONS/0115-one-assignment-of-a-module-per-node.md
- 02-DECISIONS/0113-the-vault-makes-every-secret.md
- 02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md
@@ -13,6 +14,7 @@ decisions:
- 02-DECISIONS/0084-which-provider-serves-a-consumer.md
- 02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md
- 02-DECISIONS/0038-the-mesh-assigns-the-port.md
- 02-DECISIONS/0188-a-provider-declares-what-it-derives-for-each-consumer.md
---
# 27 — A module requires, the mesh resolves
@@ -136,6 +138,12 @@ lost is a named volume, not a directory ([ADR 0030](../../02-DECISIONS/0030-data
the assignment says nothing;
- **a placement**, where the assignment puts one directory elsewhere: on a second disk, or where an
adopted machine's data already is ([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)).
*Built 2026-10-01 ([issue 153](../../04-ISSUES/153-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md)):*
`places` on the assignment's settings, by directory id, with an owner where the data already has
one; and `accesses`, by access id, for the operator's data — an access has an id and its mounts
name it as `${access:<id>}`. Both validated as `endpoints` is: an id the definition does not
declare is refused. *How it is checked:* the controller's placement tests, and the
path-preservation proof extended to accesses.
**An operator's shared data** is an access, as before ([ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md)):
never created, owned or removed by the mesh. The module requires read or read-write access. Where
@@ -171,6 +179,50 @@ make a new external key, so rotating one means an operator handing over a new va
assignment, and the route provider answers. A public name already held by another assignment is
refused, like any other singular thing.
**Built so far, 2026-09-30** ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)):
the operator's value in its first form — `${setting:<key>}` in a file's content, from the assignment's
settings layers, refused by name when nothing set it; a module told the name its route composes
(`${bound:<route>:name}`); a build context on the git seat; and the check that no definition names an
installation, with `names-on-purpose` for the names a definition means. The host's directory in its
first form is [`${dir:<id>}`](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md),
a placed directory under the node's root. Each is this design's provider in the shape the existing
placeholders have, not yet the one requirement form below; they are phase 1's first cases.
*Phase 3, in part (2026-09-30):* every definition's **own** data directory is placed; the conversion
moved no data, proven by resolving both catalogues with the controller's rule and comparing
([issue 119](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md)).
What the mesh writes *for* a module was still placed by the definition
([issue 174](../../04-ISSUES/174-the-meshs-own-files-for-a-module-are-placed-by-the-definition/00-report.md)),
the gap this design answered on 2026-09-26; built later the same day: a directory saying
`place: "mesh"` is `<root>/mesh/<module>`, a directory beneath a placed one states its path as
`${dir:<id>}/<rest>` and moves with it, and the same proof — both catalogues resolved and compared —
shows forty-eight definitions naming the paths they named before.
*The operator's value travels only where it is asked for (2026-09-30,
[issue 173](../../04-ISSUES/173-a-modules-settings-reach-every-fact-it-contributes/00-report.md)):*
a setting overrides a key a contribution or a served fact declares and adds none; a file keeps taking
any key. A provider that must tell its consumers an operator's value — a mail server's domain, an
identity provider's issuer — declares it in what it serves as `${setting:<key>}`, and it is refused by
name when nothing sets it. That is the contract half of this design's operator provider in the shape
the placeholder allows: the definition says which values reach which requirement, and nothing else
does. *How it is checked:* the unit tests named in issue 173, and the plan comparison that closed it.
*A provider says once what it derives for each consumer (2026-10-02,
[ADR 0188](../../02-DECISIONS/0188-a-provider-declares-what-it-derives-for-each-consumer.md),
[issue 124](../../04-ISSUES/124-a-consumer-cannot-be-told-what-its-provider-derived/00-report.md)):*
where a provider **names the resource** it gives each consumer — a bucket, a database, a vhost — the
name is derived per consumer, and a literal `serves` block could not carry it. A served value may
now name the consumer the mesh is serving: `${consumer:as}`, the identity the mesh minted, and
`${consumer:as:dns}`, that same identity written as a DNS label. Nothing else — **the mesh learns no
protocol here; it spells its own name in an alphabet it already knows.** Settings are laid on first,
so an operator may still set a prefix and the mesh derives the rest. The mesh fills it at the one
moment it knows who the consumer is, and the one filled value reaches both ends: the consumer, as
its binding's served facts and as `${bound:<provision>:<key>}` in any file it writes; the provider,
as `derived` on that consumer's entry in its contributions file, so its provisioner is told the name
rather than recomputing it. A consumer that writes the derived value into its own definition instead
of asking for it is refused, naming the placeholder to use. *How it is checked:* the unit tests in
ADR 0188's "how this is checked", each run against the unchanged controller first.
## How a definition reads what was resolved
**One form, naming a requirement and a field of its contract.** A definition that needs the database's
@@ -179,7 +231,9 @@ name in a configuration file writes the same thing: the requirement's name and t
controller fills it at resolution.
This one form replaces the placeholders that exist today, one per mechanism: bound values, secrets,
ports and machine facts.
ports and machine facts. It subsumes the consumer placeholder too — a value a provider derives is
read by the consumer exactly as any other field of the contract is, and `${consumer:…}` is only
how the *provider* states the rule.
**The seat placeholder stays, for the controller alone.** The controller composes its own
declaration and reaches the store and broker it made before any module existed, so it cannot be
@@ -379,7 +433,9 @@ writes (`/var/lib/mesh/<module>`: sealed credentials, composed bindings) need a
module-visible reservation. They do not: a module *requires* a `host-path` and receives a
location; what the mesh writes for the module is the mesh's plumbing, placed where the mesh
chooses and mounted in — never part of the module's contract. One reservation per
requirement, `<root>/<module>/<name>`.
requirement, `<root>/<module>/<name>`. *(Built 2026-09-30: the mesh's directory for a module is
`<root>/mesh/<module>`, named in the definition as a placed directory and nowhere as a path —
issue 174.)*
**Resolution happens in the controller, at declaration composition.** The node receives
concrete paths exactly as today — the wire format and the host's apply do not change for
@@ -1,9 +1,14 @@
---
layer: to-be
status: proposed
code: []
updated: 2026-09-27
status: in-progress
code:
- mesh-controller internal/inventory
- mesh-controller internal/catalogue
- mesh-controller cmd/mesh-controller
updated: 2026-10-02
decisions:
- 02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md
- 02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
- 02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md
- 02-DECISIONS/0051-shared-data-is-the-operators.md
@@ -14,10 +19,10 @@ decisions:
# 29 — A node has operator accounts, and the mesh owns what lives under a home
**The mesh models machines but not the people on them.** A node record holds its name, its
address, its mode — and nothing about *who a person is* on it: `jochens` on novox, `ace` on ace,
`jochen` on shanks and g14. That username is not incidental. It decides who a file under `~` is
owned by, who a user service runs as, and — the case that surfaced this — which account `ssh
<node>` logs in as. The predecessor knew it (its per-node `user:`, and the modules that wrote a
address, its mode — and nothing about *who a person is* on it: one login name on the build node,
another on the home-server, a third on both workstations. That username is not incidental. It
decides who a file under `~` is owned by, who a user service runs as, and — the case that surfaced
this — which account `ssh <node>` logs in as. The predecessor knew it (its per-node `user:`, and the modules that wrote a
person's `~/.ssh/config`, `~/.zshrc`, `~/.config`); the mesh, taking those over, kept the machine
facts and dropped the human one.
@@ -28,8 +33,9 @@ Several things are missing, and they are one idea.
A node has one or more **operator accounts**: the human logins on it. At minimum a name; the
mesh already knows the node and its address, so `<account>@<node>` is then a complete answer to
"who am I, where." It is the mesh's to hold because everything below is derived from it, and
because it is exactly the fact that was silently lost — `ssh ace` failed to `ace` because nothing
in the mesh said ace's account is `ace`.
because it is exactly the fact that was silently lost — `ssh home-server` logged in under the
workstation's own name, because nothing in the mesh said the home-server's account is a different
one.
## 2. A resource may live under a home, owned by its account
@@ -65,14 +71,14 @@ create `~/.ssh` at `0700`, chown it to the account, and own the files it places
**The boundary — and it is the reason this is safe:** `~/.ssh` is the one directory where a wrong
declaration locks a person out of their own machine. So the mesh's *found-vs-owned* semantics
([ADR 0126](../../02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md),
([ADR 0118](../../02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md),
adoption) apply *inside* the home directory. The mesh **owns** the directory and the files above; it
**holds as found — never rewrites, never removes** — the operator's own contents: their **private
keys** and their **personal drop-ins** (`config.d/personal`, the personal `Host` aliases a
workstation carries, exactly as `hosts.local` is the home the mesh never rewrites for `/etc/hosts`).
Reconcile removing an unassigned `config.d/mesh` is fine; the same logic aimed at `id_ed25519` or an
operator's own `authorized_keys` entry is a lockout. This is the login-channel cousin of the rule
[ADR 0125](../../02-DECISIONS/0117-a-machines-uplink-is-a-seat.md) draws for the uplink and the sshd
[ADR 0117](../../02-DECISIONS/0117-a-machines-uplink-is-a-seat.md) draws for the uplink and the sshd
module draws for the firewall: **the mesh must never be able to arrange the one failure that severs
its own way back in.** The carve-out is not a convenience; it is that rule, in `~/.ssh`.
@@ -108,12 +114,12 @@ found-vs-owned boundary of §3 is exactly what guarantees nothing already there
None of this needs a node to discover the mesh, and none of it needs a control-plane module of its
own. The ssh files are **roster facts**
([ADR 0128](../../02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md)): once the
([ADR 0120](../../02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md)): once the
roster view carries a node's **host key** and its **account** beside its name and address, the
`ssh-client` module ships a template for `known_hosts`, `config` and `authorized_keys`, and the
controller renders each node's copy from the full roster and pushes it. The mesh owns the data; the
module owns ssh's format; the control plane gains no ssh syntax. It is the same act as composing a
peer list or `/etc/hosts` — which is why there is **no novox-only "mesh-ssh" module**: the
peer list or `/etc/hosts` — which is why there is **no control-node-only "mesh-ssh" module**: the
centralization is the controller's composition, not a module that runs somewhere. Only non-secret
facts travel (names, addresses, accounts, host keys, the CA public key); the private key stays the
operator's, placed as an operator-owned file, referenced by path.
@@ -129,6 +135,53 @@ operator's, placed as an operator-owned file, referenced by path.
They meet at the account and the CA, not at a bespoke module. The `sshd` server side already exists;
the client/identity side and the CA are the open pieces.
## What has shipped, and what has not
*Recorded 2026-10-01 from the controller's main branch, not from intent.*
**Built (mesh-controller, merged 2026-09-27):**
- **§1, the account as a node fact.** A node record carries an operator account and, optionally,
its home. Empty is a real state — a freshly enrolled or headless machine has no operator account
known yet — and an empty home means *derive it* (the superuser's home for the superuser, the
conventional per-user home otherwise), so the common case needs no entry. The controller's node
command sets it. One account per node is what exists; "one or several" below is still open.
- **§2, resources under a home.** The account and its home are offered as machine facts, and a
resource's *path and owner* resolve placeholders exactly as its content does — so a module places
a file under a person's home, owned by that person, naming neither. A roster file may say it lives
under the home: it is rendered per node, placed under that node's account's home, chowned to the
account, and a node with no account gets none.
- **§5, the composed ssh config.** The roster rendering carries each node's account, so the
`ssh-client` template can emit a `Host` block per node with the right login name. Composed
end-to-end in the controller's tests.
**Written but not shipped:** the `ssh-client` catalogue module itself exists on a branch of the
module repository; its pull request was closed with a hold until this design is deployed, and
nothing has deployed it since. The predecessor's generator still writes every workstation's ssh
client blocks today — which is where [issue 172](../../04-ISSUES/172-the-ssh-client-block-matches-one-spelling-of-a-machine/00-report.md)
was found.
**Not built:** the SSH CA and certificates (§4), `known_hosts` and `authorized_keys` as roster files,
the found-vs-owned boundary inside `~/.ssh` (§3 — the controller has no rule yet that refuses to
rewrite a private key), adoption of existing keys, the ssh-agent as a user service, and user-scoped
services in general. The host vocabulary still has no user-scope unit at all; a workstation's
per-user daemons (a bar watchdog, a config reloader, an audio service masked per user) have no form
the mesh can send.
**A gap this surfaced:** §1 shipped as code before it had a decision record. The account as a node
fact, the home as a placement root, and what the mesh may and may not do under a home are each a
decision this document names but no record states. They are the next records to write, before the
family of §2 modules is built.
*2026-10-02:* two of them are written. [ADR 0181](../../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)
records the account as a node fact and the home as a placement root, reconstructed from what shipped;
[ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
generalises §3's boundary to every directory under a home. The first member of the §2 family is
designed in [36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md). User-scoped
units are [ADR 0177](../../02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md),
written the same day; still unwritten: several accounts per node, and the CA. On the same day every node of the
live mesh still carried an empty account.
## Why now, and why not yet
**Why it matters:** when HAL retires, the generators that keep `~/.ssh`, shell config and the
@@ -137,7 +190,7 @@ alias and its trust, and a fresh machine has no operator dotfiles at all — the
service and leave the human unable to work on the box.
**Why not build it reflexively:** it is a real addition to the node model, the resource model, and
the seat set, and must be gotten right. The mechanism half is now settled — ADR 0128 is what lets
the seat set, and must be gotten right. The mechanism half is now settled — ADR 0120 is what lets
the ssh files be templates with no control-plane format — so what remains to decide here is the
model:
@@ -156,15 +209,22 @@ model:
unnecessary, and forwarding an agent into a node exposes the operator's keys to that node's root —
so prefer certificates and `ProxyJump` over forwarding.
**Not urgent, not blocking.** ssh and dotfiles work today because HAL's generators still run as the
substrate. This becomes load-bearing in the node-by-node retirement phase, not before — which is the
right time to build it, once the account and CA model are decided here.
**Now load-bearing.** The migration of every node to the mesh is complete; what remains of the
predecessor is exactly the user environment this design covers — ssh config, dotfiles, the desktop
stack and the per-user services of the two workstations. Those generators are the last thing
keeping the predecessor running, so the model questions above are no longer deferred: the account
record, the home as a placement root, user-scoped services and the one-off steps a hook used to run
each need a decision before the modules that replace the generators can be written.
## The family beyond `~/.ssh` — 2026-10-02
The modules §2 calls *a family* — the shell, the terminal, the desktop, everything under a home that is not `~/.ssh` — are designed in [37 — The operator's machine](37-the-operators-machine.md), under [ADR 0173](../../02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md) to [0177](../../02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md). This document keeps `~/.ssh`, the CA and the roster files. Two things it listed as not built are decided there: user-scoped services (ADR 0177) and the one-off steps a hook used to run (declared state, or a seat's verb).
## References
- The gap was found generating `~/.ssh/config` from the *HAL* registry (`hal/terminal`'s
postConfigure hook), which the nox mesh has no equivalent for.
- [ADR 0128](../../02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md) — the roster
- [ADR 0120](../../02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md) — the roster
fact mechanism that renders the ssh files, format owned by the module.
- [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) — the
system-path placement this mirrors for home paths.
@@ -173,6 +233,6 @@ right time to build it, once the account and CA model are decided here.
- [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md) — the CA key is a secret the
vault makes; [ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md)
— short-lived certs as rotation.
- [ADR 0125](../../02-DECISIONS/0117-a-machines-uplink-is-a-seat.md),
[ADR 0126](../../02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md) —
- [ADR 0117](../../02-DECISIONS/0117-a-machines-uplink-is-a-seat.md),
[ADR 0118](../../02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md) —
the never-sever-the-channel rule and the found-vs-owned semantics, applied here to `~/.ssh`.
@@ -2,8 +2,10 @@
layer: to-be
status: proposed
code: []
updated: 2026-09-27
updated: 2026-10-01
decisions:
- 02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md
- 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
- 02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md
- 02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md
---
@@ -114,6 +116,19 @@ automate the freeze.
(this is how the uplink managers and the re-registrations above were done). Only image-bearing
modules need the build machine, which narrows what the deadlock above can block.
## What a merge does now (2026-10-01)
Revision, [ADR 0162](../../02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md). The
trigger exists: the forge announces a merge on the bus and the controller acts on it (ADR 0157 made
the build narrate; this makes the merge a plan). A module's dependencies are one relation in the
catalogue — `depends-on` edges of four kinds: stands-on, packages, built-by, declared. A merge takes
what changed and everything reachable from it along those edges, sorts the set into tiers, writes the
plan to the store, asks the first tier and returns. Each outcome advances the plan; a tier whose
rolled-out modules a later tier is built by waits until the machines report them applied; a
controller replaced mid-plan resumes from the store. `status` lists open plans and names one that
has waited too long. The transition discipline for breaking changes in the list above is still
unwritten, and still the next thing.
## Why now, and why not yet
**Why it matters:** self-update is the difference between a mesh a person maintains by typing
@@ -1,10 +1,15 @@
---
layer: to-be
status: proposed
code: []
updated: 2026-09-27
status: in-progress
code:
- mesh-controller: internal/catalogue/jails_into.go, internal/catalogue/manifest.go (Jail, Jailing)
- mesh-catalog: modules/fail2ban (jailing, the base and the seat's verbs), modules/mailu, modules/route-proxy, modules/gitea (jails)
- mesh-host: internal/declaration/declaration.go (a container's logging)
updated: 2026-10-02
decisions:
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
- 02-DECISIONS/0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md
- 02-DECISIONS/0186-a-ban-list-never-holds-a-neighbour.md
---
# 31 — A module declares its fail2ban jail, and the mesh composes them per node
@@ -63,3 +68,38 @@ jail, composed from the postgres module's manifest, without anyone editing a nod
beside)
- mesh-catalog `modules/fail2ban` (the base: sshd, recidive, ignoreip); the service modules
(`postgres`, `mssql`, `mailu`) that will declare jails
## Decided and built, 2026-10-02
[ADR 0179](../../02-DECISIONS/0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md)
made this the rule and built it. A module declares `jails` — each a name, the `failregex` of a
failed attempt in its log, and the stanza's own keys — and the fail2ban module declares `jailing`:
the one file the stanzas compose into and the directory each filter lands in. The controller gathers
every assigned module's jails per node into those; the holder's daemon restarts on the composed file.
What made it workable was the log. A container's output went to a file of the runtime's own, under
a path that changes when the container is recreated, so no jail could read a container's service
however it logged. A container now declares `logging: journald`, the host runs it with the journal as
its driver, and a jail reads it with `backend = systemd` and a `journalmatch` on the container's
name — the same way the base's ssh jail has always read the ssh daemon. The first three doors: the
mail front end (every login failure on its proxying ports), the forge (a failed authentication
attempt) and the public proxy (a certificate or request for a name the mesh does not serve, which
the proxy now says in its log). The base is strict — three in a day for a day; twice banned in two
weeks for four — and the mesh's own range stays never banned.
The seat the module holds serves `status`, `banned`, `ban` and `unban`, from a runtime that carries
only the fail2ban client with the daemon's socket shared in; the jails are composed, the ban list is
the daemon's, and both are read through the console.
*How it is checked:* ADR 0179's table.
## What the first jails taught, 2026-10-02
[ADR 0186](../../02-DECISIONS/0186-a-ban-list-never-holds-a-neighbour.md). Within an hour of the
first public jail the home server had banned the house's own router: the router reflects local
traffic, so every client in the building arrives as the gateway's address. The never-ban list now
holds every private range as well as the mesh's own. And the mesh read its own ban chain as a
foreign rule set on that machine, because the chain hangs off the container runtime's user chain and
that machine's forward policy is the runtime's DROP — the reader now calls a chain of source-named
refusals a ban wherever it hangs, as it already did for the packet filter's own tables.
@@ -11,8 +11,9 @@ code:
- mesh-host internal/apply/apply.go
- mesh-tools src/main.ts
- mesh-catalog modules/mesh-catalog
updated: 2026-09-28
updated: 2026-10-02
decisions:
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
- 02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md
@@ -23,6 +24,7 @@ decisions:
- 02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md
- 02-DECISIONS/0135-a-module-version-prepares-its-state-before-it-runs.md
- 02-DECISIONS/0136-a-step-gates-its-module-not-the-machine.md
- 02-DECISIONS/0187-a-dead-tracker-is-not-the-machines-failure.md
- 02-DECISIONS/0134-the-mesh-says-what-it-applied.md
---
@@ -120,6 +122,12 @@ and a module consuming one event from two emitters could tell them apart only by
The subject already carries the emitter, so the key a module sees names it too — which makes a
disagreement between a manifest and the code a typo rather than a category error.
*2026-10-01 ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)):* where a name lands is now
*issued* to each assignment as a membership the controller publishes, rather than derived by a rule
the runtime carries; a module still declares only names, and gains one fact about itself — whether its
instances are interchangeable — which decides whether the mesh issues it the module's plain subject
beside its machine's.
## 2. Three namespaces, and nothing else
**Its own** — `mesh.mod.<module>.>`. Its events and its tools. Nothing else may publish into it,
@@ -463,6 +471,21 @@ moment the mesh can mint for itself: **the bus's own accounts** (§the bootstrap
needs an account before it can run) and **the vault's own credential**. Any third exception is a
design failure, and naming these two is what makes a third one visible.
## What a step is answerable for, 2026-10-02
[ADR 0187](../../02-DECISIONS/0187-a-dead-tracker-is-not-the-machines-failure.md). A step gates its
module and not the machine ([ADR 0136](../../02-DECISIONS/0136-a-step-gates-its-module-not-the-machine.md)),
but a step that exits non-zero still leaves the machine reporting that it is not doing what it was
told — and a clean apply gates other things entirely, the found firewall's retirement among them. So
what a step calls a failure matters beyond the step.
The rule the media step now follows, and the one to copy: a step fails for what the mesh chose and
can fix, and reports what it merely found and cannot. An indexer entry the mesh re-pointed at this
mesh's proxy is plumbing the mesh is answerable for; whether the public tracker behind it answers
today is not. An entry the operator listed is the operator's to insist on, and still fails. Six
hours of a machine reading as broken, for one tracker that had died, is what the distinction costs
when it is missing.
## 11. Open
**Semantic change has no mechanical defence** (§8). Recorded as open rather than solved, because
@@ -1,9 +1,14 @@
---
layer: to-be
status: designed
code: []
updated: 2026-09-28
status: implemented
code: [mesh-controller, mesh-tools]
updated: 2026-10-02
decisions:
- 02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md
- 02-DECISIONS/0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
- 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
- 02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
- 02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md
- 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
@@ -78,6 +83,16 @@ machine's holder and the holders' queue group would hand the call to whichever a
node-scoped seat's tool therefore carries the node it is asked of. Nothing about a mesh-scoped seat
changes.
*2026-10-01 ([ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)):*
the same shape now serves a **module's** tool on several machines, which had the queue-group fault
this section describes for seats: each instance also serves `mesh.mod.<module>.tool.<tool>.<node>`,
a caller writes `<module>.<tool>@<node>`, and every answer names the machine that gave it. And §3 is
built for every holder, not only the controller: the credential names the seats a module claims and
their verbs, the runtime serves each with the tool of the same name on the seat's subject, and the
bus admits it only where the module holds the seat. *Later the same day ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)):*
the subjects a holder serves, and a module's own, stop being derived in the runtime and are issued to
the assignment as a membership the controller publishes; the shape stays, the deciding moves.
## 5. Discovery
**What a role answers is a read.** The seats and their protocols are records, so the list is a query
@@ -102,6 +117,14 @@ being something a person carries and becomes something the mesh runs, on a node,
An agent's authority can then be role-shaped: *the forge's tools*, rather than a list of
module-specific names that changes the day the forge is replaced.
*Decided and designed on 2026-09-30:* the module is the console —
[ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md),
[34 — The console](34-the-console.md). It builds the second half of §5 now (a module's tools are
asked of the module, through a `tools` verb every runtime answers) and lists a role's tools when the
records carry them.
*Amended 2026-10-02 by [ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md):* the module that serves this to an agent is the node tools runtime — one per node, host-side, serving every assigned module's tools as well as answering the person on loopback. The console is its serving mode, renamed. See [37 — The operator's machine](37-the-operators-machine.md) §3.
## 7. Versioning
A seat's tools are an interface and change like one. Additive within a version. A change that would
@@ -120,6 +143,60 @@ by side until nothing is bound to the old one.
- **Two nodes holding one node-scoped seat derive two addresses.** Checked by the same test as the rest
of the subject table.
## What is built, 2026-09-30
Under [ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md): §2's
two constraints (the protocol in the store's row, seeded additively; a verb with description and
schema, a bare name still accepted), §3 for the mesh's seats (holding refused by naming the missing
verbs), §4 (a node-scoped seat's tool carries the node as its last subject token; a user publishes
`*`), and the third family — twelve verbs on the `mesh-controller` seat, each running the command it
names in the controller's own binary. §5's first half is served rather than read: the seat's `tools`
verb answers every seat's tools from the records, because the console cannot read the store; the
console lists a role's tools beside the modules' own and resolves `<seat>.<verb>` to the seat when the
seat declares that verb. Which verbs each *other* seat serves stays a decision per seat, still untaken.
## What shipped, 2026-09-30
mesh-controller PRs 166 and 167, mesh-tools PR 21. Verified on the live mesh the same evening: the
console on a workstation lists the twelve verbs as `mesh-controller.<verb>` beside 67 module tools, and
`mesh-controller.nodes` and `mesh-controller.status` answer through it with what the commands print.
Two things shipped bent. **The grant arrived after the holder started**: the controller composes the
bus's user list and a push delivers it, so the first controller to serve its seat subscribed before
the broker's list named the grant, the server refused all twelve subscriptions, and the client never
retried — a holder now rebinds a refused subscription every thirty seconds (PR 167), and a controller
roll-out that adds a grant is followed by a push to the broker node. **A JSON verb's answer was parsed
from both output streams**, so `status --json`'s warnings hid the document as data; the answer is now
parsed from standard output alone (mesh-controller PR 168, pending). The `output` field carried it
either way.
What stays as designed and not built: which verbs any *other* seat serves, and §3 for module-declared
seats' schemas beyond the names their manifests already list.
## The firewall seat's verbs, 2026-10-02
[ADR 0170](../../02-DECISIONS/0170-the-firewall-seat-serves-its-verbs.md). The first node-scoped seat
to carry verbs: `node-packet-filter` serves `rules` (the filter as the machine enforces it, nftables
and legacy), `reload` (the mesh's own filter from its file) and `remove` (one rule set the mesh did
not write, named as the host reports it under ADR 0168; refusing the mesh's tables, the runtime's
own chains, a built-in chain and an active found firewall's). Every holder serves all three; the
nftables module does so from a runtime on the machine's network with `NET_ADMIN`, which is the first
container to declare a capability. Removing a predecessor's rule set is an operator's act reached
through the seat, recorded on the bus, instead of a shell on the machine. *How it is checked:* ADR
0169's table.
## The intrusion seat's verbs, 2026-10-02
[ADR 0179](../../02-DECISIONS/0179-the-intrusion-seat-serves-its-verbs-and-every-door-declares-its-jail.md).
The second node-scoped seat to carry verbs: `node-intrusion-prevention` serves `status` (every jail
with what it watches and holds), `banned` (every address held now, with its jail and when the ban
ends), `ban` and `unban` (an operator's act on the live ban list). The fail2ban module serves them
from a runtime that carries only the daemon's client, the socket shared in from the machine — no
capability, no machine network, since the daemon on the machine does the banning. That runtime is the
per-module container [ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)
retires; the verbs and the client are the same code once the node's own runtime loads them as a bundle.
The module's own tool beside them reads one jail's effective settings. *How it is checked:* ADR 0179's table.
## What this does not settle
- Which verbs each seat should serve. That is a decision per seat, and the reason to do it slowly: a
+153
View File
@@ -0,0 +1,153 @@
---
layer: to-be
status: implemented
code: [mesh-catalog, mesh-tools, mesh-controller]
updated: 2026-10-02
decisions:
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
- 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
- 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
- 02-DECISIONS/0035-one-implementation-several-surfaces.md
- 02-DECISIONS/0034-the-local-account-owns-the-mesh.md
---
# 34 — The console
**The mesh's tools, on the machine a person sits at, served by a module the mesh assigned there.**
An agent reaches them over MCP on the machine's loopback; a person reaches the same endpoint. Nothing is
installed by hand, nothing is configured with an address, and the mesh knows the surface exists because
it put it there ([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)).
> **Amended 2026-10-02 by [ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md).** What this document describes stays true in substance and changes in form: the console becomes the serving mode of the node tools runtime, a host-side process the host supervises rather than a container, which also serves every assigned module's tools from their bundles. The module is renamed `node-tools`. [37 — The operator's machine](37-the-operators-machine.md) §3 is where it now lives.
## 1. What it is
A module, `mesh-console`, in the catalogue. Its image is the tool runtime's own — the client that
already speaks the bus as a command line and as an MCP server — started in a mode that reads the
module's credential and listens on loopback. It has no state, no provision, no seat. What it needs is
the bus, which it gets the way every module does: a credential the mesh minted for `<node>.mesh-console`,
sealed to the machine, delivered as the module's own secret.
*2026-10-02:* it gains one provision, at node scope — the MCP endpoint on loopback, serving the port
the machine gave it — so that a module whose software must be told where the console is requires that
and is coupled to an endpoint rather than to a module's name
([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)). The first
consumer is the operator's agent, [36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md) §6;
a machine without the console refuses such a module by name. Nothing else above changes: no seat, no
state, no tools of its own.
Its manifest says three things nothing else in the catalogue says together:
- `invokes: ["*"]` — it calls every tool on the mesh, and the bus grants exactly that publish side;
- `listens` on a port `from: machine` — the filter opens nothing for it, because loopback is not outside;
- no `emits`, no `consumes`, no `tools` — it answers nothing on the bus and nobody can address it there.
## 2. What it serves, and to whom
**One endpoint, `POST /mcp` on the machine's loopback**, speaking MCP over HTTP: `initialize`,
`tools/list`, `tools/call`. An agent on the machine is pointed at it once — the address is the machine's
own and never changes — and sees every tool the mesh can say it has. A person at a terminal uses the
same endpoint through the `mesh` client, or through anything that can make an HTTP request; the client
needs no credential, because the console holds it.
**The endpoint is the machine's login.** It binds `127.0.0.1` and nothing else. Whoever can connect is
on the machine, and whoever is on the machine is the account that owns the mesh there
([ADR 0034](../../02-DECISIONS/0034-the-local-account-owns-the-mesh.md),
[ADR 0144](../../02-DECISIONS/0144-anything-on-a-machine-may-call-anything-on-it.md)). There is no
token, no login page and no second identity, on purpose: a credential a person had to carry to reach
their own machine's console would be the arrangement this replaces, moved one hop.
## 3. How it knows what the mesh can do
Design [33](33-the-tools-the-mesh-answers.md) §5 splits discovery in two: a role's tools are read from
the mesh's records, a module's own are asked of the module. The console builds the second half now and
reads the first when it exists.
**Every tool runtime answers `tools`.** The runtime that serves a module's tools also serves one verb of
its own under that module's name, `mesh.mod.<module>.tool.tools`, answering the module's tool names,
descriptions and argument schemas — the definitions from the code that answers them, and from nowhere
else. A module may not name a tool of its own `tools`; the runtime refuses the collision at load.
**The console asks the catalogue which modules the mesh holds, then asks each.** `catalog_modules`
answers the roster; one `tools` request per module, in parallel, answers the list. The bus refuses at
once a request nothing serves, so a module that is not running costs nothing and is named in the answer
as not answering, rather than silently absent — *silence and success must never look alike*. The list is
kept for a short while and refreshed, so an agent asking on every turn does not fan out on every turn.
**Every module tool takes the machine to ask** (*2026-10-01*, [ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)):
an optional `node` the console lists on each one, puts into the subject and never hands to the module,
for a module that runs on several machines; without it whichever instance answers first does, and the
console appends *answered by <machine>* to every answer. A seat's verb takes none; the seat's scope
decides. *Later the same day ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)):* the console composes no
subject at all; each tool's subject comes with the listing, and a stateful module on two machines is
listed once per machine because the mesh issued it no plain subject.
**A tool that was not listed can still be called.** Listing is discovery; calling is the grant. An agent
that knows a tool's name asks for it by `<module>.<tool>` and the module answers or the bus says why not.
**What is missing from the list, and until when.** A role's tools and the mesh's own verbs — `status`,
`push`, `assign` — are the `mesh-controller` seat's under ADR 0132 and are not served yet; their three
prerequisites are listed in that record. When the seat serves them, the console lists them beside the
modules' own, and the person stops opening a shell for the mesh's own questions. Until then the console
says so in its handshake.
## 4. Where it runs
On whichever machines an operator sits at, by assignment. It is not on the control node by default and
does not need to be: it reaches the bus like any module, from anywhere in the mesh. A machine that is
not a node cannot have it, which is the right refusal — the mesh reaches what it declares, and a
workstation that wants the console joins first.
The person's credential and the `mesh` client (design [25](25-the-bus-on-nats.md) §7) remain the path
for a machine that is not a node, and the path to a mesh not yet far enough along to assign anything.
## 5. Removing it
Unassigning the console from a machine revokes its bus account at the next composition and stops the
container; nothing is left on the machine that could still connect. An agent pointed at the loopback
address gets a refused connection, which is the truthful answer.
## How it is checked
| Check | Defends |
|---|---|
| a module invoking one tool may publish that subject and no other tool's; `*` may publish every one; neither may publish an event or subscribe what it did not consume | ADR 0152, the grant |
| a module registering two tools answers three names to `tools`, with schemas; a module naming its own `tools` is refused at load | ADR 0152, discovery |
| against a real bus: two modules up, a third held and not running — the console lists the two and names the third as not answering | ADR 0152, silence is not success |
| a call through the console's endpoint reaches a module over the bus and the answer is the module's own, unshaped | ADR 0035, a surface decides nothing |
| on the live mesh: the console assigned to a workstation answers `tools/list` on loopback and a call to the forge returns repositories | the exit of work-order step 3 |
| the composed filter for a machine carrying the console opens no port for it | ADR 0144 |
## What shipped, 2026-09-30
Everything above, the same day: mesh-controller PR 164 (`invokes`, `module check`), mesh-tools PR 20
(`mesh serve`, the `tools` verb), mesh-catalog PR 181 (`mesh-console`). Verified on the live mesh: the
console assigned to a workstation answered `tools/list` on its loopback with 62 tools from the modules
whose runtimes had been rebuilt to answer `tools`, named 36 modules as not answering (modules that serve
no tools, and modules whose new runtime the mesh records rather than rolls out), and a `tools/call` of
the forge's `gitea_list_repos` returned repositories. An agent on that machine reaches it as an HTTP
MCP server and reports it connected. The mesh assigned the declared port unchanged, which is what a
machine with nothing else on it does; the console binds whatever it is given.
Two things shipped bent, both stated in [`00-as-is/13-the-console.md`](../00-as-is/13-the-console.md):
the person's client through the console (`--console`) exists and was exercised in the test suite, not
on the live mesh; and a module registered from the catalogue by hand recorded its source as a URL rather
than as a path on the git seat, because `--self` takes the forge path form — the rebuild-on-merge still
matched it by URL.
## What this does not settle
- Narrowing a console's grant per assignment. ADR 0046 makes it a setting; nothing reads one yet.
- The mesh's own verbs on the bus. Design 33's third family; this document only says where they appear
once they exist.
- A person's identity behind the console. The mesh sees the console's account; design 15 keeps the
question open.
## References
- [ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md) — the decision
- [33 — The tools the mesh answers](33-the-tools-the-mesh-answers.md) — what the console lists
- [25 — The bus on NATS](25-the-bus-on-nats.md) §7 — the person's client this makes a module of
- [issue 147](../../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md) — the symptom
@@ -0,0 +1,99 @@
---
layer: to-be
status: implemented
code: [mesh-catalog modules/records]
updated: 2026-09-30
decisions:
- 02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md
- 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
---
# 35 — Reading the record
**The design record, answered from a checkout the mesh keeps, at the commit it read.** A module,
`records`, holds a working copy of a repository of markdown — this one, for this mesh — and answers
where a phrase appears, what a document says, what a folder holds and where the copy stands. The
console lists those answers beside every other tool
([ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)).
## 1. What it keeps, and why that is not a copy
A git checkout, cloned from the forge that holds the `git` seat, brought up to date on every merge the
forge announces and every ten minutes besides. The bytes are the repository's; nothing is derived
from them and stored. What [ADR 0025](../../02-DECISIONS/0025-the-design-record-is-read-not-copied.md)
refused was a second store that is *searched* while the first is *edited*, drifting silently. A
checkout cannot drift; it can lag, and the lag is in every answer as the commit it was read at and
in `records_status` as when it was last brought up to date.
The checkout is reset to the origin on every sync, never merged: it is the mesh's, so a local change
is nobody's.
## 2. What it answers
| tool | answers |
|---|---|
| `records_search` | every place a phrase appears, as written and case-insensitively: document, line, the nearest heading above it; bounded, and says when it was |
| `records_read` | one document, whole, or its first part with a note when very long |
| `records_list` | what a folder holds: sub-folders and documents |
| `records_status` | repository, forge, commit and its date, last sync, document count, last error |
| `records_sync` | bring the checkout up to date now |
No ranking and no summary, on purpose: a record is found by its own words, and the reasoning is in
the document, not in the tool.
## 3. What it is told, and what it refuses to guess
Three things, none from a manifest ([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md)):
the directory its checkout lives in (a resource the mesh gives it), the forge's address (the `git`
provision's binding, written into a file as `scheme://host:port`), and the repository's path on the
forge (a **setting**, `{"repository": "<owner>/<name>"}`). Without the third it serves no tools and its
log says so. Public repositories only; it holds no credential.
## 4. How it is found
The console asks every module what it serves and lists `records_search` with a description that says
when to call it — *search the literal words of a symptom or a term before forming a hypothesis*. That
is 0025's second half in today's mesh: there is no store to be beside, and an agent choosing from a
tool list is the search.
## 5. Where it runs
Anywhere a node has the forge in reach; one assignment is enough, and a second on another machine is
harmless. The mesh session of design 15, when it exists, calls this rather than reading for itself.
## How it is checked
| Check | Defends |
|---|---|
| a phrase in one document of a repository the test makes comes back from that document, with the commit; a second commit on the origin is pulled and the next answer names it | ADR 0025's check, ADR 0153 |
| a path outside the checkout is refused; an empty search is refused | the reader reads the repository and nothing else |
| a sync against an unreachable origin leaves the checkout standing and says why | silence and success never look alike |
| live: through the console, `records_search` for a phrase that appears only in a design document here returns it | issue 006's closing check |
## What shipped, 2026-09-30
mesh-catalog PR 183, then PR 185. Verified on the live mesh the same evening: `records` assigned to the
control node with `{"repository": …}` as its setting, its checkout at the repository's `main` with 478
documents, its five tools listed by the console beside every other tool, and — ADR 0025's check —
`records_search` for a phrase from this document's title returned it from where it is written, with
the commit. The first live search missed: the phrase chosen from ADR 0025 straddled a line break under
emphasis, and the reader matched single lines. PR 185 matches a line together with the next and
ignores emphasis marks, which the module's test now covers; until it rolls, a phrase that wraps is one
to shorten.
The reader's `consumes` names the forge module's event rather than the `git` seat, because the seat
declares none; a merge into the repository was seen and pulled within seconds.
## What this does not settle
- Ranking or meaning. A search that understands a question is the session's job, not the reader's.
- A private repository. That is a credential the module would have to hold, and a decision about
what may read what.
## References
- [ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)
- [ADR 0025](../../02-DECISIONS/0025-the-design-record-is-read-not-copied.md)
- [34 — The console](34-the-console.md) — what lists it
- [issue 006](../../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md)
@@ -0,0 +1,212 @@
---
layer: to-be
status: designed
code: []
updated: 2026-10-02
decisions:
- 02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md
- 02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md
- 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
- 02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
- 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md
- 02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md
- 02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md
---
# 36 — The operator's agent on a machine: the `claude-code` module
**The agent a person runs at a terminal, put on the machine by the mesh, instructed by the mesh, pointed
at the console, and holding the licence the manager hands it.** It is a member of the family
[to-be 29 §2](29-a-node-has-operator-accounts.md) names, the modules that touch a person's machine, and
its counterpart is [39 — The Anthropic licence manager](39-the-anthropic-licence-manager.md).
What it replaces: the predecessor's module of the same name and a sibling, which placed six files under
the operator's home. The predecessor is retired; the six files are still on both workstations telling
every session to use tools that no longer exist.
**Three rules shape everything below.** The host is module-agnostic: it installs the package and gives
the module a state directory, and knows no vendor, no agent, no path under a home. The controller has no
part beyond resolving what it resolves for every module. And the module handles its own files: the
mesh's part of the agent's configuration is written by the module's own code, from what the mesh
delivered it and what the manager handed it.
## 1. Where the mesh's configuration lives: the agent's managed directory, not the home
The agent reads a machine-wide, administrator-owned configuration directory under `/etc`, documented
by the vendor: a managed settings file that outranks every user and project setting; a key in it that
adds HTTP tool servers *beside* a person's own without blocking them; and a managed instruction file every
session reads before the user's and the project's. The agent has **no** machine-wide directory for
rules, skills, slash commands or hooks; those exist only under a home or a project.
So the mesh's part of the agent's configuration lives there, **owned whole by the module**, and the home
is left alone. What the predecessor shipped as two rule files and two skills folds into the managed
instruction file and the manager's tools:
| the predecessor placed | becomes |
|---|---|
| `~/.claude/CLAUDE.md` | the managed instruction file: how a session on this mesh works (§3) |
| `~/.claude/rules/00-hal-mesh.md`, `~/.claude/rules/conventions.md` | sections of the same file: this node's identity, the repositories' conventions |
| `~/.claude/settings.json`, merged | the managed settings file: the mesh's keys only, outranking nothing a person did not also set |
| `~/.claude/skills/hal-switch-license/SKILL.md` | the manager seat's `switch` verb, listed by the console, and a sentence in the instruction file saying to use it |
| `~/.claude/skills/cleanup/SKILL.md` | nothing; it named the predecessor's forge |
| the console's entry in the agent's user-scope state | the managed settings' tool-server key, from the console's provision (§4) |
**The home.** Under [ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
every path under `~/.claude` is *found*, with one exception: the agent's credentials file, which the
module's own code writes for a subscription licence (§5). The person's memory, history, projects, local
settings, their own rules, skills and tool servers are never read or written by the mesh. **The six
predecessor files are the operator's to remove, once, on each workstation**; the module's documentation
lists them, and until they go the agent reads stale instructions beside the mesh's.
## 2. What the module declares and what its code writes
**Declared, applied by the host:** the agent's package (§7); the module's state directory; a facts file
in that directory carrying the node's name, the operator account, the console's endpoint, the module's
settings; the bus, the console's provision, and that it uses the `anthropic-licence-manager` seat.
Nothing under the home, nothing under `/etc`.
**Written by the module's code**, from the facts file and the manager's hand-over, whenever either
changes:
| path | content |
|---|---|
| the managed settings file | the mesh's keys: the tool servers (the console, plus any the operator declared as settings), the attribution trailers, and — for an API-key binding only — the key-helper that serves the key |
| the managed instruction file | §3 |
| the agent's credentials file under the operator's home | for a subscription binding only: the access token the manager handed over, as the operator, readable by the operator alone, atomic, no refresh token |
| the module's keypair in its state | made once, the private half never leaves (§5) |
Writing under `/etc` and as the operator under the home are two escalations the module's code performs
for itself; the mesh does not run the module as root for everyone, and the caller does not know
([ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)).
**Which settings keys are the mesh's.** A key is the mesh's when it encodes a rule of the mesh: the tool
servers that reach the mesh, the attribution convention of its repositories, the key-helper a binding
requires. The model, the spinner, the drafts and every other preference are the person's, and the
predecessor's experience with the model key is the evidence: a mesh that sets a preference reverts a
person's choice on every push.
## 3. What the instruction file says
Prose, not a paste; the file is the module's.
**How a session on this mesh works.** The console is the only path to the mesh, and its tools are the
vocabulary: the record is asked through the records module, symptom first — the literal error text before
a hypothesis ([ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md));
the mesh is asked and changed through the controller seat's verbs; the forge through the forge module's
tools; a licence through the `anthropic-licence-manager` seat's verbs, never by editing a file. The hard
rules in new words: a file the mesh manages is changed through the verb that owns it or through the
catalogue, never on disk; a store's database is never written by hand; main is never pushed; the mesh
creates no symlinks and nobody else does; a package is declared, not installed by hand. The glossary's
words, none of the predecessor's.
**Who this node is.** The node's name, from the facts file; the node's role, from the module's settings
on the node's layer; and that the other nodes are asked of the controller's `nodes` verb rather than
listed here, because a table is a copy that drifts.
**The repositories' conventions.** Concise commit messages in the imperative, about why; a branch, a
pull request and a human approval for every merge; test before pushing, because nodes update unattended;
the playbooks in the record.
## 4. The console
The module tells the agent where the console is, and the port is the console's to say. **The console
provides a node-scoped provision** — its MCP endpoint on loopback — serving the port the machine gave
it, and the module requires it. A requirement names what the consumer is coupled to
([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)); co-location
resolves it; a machine without the console refuses the module by name. [To-be 34](34-the-console.md) is
amended in the same change; issue 192 (open) found the gap.
**Other tool servers** a person wants on every machine, or on one, are a declared setting of this module
— mesh layer or node layer — rendered into the same managed key. A module tool, `mcp_configure`,
validates a server and sets the setting through the controller's settings verb, so the list stays
declared state. The agent's own HTTP-only constraint for managed servers applies; a person's local
command-based servers stay their own, in their own file.
**The entry's name is `mesh`.** The hand-made entry both workstations carry today is named after this
installation, which a definition may not be ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md));
it is the person's to remove, and until then the agent sees the mesh's tools twice.
## 5. The licence: the consumer side
[ADR 0183](../../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md)
decides it; to-be 39 is the manager's half. This module:
- **makes a keypair** in its state the first time it runs and registers the public half with the seat;
- **serves `apply`**: the manager's hand-over, a token sealed to the module's key, with the licence's
name and kind. A rotation of the same licence is applied only if newer within one lineage; a switch is
applied regardless, because across licences the expiries are unrelated. The answer says applied or
refused and why, and never echoes a token;
- **pulls** at start and when its token nears expiry, by the seat's `current` verb, and keeps the last
token when the manager does not answer, saying so;
- **writes** for a subscription licence the credentials file as the operator, access-token-only; for the
API-key licence sets the key-helper in the managed settings to a small program that prints the key
from the module's state, so no file under the home is touched;
- **offers a login to the manager**: when the credentials file changes by a person's login, it reads the
account's identity from the agent's state file and offers the grant to the seat, sealed to the manager's
key, for adoption; the manager decides;
- **serves `licence_status`**: which licence and kind this node holds, when the token expires, whether
the file matches what was handed over — by fingerprint, never by value.
Switching is the seat's `switch` verb, asked through the console; this module only applies what it is
handed.
## 6. Scope, settings and the order of assignment
**Every node with an operator account** ([ADR 0181](../../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)).
None has one today; the operator states them first. **Per node:** the role. **Per mesh or per node:**
extra tool servers. **Prerequisite:** the manager holds its seat and has adopted the licences.
**Order:** the manager assigned and a refresh observed; the console's provision in the catalogue; this
module on one workstation; the six predecessor files and the hand-made console entry removed there; a
new session read to confirm it sees the mesh's instruction file, the console's tools under `mesh`, and
its licence; then the rest.
## 7. The package
The module declares the agent's package. The distribution every node runs does not carry it in its
repositories: the two workstations have it from a build the predecessor's helper made from the community
repository, and nothing updates it since. On those two the declaration is satisfied. **On a fresh machine
the host's package manager refuses it, in its own words, and the module is not applied there.** The
answer is a package repository for this ecosystem as a seat
([ADR 0109](../../02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md)), fed by the builder
and trusted by every node's package manager; not built, and not this module's to build. The vendor's own
installer is rejected: it puts a self-updating binary under the person's home, invisible to the mesh.
## How it is checked
| Check | Defends |
|---|---|
| the module's definition names no node, path or login, declares nothing under a home or `/etc`, and no file resource carries a secret | ADR 0112, ADR 0155, ADR 0183 |
| on a lab machine with an account and a seeded home holding a person's rule file and the predecessor's leftovers: after assign, the managed directory holds the mesh's files, the home is byte-identical except the credentials file, which is owned by the operator and names no refresh token; after unassign, the managed directory's files are gone and the home is untouched | ADR 0182, the host's agnosticism |
| on a lab machine with no account, the assignment is refused naming the fact | ADR 0181 |
| a switch asked of the seat through the console changes the licence and the token on the node; no tool answer and no log line holds a token | ADR 0183 |
| the API-key binding writes nothing under the home and the agent authenticates through the helper | ADR 0183 |
| the console's provision resolves by co-location; a machine without the console refuses the module by name | ADR 0027, ADR 0152 |
| a new session on the assigned workstation lists the console's tools under `mesh` and answers "which node am I" from the instruction file | the exit of the build |
## What this does not settle
- **Several operator accounts on one node** (ADR 0181 decides one).
- **A worker's own licence on a machine.** Every interactive session shares the node's one agent
directory and its licence, however many run. A worker runs from a home of its own with an agent
directory in it, bound to its own licence through the manager (to-be 39 §5); that is for when workers
exist, and nothing here changes for it.
- **The package repository seat** (§7).
- **How the module's tools are run** is decided: the node's tool runtime, host-side
([ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)).
The managed files and the credential write are tools of this module that runtime serves. Until the
runtime exists on every node, the module's code runs as a supervised process of its own
([ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md)),
which changes nothing in what it writes.
## References
- [ADR 0181](../../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md),
[ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md),
[ADR 0183](../../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md) — the decisions
- [39 — The Anthropic licence manager](39-the-anthropic-licence-manager.md), [34 — The console](34-the-console.md), [29 — A node has operator accounts](29-a-node-has-operator-accounts.md)
- [research 018](../../01-RESEARCH/018-the-operators-machine-as-modules/00-overview.md) — the operator's machine as modules, and where tools run
- the vendor's documentation on managed settings, managed tool servers, the managed instruction file and the key-helper, read 2026-10-02
- the predecessor's two modules and the six files on the workstations, read 2026-10-02
@@ -0,0 +1,142 @@
---
layer: to-be
status: in-progress
code: [mesh-host, mesh-controller, mesh-tools, mesh-catalog]
updated: 2026-10-02
decisions:
- 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/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.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/0040-what-a-module-is.md
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
- 02-DECISIONS/0161-what-deserves-a-seat.md
- 02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md
---
# 37 — The operator's machine
**Every configurable thing on a node is a module, the home included, and the same catalogue serves
a server and a laptop.** One default configuration per module, varied per node by a setting or a
kept region; roles a machine has once as node-scoped seats with tool contracts; one tool runtime
per node serving every module's tools on the host side
([ADR 0173](../../02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md)
to [0177](../../02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md)).
This is the design [to-be 29](29-a-node-has-operator-accounts.md) §2 called *a family* and
[research 018](../../01-RESEARCH/018-the-operators-machine-as-modules/00-overview.md) measured.
## 1. What a module of the environment looks like
Worked on the first one, a shell. The `zsh` module declares:
- a **package**, `zsh`;
- **files under the home**, owned by the account: the shell's rc file with the module's default
configuration, carrying a kept region for the operator's own lines, and `${setting:…}`
placeholders for the few values a node varies; the account and its home are machine facts the
controller resolves ([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md),
to-be 29 §2);
- a **seat declaration**, `login-shell`, node-scoped, with its one verb; and a **claim** on it;
- a **`user` shape** naming the shell, applied only where the module holds the seat;
- a **tools bundle**, the artifact kind for interpreted code, with `execute` and the module's own
`show-config`.
No container, no unit, no service. It is assigned to every node with an operator account. The
`fish` and `bash` modules are the same with another package and other files; one of the three
holds the seat on each node.
The second shape is **system scope**: the login manager declares a package, two files under
`/etc`, and a service, which is exactly what the ssh daemon module declares today. The third
shape is **graphical**: the window manager declares a package, files under the home, a
user-scoped unit or two, a claim on the display-session seat, a dependency on the display server
being held, and a bundle with its tools. Nothing in any of them says which machine it is for.
## 2. Variation
A node differs from the default in two ways and no other
([ADR 0174](../../02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)):
a **setting** the module declared, set in the node's layer and rendered into the file; or lines in
a **kept region** the file marks. The predecessor's ninety theme variables become the settings of
the modules whose files read them. Until the settings record proposed alongside the
container-runtime records ships — a setting names the file it lands in — environment modules carry
defaults in their files and declare no setting; that is the order, not a preference.
## 3. The node tools runtime
One per node, started and restarted by the host as a sibling process, never a container
([ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)).
It is the tool runtime that exists, in the role it was written for: it reads the memberships of
every module assigned to the node, loads each module's tools bundle, and serves every tool and
every held seat's verb on the subjects issued. It holds the node's one bus credential and may call
every tool on the mesh. Its serving mode on the machine's loopback is what the console was
([to-be 34](34-the-console.md)); the module is renamed **node-tools** and declares the interpreter
it needs as a package.
A bundle reaches the node as any artifact does. A push that adds or replaces one is a reload. A
bundle that fails to load is named in the node's report and the others serve. A tool that needs
root escalates itself.
## 4. The seats of the environment
Decided now: **`login-shell`** (module-declared; zsh, fish, bash; verb `execute`) and
**`node-service-manager`** (the mesh's own; systemd; verbs over units in both scopes). The rest
are candidates from [research 018](../../01-RESEARCH/018-the-operators-machine-as-modules/04-the-seats-of-the-environment.md),
one record each when its first holder is written: display server, display session, terminal
emulator, launcher, notifier, compositor, lock screen, bar, login manager, audio, clipboard, boot.
Editors, browsers, media players, the agent, the downloads and scripts folders are modules with
tools and no seat.
A module that needs a role filled depends on **the seat being held** on the node, not on a
capability: the window manager needs the display server seat held, by xorg or by a compositor
that is its own server. Whether a held seat can gate an assignment is the first question the
resolver is asked by the second graphical module; the display server itself is gated by the
`graphical-session` capability the profile already reports.
## 5. What the host gains, and what it does not
- `service` gains `scope: user`, applied as the account
([ADR 0177](../../02-DECISIONS/0177-a-unit-may-be-user-scoped-and-the-service-manager-is-a-node-seat.md)).
- The host starts and supervises the node tools runtime as it would any host-side process, and
delivers bundles as artifacts.
- Nothing else. No hooks, no actions: `chsh` is the `user` shape, enabling a unit is the `service`
shape, rebuilding boot images is a verb of the boot seat when that seat is written.
- A gap, recorded: the `package` shape drives the distribution's package manager and nothing
outside its repositories. The login manager in use is such a package; it waits on an official
package or a decision the host does not yet have.
## 6. The order of the build
1. **The operator account on every node** — `mesh-controller node` with the login name; empty on
all four today. Nothing home-scoped composes before it.
2. **The node tools runtime** — mesh-host supervises it; mesh-tools serves bundles from memberships
and reloads; mesh-controller composes the bundle into the declaration and the memberships to one
runtime per node; the catalogue renames the console. Proven when the packet-filter verbs answer
from it and its container is gone.
3. **`zsh`**, the first environment module: seat, `user` shape, home files, `execute`. Proven on a
server first, then every node.
4. **`systemd`** and user scope: the host's field, the seat seeded, the module. Proven by the
desktop's reload watcher declared `scope: user` on a workstation.
5. **The login manager**, system scope, once its package is installable; then the display server,
the window manager, and the rest of the graphical stack, each seat its own record.
6. **Settings** for the theme knobs, after the settings record ships and issue 168 closes.
## How it is checked
| Claim | Checked by |
|---|---|
| A module with a package, home files, a seat and a bundle resolves and composes on a node with an account, and is refused on one without | the controller's composition tests |
| One runtime per node serves every assigned module's tools; a per-module tool container no longer exists | the runtime's tests; `docker ps` on a converged machine |
| A user-scoped unit is applied as the account | the host's tests |
| A node's difference from a module's default is visible as a setting with a source or a kept region | `mesh-controller.settings`; the host's write-into tests |
| The same manifests assign to a server and a workstation; the graphical ones are refused on the server by name | the resolver's tests and the live mesh |
## References
- [Research 018](../../01-RESEARCH/018-the-operators-machine-as-modules/00-overview.md)
- [To-be 29](29-a-node-has-operator-accounts.md) — the account and the home; this design is the
family its §2 names, beyond `~/.ssh`.
- [To-be 33](33-the-tools-the-mesh-answers.md), [to-be 34](34-the-console.md) — the tools and
the console, amended by ADR 0175.
- [To-be 05](05-the-node-host.md) — the host's vocabulary, widened by ADR 0177.
@@ -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@<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](../../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.
@@ -0,0 +1,170 @@
---
layer: to-be
status: designed
code: []
updated: 2026-10-02
decisions:
- 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
- 02-DECISIONS/0024-model-access-is-a-provision.md
- 02-DECISIONS/0050-model-access-is-vendor-agnostic.md
- 02-DECISIONS/0054-model-usage-is-recorded-at-two-grains.md
- 02-DECISIONS/0113-the-vault-makes-every-secret.md
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
- 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
---
# 39 — The Anthropic licence manager
**One module knows every Anthropic licence the mesh has, keeps each alive, decides which consumer gets
which, and hands every node's agent its token over the bus.**
[ADR 0183](../../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md)
decides it; this is the shape. It is the successor of the predecessor's manager module, built from what
that module learned the hard way, and the counterpart of [36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md),
which is the consumer on every node.
## 1. What it is
A module, `claude-licence-manager`, holding the mesh-scoped seat **`anthropic-licence-manager`**. One
holder, on the node the operator assigns it to — the control node is the natural one, and nothing in the
definition says so. It requires a database for its own store and the bus; it claims the seat; it serves
the seat's verbs. It has no port, no route, no file under anyone's home.
Its store holds four things:
| table | holds |
|---|---|
| **licences** | name, kind (`subscription` or `api-key`), the account's identity (id, address, organisation) once adopted, the grant encrypted at rest, when the access token expires, when the refresh token expires, consecutive failures, the refresh lease, when a person was last notified |
| **bindings** | one row per consumer: kind (`node-agent`, `node-session`, `worker`), its key (the node, or the node and the worker), the licence, or *inherit* |
| **usage** | the vendor's readings per licence per period, raw beside normalised ([ADR 0054](../../02-DECISIONS/0054-model-usage-is-recorded-at-two-grains.md)) |
| **audit** | every switch, adoption, refusal and drift, with who asked |
**The grants are encrypted with a key the vault made for the manager** — its one `secret` requirement.
The vault keeps that key; the manager keeps the grants. That is ADR 0050's carve-out, one module, one
node, the long-lived grants only.
## 2. The licences it manages today
Two subscription accounts and one API key. They differ in kind and the manager treats them so:
| kind | what the grant is | refresh | what a node is handed | how the agent uses it |
|---|---|---|---|---|
| `subscription` | an OAuth grant: an access token that lives hours and a refresh token that lives weeks | the manager rotates it, alone | the access token only | written into the agent's credentials file by the agent module, as the operator |
| `api-key` | a key the operator obtained from the vendor | none; a new key is a new adoption | the key | served to the agent through its key-helper setting; nothing is written under the home |
## 3. Keeping a grant alive
Carried from the predecessor, where each rule was earned by an incident:
- **One rotation source.** Only this module calls the vendor's token endpoint. An OAuth refresh is
presumed to rotate the refresh token, so a second refresher presenting the old one would kill the
grant; whether that presumption holds is to be measured in the lab, and the design is safe either way.
- **A lease per licence**, taken in the store before the row is read. A duplicate run sees the token its
predecessor just wrote, finds hours of life on it, and does nothing.
- **An expiry floor and a cadence.** Within an hour of expiry a refresh must happen; otherwise a grant is
rotated once it is older than a declared setting, so a node that misses one rotation still holds hours
of life and a broken refresh surfaces in minutes rather than the next morning.
- **Failure is counted and escalated once.** Consecutive failures are recorded; past a threshold a
notification is emitted, and at most once a day while it stays broken — the predecessor sent one alarm
411 times in 35 hours and the incident went unnoticed inside its own alarm.
- **A refresh token's own expiry is warned about three days ahead**, because the only remedy is a person
logging in again.
- **The vendor's reason is logged**, never only the status code: a malformed request and a revoked grant
both answer 400, and the predecessor built three concurrency fixes for a bug that was a wrong client id.
## 4. Handing a token to a node
Every node that runs the agent module registers that module's public key with the seat when it first
runs. From then on:
- **On rotation**, the manager calls `claude-code.apply@<node>` on every node bound to the rotated
licence, with the new token sealed to that node's module key. The module answers *applied*, or
*refused* and why, and the manager records it.
- **On a switch**, the same call with the other licence's token, and the binding is the authority: the
module applies a bind without comparing expiries, because across two licences the numbers are
unrelated.
- **On a pull** — the module starting, or finding its token near expiry — the module calls the seat's
`current` verb for its binding and is answered sealed the same way.
- **Never as an event.** What the manager emits names the licence and the outcome and carries no token.
A node whose module has not registered a key cannot be handed a token, and the manager says so by name
rather than falling silent. A node whose module refuses — a wrong identity, a stale grant within one
lineage — is recorded as drift and reported.
## 5. Who gets which licence
Three consumer kinds, the predecessor's touchpoints with their fallbacks:
| consumer | bound by | falls back to | if the bound licence cannot be served |
|---|---|---|---|
| **the node's interactive agent** | the node | nothing: an unbound node has no licence and the agent says so | keeps the last token, which expires within hours; a notification is emitted |
| **the mesh's session on a node** | the node, for that session | the node's agent licence | refused |
| **a worker** | the worker | the node's session licence, then the node's | refused: a worker never borrows a person's account |
**One agent directory per machine, shared by every interactive session**, so a node's binding is the
licence of all its sessions at once. A worker is a consumer of its own because it runs from a home of its
own, with its own agent directory and credentials file, which the agent module on that node writes for
it as it writes the operator's — the predecessor ran its agents exactly so.
**Binding is a person's act through the seat's verbs**, listed by the console: `bind`, `switch`,
`release`. **Exhaustion is observed, not acted on**: usage is read every few minutes, a crossing of a
declared threshold in the five-hour window is notified once per crossing, and moving a consumer is the
operator's call. Switching remains a reaction, not a declaration
([ADR 0024](../../02-DECISIONS/0024-model-access-is-a-provision.md)), and an automated policy — move to the
least-used licence, stay off a dying one — is designed later if wanted, on the readings this module
already keeps.
## 6. Adopting a grant
A licence enters the mesh one of two ways, and the token never passes through a prompt, a terminal or an
argument:
- **From a node's login.** A person logs in on a node, as they always have. The agent module there reads
the account's identity from the agent's own state file, and offers the full grant to the seat sealed
to the manager's key. The manager adopts it into the licence the node is bound to **only if the
identity matches** that licence's recorded account; a licence not yet identified is identified by its
first adoption; a mismatch is refused and notified, because the predecessor once filed one account's
grant into another's row this way.
- **An API key** is delivered to the manager by the operator through the seat's `adopt` verb from a file
on the manager's node, never as an argument.
## 7. What it emits and serves
**Events**, no secret in any: `licence.rotated`, `licence.switched`, `licence.adopted`,
`licence.failing`, `licence.refused`, `usage.read` — the audit logger records them all.
**The seat's verbs**, the contract every future holder must serve: `licences` (each with kind,
identity, expiry, failures, who is bound), `bindings`, `bind`, `switch`, `release`, `refresh` (now, one
or all), `usage` (current and history), `adopt`, `register` (a node's module key), `current` (a
consumer's token, sealed, asked by the consumer's module).
## 8. Settings
The refresh cadence; the usage threshold; the notification cooldown. Each declared with a default, so
one definition serves and one mesh may differ.
## How it is checked
| Check | Defends |
|---|---|
| two refresh runs started together rotate one grant once; the second does nothing and says so | ADR 0183, one rotation source |
| every event the manager emits is free of any token; the hand-over opens only with the receiving module's key | ADR 0183, to-be 32 §10 |
| a worker bound to a dead licence is refused, never answered with another licence's token | ADR 0183, the fallbacks |
| a grant offered with a mismatching identity is refused and one notification emitted | ADR 0183, attribution |
| a failing licence notifies once, and once a day after, not once per tick | §3 |
| the console lists the seat's verbs and `switch` changes a workstation's token end to end | ADR 0132, the exit of the build |
## What this does not settle
- An automated switch on exhaustion (§5).
- Whether an OAuth refresh token is single-use; the lab measures it, and §3 holds either way.
- How the mesh's own session and a worker read their token on a node once those exist
([to-be 15](15-the-agent-session.md), [ADR 0003](../../02-DECISIONS/0003-agents-are-persistent-employees.md)):
the agent module on that node is their local source, and the reading is theirs to design.
## References
- [ADR 0183](../../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md) — the decision
- [36 — The operator's agent on a machine](36-the-operators-agent-on-a-machine.md) — the consumer on every node
- [14 — Model access](14-model-access.md) — the vendor-blind provision this sits beside
- the predecessor's `claude-licences` module: the lease, the floor, the cadence, the cooldown, the identity guard — read 2026-10-02
+3 -1
View File
@@ -38,9 +38,11 @@ document is written and this one's status becomes `implemented`.
| [`26-the-seats.md`](26-the-seats.md) | **Proposed.** What a mesh can have one of, who fills each, and a seat's holder answering for the provision it delivers — including the `git` seat a build's source can live on | [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md) (superseding [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)), [ADR 0111](../../02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md), [ADR 0109](../../02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md) |
| [`27-a-module-requires-the-mesh-resolves.md`](27-a-module-requires-the-mesh-resolves.md) | **Proposed.** One concept for everything a module needs: a requirement with a contract, answered by one of four kinds of provider, resolved at assignment or refused. Retires settings, placeholders, facts and paths in definitions | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md), [ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md), [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md) (superseding [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)) |
| [`28-building-the-bus.md`](28-building-the-bus.md) | **Proposed.** The five steps of the bus work in the order their dependencies allow, each ending at a bed — with the surface measured, so no step's size is a guess | [ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md), [ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md), [ADR 0074](../../02-DECISIONS/0074-the-wire-is-specified-not-the-types.md) |
| [`29-a-node-has-operator-accounts.md`](29-a-node-has-operator-accounts.md) | **Proposed.** The mesh models machines but not the humans on them: a node gains operator accounts, and a resource may live under a home owned by its account — what would own ~/.ssh, dotfiles and ~/.config when HAL retires | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md) |
| [`29-a-node-has-operator-accounts.md`](29-a-node-has-operator-accounts.md) | **In progress.** A node has an operator account and a resource may live under its home — built in the controller; the ssh-client module, the SSH CA, the `~/.ssh` boundary and user-scoped services are not. The account fact still wants its decision record | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md) |
| [`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
@@ -1,9 +1,9 @@
---
status: located
status: resolved
opened: 2026-08-23
located-in: [hal, hq]
fixed-by:
amended-design: 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
located-in: [mesh-catalog modules/records, mesh-catalog modules/mesh-console]
fixed-by: ADR 0153; mesh-catalog PR 183 (records), PR 185 (a phrase that wraps)
amended-design: 03-DESIGN/01-to-be/35-reading-the-record.md
---
# 006 — This repository is not indexed into the knowledge base, and the claim that it is holds up a decision
@@ -169,3 +169,40 @@ them into. The record stays open, and its answer is no longer "index this reposi
is whatever the mesh grows as its own knowledge surface, if it grows one. Until then the honest fix
is the README, which should stop claiming a property nothing provides.
## Where this stands, 2026-09-30
The README no longer claims a property nothing provides: it says the indexing never existed, that the
store it named is unreachable since the cut-over, and that ADR 0025's answer — read, not copied, by an
agent that consults this repository — is decided and not built. That was the honest fix the previous
note asked for, and it is done.
The record stays open on ADR 0025's build, and on nothing else. The mesh's own operator surface is now
a module ([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)); the
reader ADR 0025 describes is the mesh session of design 15, which would answer through that surface
like any tool. What closes this is still the check 0025 names: search for a phrase that appears only in
a design document here, and get it back.
## Built, 2026-09-30
[ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md): the
reader is a module, `records` (mesh-catalog PR 183), keeping a checkout of this repository from the
forge and answering `records_search`, `records_read`, `records_list`, `records_status` and
`records_sync` at the commit it read; the console lists them beside every other tool, which is where
"beside everything else" lives in a mesh with no store. Design
[35 — Reading the record](../../03-DESIGN/01-to-be/35-reading-the-record.md). The module's test runs
0025's check against a repository it makes; this record closes when the same check passes through the
console on the live mesh, and says so below.
## Resolved, 2026-09-30
The check ADR 0025 names passed on the live mesh: through the console on a workstation,
`records_search` for a phrase that appears in one design document here returned that document and the
commit it was read at, from a checkout the mesh keeps and nobody copied. What this record asked on
2026-08-23 — *does the searcher find it without already suspecting it exists?* — is answered by where
the tool sits: in the same list as the forge's and the mesh's own, described as the thing to search
before forming a hypothesis. Reachable became surfacing when the surface became a list.
Open beside it, and not this record's: the mesh has no symptom-indexed memory at all since the
cut-over ([as-is 07](../../03-DESIGN/00-as-is/07-knowledge.md) says so), and the lessons of these
days are in this repository by hand.
@@ -1,8 +1,8 @@
---
status: located
status: resolved
opened: 2026-09-22
located-in: [mesh-controller internal/overlay, mesh-host internal/apply]
fixed-by:
fixed-by: ADR 0102 (mesh-controller internal/overlay: the runtime file written into, reloaded), issue 128 (the hosts file as a region)
amended-design: 03-DESIGN/01-to-be/05-the-node-host.md
---
@@ -56,3 +56,12 @@ and nothing checks for it today.
- Should an adopted node that cannot trust the registry be refused a module that needs to pull?
Or should the refusal come earlier, when the node joins?
## Resolved, 2026-10-02
The runtime's file is written into and the runtime reloaded, never restarted
([ADR 0102](../../02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md), the
diagnosis above); the hosts file is a marked region the mesh owns alone
([issue 128](../128-the-hosts-file-is-written-whole/00-report.md)). Neither whole file remains. Read
into [ADR 0168](../../02-DECISIONS/0168-a-converged-machine-is-filtered-by-the-mesh-alone.md), rule 5,
which names the one whole machine-wide file the mesh still writes — its own filter at the
distribution's path — as a difference a take shows, not a fault.
@@ -1,8 +1,8 @@
---
status: open
status: resolved
opened: 2026-09-22
located-in: []
fixed-by:
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
fixed-by: mesh-controller 201 (the preview names the narrowing and the port's reach), 206 (`take --yes <digest>` acts on the preview read)
amended-design:
---
@@ -35,3 +35,18 @@ it changes before it changes it, and for taking a module this one does not.
included, and ask for the same kind of confirmation as the flip?
- Or should taking refuse while a port of the module is reachable more widely than the module
declares, until the operator either changes the module's exposure or confirms the narrowing?
## Decided, 2026-10-01
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1 and 2: the preview names a narrowing. Building follows,
host first, then the controller's `take`.
## Built, 2026-10-02
mesh-controller 201 and the pull request after it: the preview names it, and `take --yes <digest>`
acts on the preview that was read. Stays located until a take is read on an adopted machine — every
machine of this mesh is converged today, so the record's live row has not been run.
## Resolved, 2026-10-02
Closed on the operator's decision of 2026-10-02 with the built and tested code live on every machine (mesh-controller 206, mesh-host 64), not on a take read on an adopted machine: every machine of this mesh is converged, so none holds a found thing to compare, and the record's live row — ADR 0163's last — will be read at the next real adoption rather than staged. Said here so nobody later believes that row was run.
@@ -1,8 +1,8 @@
---
status: open
status: resolved
opened: 2026-09-22
located-in: []
fixed-by:
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
fixed-by: mesh-host 64 (genesis raises the forge as `gitea`, on the module's image digest, with the module's data directory at /data; a test holds the installer to the module's manifest)
amended-design:
---
@@ -48,3 +48,22 @@ network, or it is not a takeover.
directory — so the module adopts it by the rule that already exists?
- Should something refuse to call a module the successor of a bootstrap service it cannot adopt?
- Is the forge's own address better resolved than set, which is [issue 088](../088-the-forges-own-address-names-a-port-it-may-not-have/00-report.md)?
## Decided, 2026-10-01
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rule 7: genesis raises as the module declares. Building follows,
host first, then the controller's `take`.
## Built in part, 2026-10-02
mesh-host 64: genesis raises the forge under the module's container name (`gitea`), pinned to the
module's image digest, with the module's data directory mounted at `/data` — so the module finds it,
holds it, and a take compares equal images and the same data. A test holds the installer's constants
to the module's manifest where the catalogue is checked out beside it. The network is the difference
left: the bootstrap forge runs on the machine's network to reach the store on its loopback, the module
runs bridged and publishes its ports, and the take says so. Closing waits for group 9's genesis test —
a mesh raised, the module assigned, and the module found holding rather than raising a second forge.
## Resolved, 2026-10-02
Closed on the operator's decision of 2026-10-02. Name, image and data directory align; the network does not — the bootstrap forge runs on the machine's network to reach the store on its loopback, the module runs bridged — and a take says so rather than hides it. Whether genesis should move the forge onto a bridge, and the test that raises a mesh and finds the module holding rather than raising a second forge, belong to group 9's genesis work and are not owed by this record any more.
@@ -1,8 +1,8 @@
---
status: located
status: resolved
opened: 2026-09-22
located-in: [mesh-catalog, mesh-controller internal/catalogue]
fixed-by:
fixed-by: ADR 0104 — the route adapter module (mesh-catalog modules/route-adapter) writes each migrated route into the predecessor's proxy; it runs on the home server's migration
amended-design:
---
@@ -71,3 +71,10 @@ answered by an **adapter** that writes into the predecessor's own configuration.
the predecessor's proxy keeps serving every name and keeps its certificates, while each migrated
module's name is pointed at the mesh's container. The proxy is the last cutover again, and by then
every route is one the mesh contributed.
## Resolved, 2026-10-01
The adapter ADR 0104 decided exists and runs: `route-adapter` provides `route` on an adopted
machine by writing each migrated module's route where the predecessor's proxy reads it, and the
proxy itself is the last cutover. [ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md)
records the rest of what a take compares.
@@ -1,8 +1,8 @@
---
status: open
status: resolved
opened: 2026-09-23
located-in: []
fixed-by:
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
fixed-by: mesh-controller (the pull request after 201: JudgeSettings, LeftOut), mesh-host 64 (left_out kept)
amended-design:
---
@@ -58,3 +58,17 @@ knowing the code.
an operator to undo it without reading the source?
- Is there anything a node must never be pushed without, such that sending a partial declaration is
worse than sending none?
## Decided, 2026-10-01
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rule 6: judged where stored; an impossible statement costs a module. Building follows,
host first, then the controller's `take`.
## Resolved, 2026-10-02
One judgement, in the catalogue, run where a setting is stored and where a machine is composed. Stored,
a setting that cannot compose with the module's current definition is refused naming the node, the
module, the layer and the key; a key that reaches nothing is refused there too. Composed, a definition
that moved under a stored setting leaves that module out of the machine's declaration — the envelope
names it, the host keeps what it holds and wrote for it, `plan` and `push` say it — and the machine is
told everything else. A stray setting no longer refuses the whole machine where it is read.
@@ -1,8 +1,8 @@
---
status: open
status: resolved
opened: 2026-09-23
located-in: []
fixed-by:
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
fixed-by: mesh-host 63 (former targets removed, strays reported), mesh-controller 201/202 (strays shown)
amended-design:
---
@@ -75,3 +75,16 @@ found, and so would be kept for ever on purpose.
module unassigned between the two declarations?
- What reports this? Nothing on the machine currently answers "what is running here that the mesh
did not ask for", which is the question that would have found this in seconds.
## Decided, 2026-10-01
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rule 5: former targets are removed and strays reported. Building follows,
host first, then the controller's `take`.
## Resolved, 2026-10-02
mesh-host 63: the host's record keeps a resource's former targets, removes a container or file it
wrote under a name the declaration no longer names, never what was found, and reports strays — what
runs on the machine that the mesh neither wrote nor holds. mesh-controller 201 and 202 show strays
on `node show` for an adopted and a converged machine alike; the live mesh reported four on the
control node the evening it rolled.
@@ -1,8 +1,8 @@
---
status: open
status: resolved
opened: 2026-09-23
located-in: []
fixed-by:
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
fixed-by: mesh-host 63 (the kept original's difference), mesh-controller 201 (shown; a differing file refuses unless `--replace <path>`)
amended-design:
---
@@ -63,3 +63,18 @@ written.
substitutes settings into content today.
- Is the kept original enough of an answer, given nothing restores it and nothing points at it
when the service starts behaving differently?
## Decided, 2026-10-01
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1 and 2: the difference is shown and a differing file refuses. Building follows,
host first, then the controller's `take`.
## Built, 2026-10-02
mesh-host 63 reports the difference between the kept original and the declared content; mesh-controller
201 shows it in the preview and refuses a differing file unless `--replace <path>` names it, or the
module declares the file partially. Stays located until a take is read on an adopted machine.
## Resolved, 2026-10-02
Closed on the operator's decision of 2026-10-02 with the built and tested code live on every machine (mesh-controller 206, mesh-host 64), not on a take read on an adopted machine: every machine of this mesh is converged, so none holds a found thing to compare, and the record's live row — ADR 0163's last — will be read at the next real adoption rather than staged. Said here so nobody later believes that row was run.
@@ -1,8 +1,8 @@
---
status: open
status: resolved
opened: 2026-09-23
located-in: []
fixed-by:
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
fixed-by: mesh-host 63 (both images' creation dates), mesh-controller 201 (DOWNGRADE said; refused unless `--downgrade`)
amended-design:
---
@@ -60,3 +60,18 @@ expected rate.
nothing answers the first.
- Is a digest pin the right thing for a module that takes over an existing service at all, or
should a cutover be able to say *keep what is running* and record what that was?
## Decided, 2026-10-01
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1 and 2: the images are compared by age and a downgrade refuses. Building follows,
host first, then the controller's `take`.
## Built, 2026-10-02
mesh-host 63 reports the found image and both images' creation dates; mesh-controller 201 says
DOWNGRADE and refuses unless `--downgrade` is said. Stays located until a take is read on an adopted
machine.
## Resolved, 2026-10-02
Closed on the operator's decision of 2026-10-02 with the built and tested code live on every machine (mesh-controller 206, mesh-host 64), not on a take read on an adopted machine: every machine of this mesh is converged, so none holds a found thing to compare, and the record's live row — ADR 0163's last — will be read at the next real adoption rather than staged. Said here so nobody later believes that row was run.
@@ -1,8 +1,8 @@
---
status: open
status: resolved
opened: 2026-09-23
located-in: []
fixed-by:
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
fixed-by: mesh-controller 201 (`secret accept --provider` reaches a required secret), 206 (a module's secrets listed with origin; a minted one for found data refuses unless `--mint <name>`)
amended-design:
---
@@ -65,3 +65,20 @@ the module can only be installed fresh.
Should it, so the dangerous case can be refused rather than discovered?
- What is the reverse path: the mesh has minted one, the service ignored it, and the working value
is still on the machine. Nothing reconciles those.
## Decided, 2026-10-01
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 2 and 3: a minted secret for found data refuses; secret accept reaches required secrets. Building follows,
host first, then the controller's `take`.
## Built, 2026-10-02
`secret accept <node> <module> <name> --provider <node>` reaches a required secret (mesh-controller 201).
The pull request after it reads every secret a module holds on a machine with its origin, and a take
of a module whose data was found refuses a minted, unaccepted one — naming the accept that carries
the existing value in, or `--mint <name>` to let the service take the new one. Stays located until
a take is read on an adopted machine.
## Resolved, 2026-10-02
Closed on the operator's decision of 2026-10-02 with the built and tested code live on every machine (mesh-controller 206, mesh-host 64), not on a take read on an adopted machine: every machine of this mesh is converged, so none holds a found thing to compare, and the record's live row — ADR 0163's last — will be read at the next real adoption rather than staged. Said here so nobody later believes that row was run.
@@ -1,8 +1,8 @@
---
status: open
status: resolved
opened: 2026-09-23
located-in: []
fixed-by:
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
fixed-by: mesh-controller 201 (the neighbours on a found network are named), 206 (the per-machine `networks` setting), mesh-host 64 (the taken container joins the kept network)
amended-design:
---
@@ -61,3 +61,19 @@ exercise.
learn to take a group atomically? Nothing takes more than one module at a time today.
- Does the same hole exist for anything else the predecessor's runtime resolves and the mesh's does
not — a network alias, a `depends_on`, a name in a shared `/etc/hosts`?
## Decided, 2026-10-01
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1 and 4: the neighbours are named; a found network may be kept by a setting. Building follows,
host first, then the controller's `take`.
## Built, 2026-10-02
The preview names every neighbour on a found network (mesh-controller 201). The pull request after it
adds the per-machine setting `networks` — a container id to the found networks it keeps — judged for an
adopted machine only, and mesh-host 64 has the taken container join each once it runs. Stays located
until a take is read on an adopted machine.
## Resolved, 2026-10-02
Closed on the operator's decision of 2026-10-02 with the built and tested code live on every machine (mesh-controller 206, mesh-host 64), not on a take read on an adopted machine: every machine of this mesh is converged, so none holds a found thing to compare, and the record's live row — ADR 0163's last — will be read at the next real adoption rather than staged. Said here so nobody later believes that row was run.
@@ -1,9 +1,9 @@
---
status: open
status: resolved
opened: 2026-09-23
located-in: []
fixed-by:
amended-design:
located-in: [mesh-controller cmd/mesh-controller/network.go (the placing command), internal/inventory/migrations/0004-the-overlay.sql (one hub)]
fixed-by: nothing to build — ADR 0161 rule 2; the second hub was already refused by name, and the private network's seat waits for ADR 0121's server and client modules
amended-design: [03-DESIGN/01-to-be/26-the-seats.md]
---
# 105 — The hub of the private network is a placement, not a seat
@@ -32,3 +32,16 @@ a node-scoped seat held by every node, which is true and not what was asked.
- Does the per-node seat still say anything once the hub is a seat, or is it the interface's
presence restated?
- What else in the mesh is "exactly one" and recorded as a placement rather than a seat?
## Resolved, 2026-10-01
Read against the code: the store has kept one hub since the overlay's first migration (a unique
index), and `overlay place <node> --hub` refuses a second naming the first. What this report saw as
silent is not. [ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md), rule 2, answers the
question that remained: a singular fact about machines is a placement with a capacity of one,
refused by name and named in the listing — never a seat, because a seat is held by a module
assignment and the private network is the host's own until
[ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)'s
server and client modules exist. That seat stands, deferred with the split it needs.
*How it is checked:* the overlay command's test for a second hub, and the index.
@@ -1,9 +1,9 @@
---
status: open
status: resolved
opened: 2026-09-23
located-in: []
fixed-by:
amended-design:
located-in: [mesh-controller internal/catalogue/seats.go (the seed lacks mesh-vault), mesh-catalog modules/mesh-vault/module.json (claims nothing)]
fixed-by: mesh-controller PR 192 (the seat row), mesh-catalog PR 205 (the claim; the vault's events renamed to its own)
amended-design: [03-DESIGN/01-to-be/26-the-seats.md]
---
# 106 — The vault claims no seat, so nothing refuses a second one
@@ -31,3 +31,19 @@ others were missed the same way — every provider added after 0079.
- A mesh-scoped seat `mesh-vault`, by the 0079 convention — is there any reason not to?
- Should a provider of a mesh-scoped provision be required to claim a seat, or say explicitly that
more than one is allowed, so the omission cannot recur?
## Decided, 2026-10-01
[ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md), rule 1: `mesh-vault` joins the mesh's
own set, mesh-scoped, delivering `secret`, and the vault claims it; a second provider is a second
claimant, refused by name. The record also answers the second question: a provision the mesh's own
code dereferences by name gets a seat, every other mesh-scoped provision may have several providers.
Design 26's *reserved* for `secret` named an effect no rule produced; corrected there.
## Resolved, 2026-10-01
`seats` on the live mesh lists `mesh-vault` at mesh scope, delivering `secret`, held by the vault on
the control node. A second provider of `secret` is now a second claimant and refused by name
(`CanHold`'s test). Found on the way: the vault's definition could not be rebuilt at all — it emitted
`secret.provisioned` and the like, which the builder reads as another module's events — so the events
are now the vault's own, `provisioned`, `rotated`, `deprovisioned`.
@@ -1,9 +1,9 @@
---
status: open
status: resolved
opened: 2026-09-23
located-in: []
fixed-by:
amended-design:
located-in: [mesh-controller internal/inventory, mesh-controller internal/artifacts, mesh-host internal/apply, mesh-catalog modules/distribution]
fixed-by: 02-DECISIONS/0189-the-store-keeps-what-the-records-name.md
amended-design: 03-DESIGN/01-to-be/18-building-a-module.md
---
# 108 — The registry has no garbage collection, and two doors make it harder to add
@@ -75,3 +75,32 @@ real thing services need, and the mesh cannot express one.
images by digest and moves by version — is retention "the digests no recorded build names"?
- Who owns the routine when the store and its public door are two modules — the store, since the
volume is its?
## Answered, 2026-10-02 — [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md)
The three open questions, answered:
- **A maintenance step, or a backend that does not need its writers stopped?** The step. A
scheduled container may name `while-stopped` — resource ids of **its own module's** containers,
which the host stops before the run and starts again after it whatever the step did. A storage
backend the mesh does not run would be a bigger thing to own than the mechanism it avoids, and
the mechanism is wanted anyway: a service that cannot have work done underneath it is a real
shape and the mesh could not express it at all.
- **Is retention "the digests no recorded build names"?** Nearly. An artifact stays because a
definition the mesh holds names it (no age limit), or because it belongs to one of the five most
recent successful builds of its module. Last-N-tags was the predecessor's rule for a registry
that knew nothing else; this mesh knows what each digest is for.
- **Who owns the routine now the second door is gone?** Both halves, each where it can be. The
**mesh** decides what may go — only it holds the records — and asks the store to drop it. The
**store** reclaims the bytes, because only it can stop its own server. Neither half can be done
by the other.
And the sharpened point — enabling deletion on a door with no accounts — dissolved on inspection:
**that door already accepts a push**, so a writer who can reach it can already replace any tag.
Delete takes nothing a push did not have. What it does not do is undo ADR 0082's bargain, which
putting an authenticated door in front of deletion would have.
The second registry process is not built, as the 2026-09-26 note says, so the shared blob cache
and the deletion-cached-by-the-other-door problem never arise. Plain `garbage-collect` is enough:
what the mesh keeps is still a manifest in the store, so `--delete-untagged` — the flag that would
delete images machines are running — is not needed at all.
@@ -1,8 +1,8 @@
---
status: located
status: resolved
opened: 2026-09-24
located-in: [mesh-host internal/apply]
fixed-by:
located-in: [mesh-controller internal/catalogue/declaration.go (every container was given the roster at creation)]
fixed-by: mesh-controller PR 161 — no container is given a mesh name; it resolves through its machine's resolver (ADR 0148, landed 2026-09-30 once issue 110 did)
amended-design:
---
@@ -73,3 +73,13 @@ copying: a container resolves through its machine's resolver at the moment it as
record reports then has nowhere to occur. It is gated on
[issue 110](../110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/00-report.md), so
until that lands the mesh still copies and still compares.
## Resolved (2026-09-30)
110 landed the same day ([its resolution](../110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/01-resolution.md)),
and mesh-controller PR 161 then removed the copy: no container is given a mesh name or a mesh address,
and a module's own declared entries are the only `host` lines it carries. Verified on the control-node
after its containers were recreated once — the last time a name will do that: the forge's container
carries no extra hosts and resolves another machine and a routed name through the machine's resolver,
so the shape this record describes has nowhere to occur. Checked in the controller's tests: a
container's declaration is byte-for-byte the same under a roster of one machine and a roster of three.

Some files were not shown because too many files have changed in this diff Show More