Compare commits

...
Author SHA1 Message Date
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 846c1f85f2 Merge pull request 'Issue 107 is resolved: a declaration carries its order' (#217) from issue/107-resolved into main 2026-09-30 12:13:57 +00:00
jschoubben 9eef0bd525 Issue 107 is resolved: a declaration carries its order
Hosts first, then the controller — a build and a push each, now that the
mesh delivers the host. The host refuses a lower sequence than it kept
and drains a batch by sequence rather than arrival; the controller
numbers each send under the node's hold, inside the signed bytes.

Measured: two pushes, sequence 2 in the kept declaration, counters in
the store agree, no machine reads as behind. That last one is the
subtlety: the mesh compares the digest of what it would send against
what it did, and a number changes the bytes, so the read-only comparison
composes with the last number sent rather than a fresh one.
2026-09-30 14:13:50 +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 6e08cdf3d6 Merge pull request 'Every machine self-updates, verified, and 107's gate has opened' (#216) from issue/142-self-update-on-every-machine into main 2026-09-30 11:51:53 +00:00
jschoubben 02f291a129 Every machine self-updates, verified, and 107's gate has opened
All four machines run a host the mesh built, published and delivered, the
last delivery unattended: each stood aside once for a genuinely newer
version and the delivered launcher started it. A following push that
delivered nothing new was applied and reported by every machine and stood
nobody aside.

The crossover needs one restart of the unit per machine, once, because
the running launcher executes from its own inode. Measured timing: three
seconds on the machine, 17-20 as the operator sees it, the difference
being the control plane composing before it sends.

107 is unblocked: a declaration field is now a build and a push.
2026-09-30 13:51:46 +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 9b14430d3f Merge pull request 'Issue 163: a delivered host stood aside on every push and reported nothing' (#214) from issue/163-a-delivered-host-stands-aside-on-every-push into main 2026-09-30 11:47:35 +00:00
jschoubben a4384f13d3 Issue 163: a delivered host stood aside on every push and reported nothing
Asked whether a newer host was delivered using the link-time stamp, which
every delivered host carries as 'development build' now that the version
comes from where the binary sits. Never matched, so it stood aside on
every push for ever; standing aside cancels the report, so the mesh never
heard from it. Read as healthy throughout.

The three-minute push wait made it invisible: a wait long enough to
absorb a whole apply is long enough to hide that the machine never
answered.
2026-09-30 13:47:28 +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 a841e2c173 Merge pull request 'The host self-updates, and an archive cannot be undeclared' (#212) from issue/161-resolved-and-162-an-archive-cannot-be-removed into main 2026-09-30 11:16:44 +00:00
jschoubben 3c535ead31 The host self-updates, and an archive cannot be undeclared
161 resolved and verified on a machine: the workstation runs a host the
mesh compiled, published, delivered and started, applying declarations
and reporting the version it was delivered as.

The system it was built for comes from the artifact — the one thing a
toolchain takes from a module, which 0142 already allowed because the
target is a property of the artifact. The version comes from where the
binary sits, which 0142 decided and nothing had implemented.

Two mistakes on the way, both caught by reading the output rather than
the line that claimed success. A second -ldflags does not merge with the
first: the binary gained its system and lost -s -w, 12.2MB against 8.5MB.
And the delivered binary was named after its package, so the first
delivery was correct, reported success and was invisible to the launcher.

A delivered host that cannot apply is a machine the mesh cannot repair,
because the declaration that would fix it is the one it cannot apply. The
launcher's fallback is what made that an inconvenience instead of an
expedition.

162 is new and not about the host: an archive has no removal, so a module
using one can never be unassigned, and the attempt takes the whole apply
with it — the machine applies nothing else either. It is how undoing the
first delivery froze the workstation.
2026-09-30 13:16:37 +02:00
jschoubben 4cf941d858 Merge pull request 'Self-update works, and a delivered host is one fact short of usable' (#211) from issue/161-a-delivered-host-has-no-link-time-facts into main 2026-09-30 10:28:08 +00:00
jschoubben 5042ffd8d3 Self-update works, and a delivered host is one fact short of usable
The loop closed on the workstation: the version landed, the launcher was
replaced, the running host stood aside, and after one restart the launcher
started a binary the mesh had compiled, published and delivered.

The launcher goes as a file resource rather than inside the archive, and
that is the safety rather than a preference. A file is written atomically,
so the running launcher keeps the inode it started from; an archive writes
in place with truncate and would cut a script a shell is reading. The
manifest carries a second copy and a test refuses any drift from the one
in packaging.

Then it would have refused the first declaration it was asked to apply.
The Makefile links in two facts the mesh's toolchain does not, on purpose,
and one of them is the system the host was built for — read before
anything is applied, so the failure is safe and total. Nothing reports it:
the unit is active, the bus link is up, and the log says it is hearing
what the node should be.

Worse, the declaration that would fix it is the declaration it cannot
apply, so the mesh cannot repair such a machine. Restored by moving the
delivered versions aside and letting the launcher fall back, which is the
fallback working as designed.

0142 already settles the version — it comes from where the component sits,
not from its linker — and that is unimplemented. The system pin has no
answer, and the candidates are a decision rather than a fix: put it in the
path too, carry it in a file beside the binary, or stop pinning at link
time at all, which is 0005's to change.
2026-09-30 12:28:01 +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
jschoubben 4f9dc3906e Merge pull request 'A machine says little about itself, and only when the mesh asks it something' (#209) from issue/160-what-a-machine-says-about-itself into main 2026-09-30 08:36:48 +00:00
jschoubben a2542e51f8 A machine says little about itself, and only when the mesh asks it something
Filed as a to-do. Nothing is broken by it: every machine here is amd64 and
reports so, and one architecture is enough for now.

When a machine joins, the mesh should collect what it reasonably can about
it and refresh that daily. It already asks what a machine can do; what it
is made of is the same question one level down.

More is already collected than it looks — eight capabilities, the links
that face outside, the host version, and on an adopted machine what it
holds, what is reachable and the firewall and tunnel it was found with.
The architecture and the kernel are in there too, and nothing reads
either: measured, all four machines report amd64 and linux, and node show
prints the capabilities beside them without printing them.

Missing: memory, disk, the processor beyond its architecture, the
distribution and its version, virtual or physical, cores, uptime. Several
are what somebody wants when deciding where a module goes, and the
placement code's own comment already imagines them.

Also missing: the refresh. A machine publishes after an apply, and the
five-minute reconcile publishes nothing, so the mesh's picture is as old
as the last push. Same mechanism 087 wanted.

This is the third thing in one day found to be collected and read nowhere,
after held resources and the host version. Whatever gets added should say
in the same breath which surface shows it, or it will be the fourth.

159 gains the note that the architecture is already reported, so matching
an artifact to a machine needs no new fact — only the comparison and a
compiler told what to target.
2026-09-30 10:36:40 +02:00
jschoubben b19b29cd3a Merge pull request 'An artifact's system is checked and then nothing uses it' (#208) from issue/159-an-artifacts-system-is-checked-and-ignored into main 2026-09-30 08:30:52 +00:00
jschoubben 4bf4fe2d06 An artifact's system is checked and then nothing uses it
Asked whether the host is built for more than one architecture. It is
not, and the reason is worse than a missing feature.

A bundle in a compiled language must name a system, must name one of
alpine, android or arch, and is refused with a careful message if it gets
that wrong. The field is then read by nothing: it does not reach the
compiler, no machine is matched against it, and nothing chooses between
two artifacts by it. The compile runs with no target named and produces a
binary for whatever the build machine happens to be.

The host is x86-64 because the build machine is, not because the
declaration said so. Correct for this mesh by coincidence — four machines,
all x86-64 Arch.

A module declaring two systems would get two identical binaries, both
published and both pinned, and the one sent to the machine it was not
built for would fail at exec. android is the sharp end: not an x86-64
platform, and an artifact declared for it today would be an x86-64 binary
wearing the label.

A field that is checked and ignored is worse than one that does not
exist, because the check is what persuades you it works.

Also noted: the processor is a second dimension the manifest has no word
for, so even implementing the present field would not answer the question
that found this. And since the Go toolchain builds statically, one binary
would run on all three systems anyway — so the pin is a policy rather
than a necessity, which is a decision and not a fix.
2026-09-30 10:30:45 +02:00
jschoubben 5e12081774 Merge pull request 'Issue 142: the mesh compiles its own host and publishes it to its registry' (#207) from issue/142-the-mesh-can-build-its-own-host into main 2026-09-30 08:05:12 +00:00
jschoubben 9ad64881a9 Issue 142: the mesh compiles its own host and publishes it to its registry
Both of the things ADR 0141's insight named as remaining are built. A Go
toolchain based on a new mesh-tools-go module, so the compiler is named
and not pinned; and ${version} in any value of a resource that uses an
archive or a bundle.

Measured rather than asserted: the mesh built the host through its own
toolchain, published it to its own registry, and the bundle fetched back
out is a statically linked stripped binary that runs and says it is the
host.

The cost was larger again than 0141's note said. Three more things in the
path assumed one language or one shape — an entrypoint became a .ts file
whatever the language, the output directory was the compiler's to create,
and a bundle was refused if it named what it is built from — and a fourth
was in the base image, which is Alpine where the first Dockerfile ran
apt-get. That last one is issue 136 in an image, and the build refused
rather than a module failing later.

The version in a path is the digest, not the commit: two builds of one
commit are the same bytes, so a content-addressed version keeps the path
an unchanged build already had.

Still nothing delivers a version to a machine. The host module declares no
resources, so the bundle sits in the registry and no machine is asked to
take it. 0141 carries the insight and 142 the account.
2026-09-30 10:05:04 +02:00
jschoubben 266ade6e28 Merge pull request 'Issue 087: what I shipped first said the opposite of the truth' (#206) from issue/087-a-commit-has-no-order into main 2026-09-30 07:29:53 +00:00
jschoubben a794ef9a3f Issue 087: what I shipped first said the opposite of the truth
It reported "N machines run an older host than another" by comparing
versions as strings. A host reports its version as a commit, and commits
have no order. On the live mesh it named the three machines running the
NEWER host as the ones behind — ced54d4 sorts above 04a27ca and that is
all it means.

The code even carried a caveat saying versions compare as strings and that
this "is enough for the timestamps and commits this mesh uses". That was
the error, written down and not noticed: enough for timestamps, meaningless
for commits, and the mesh reports commits.

It now reports the split and claims no ordering, which is more useful as
well as more honest — the reader sees who is on which side, and that is
what decides whether a field can be sent. Ordering is left with the host,
which would have to report something ordered for anybody to have it.

This is issue 145 arriving by my own door an hour after I closed it: a
report that confidently says the opposite of the truth is worse than one
that says less.
2026-09-30 09:29:40 +02:00
jschoubben e10084fed6 Merge pull request 'Issue 087: a machine states its host only when the mesh sends it something' (#205) from issue/087-a-machine-says-its-host-only-when-asked into main 2026-09-30 07:09:17 +00:00
jschoubben 941b920bd7 Issue 087: a machine states its host only when the mesh sends it something
Measured live. All four machines run the identical host binary — same
digest, installed within eighteen seconds — and at first only one reported
a version, which read as a difference where there was none.

A machine publishes a report after an apply. The five-minute reconcile
publishes nothing, because it is the machine keeping itself as declared
rather than answering anything. So a current, idle machine never says, and
the mesh cannot tell that from a machine running something ancient.
Confirmed by pushing: not reported, then 04a27ca.

Enough for the purpose, not enough for the claim. For deciding whether a
new declaration field is safe it is sufficient — pushing is what the mesh
is about to do, and the answer arrives with the act. For knowing what the
mesh runs it is not, and "not reported" is worded as "nobody has asked
recently" for that reason.

Making a heartbeat carry it would close the gap and would change what a
heartbeat is — a bare word that the node is there, deliberately carrying
nothing else. Left alone rather than widened in passing.
2026-09-30 09:09:10 +02:00
jschoubben ecbd2ce2e6 Merge pull request 'Group 1: 145's report states its scope, and 107 waits for delivery' (#204) from issue/145-and-107-what-group-one-leaves into main 2026-09-30 07:00:32 +00:00
jschoubben b7f7b97d8a Group 1: 145's report states its scope, and 107 waits for delivery
145, partly resolved. The sentence that was true for eleven hours of a
mesh in which no module could reach another now says what it is not a
claim about: that is the mesh and the machines agreeing, and nothing here
dials a provision. It checks nothing and does not pretend to — ADR 0146
decides the check and is deliberately not built. What changed is that the
report no longer implies otherwise. Stays open for that reason.

Carried forward: 0146's check needs an internal name fetched over TLS with
the certificate verified, and until today no machine trusted the mesh's
authority. Three of four do now, so whoever builds it does not have to
solve that first.

107, diagnosed and deliberately not built. The premise is confirmed in the
host's own words — unknown fields are refused because "a field the host
does not know is a thing the control plane believes it asked for" — so the
fix is a flag day, not an addition. 087 now makes the cost measurable, and
the measurement is why it waits: one machine of four runs an older host,
it is parked, and nothing delivers a host at all (142). Shipping the field
means hand-placing binaries and unparking a machine, and one missed in
that sequence is unreachable, not degraded. The fault it prevents has
never been observed.

142 gains the note that it is 107's gate, and that it is what makes a
declaration field cost a rollout instead of an expedition.

A judgement about order, not a refusal, and cheap to overrule.
2026-09-30 09:00:18 +02:00
jschoubben c2cf0d72d6 Merge pull request 'Issue 087 is resolved: the mesh knows which host runs a machine' (#203) from issue/087-the-mesh-knows-which-host-runs-a-machine into main 2026-09-30 06:55:05 +00:00
jschoubben 0a5006b366 Issue 087 is resolved: the mesh knows which host runs a machine
The machine has reported its host version since ADR 0141, whose own
comment says why it must: without it nothing can say a machine is behind.
The controller's copy of the report did not have the field, so it
unmarshalled into nothing and was thrown away on arrival. Two structs
describe one message and only the sending side had it.

node show names it per machine, "not reported" where the mesh has not been
told. status names every machine running an older host than another does,
and which is newest.

Disagreement rather than staleness, deliberately: nothing delivers a host
version yet, so the mesh holds no canonical current one and "behind" has
no fixed point. What it can say is that the oldest host in the mesh is
what the mesh may send.

Two refusals to guess: a machine that reported nothing is not called
behind, and versions compare as strings — right for the timestamps this
mesh uses, wrong for a scheme where 10 sorts before 9, said at the place
that would have to learn.

107 gains the note that this is what makes its new field safe to consider,
and that one machine of four is behind today, so it is not free yet.
2026-09-30 08:54:52 +02:00
jschoubben 3ea9601904 Merge pull request 'Issue 125 is resolved: a hold is a line in the report, and it breaks "all well"' (#202) from issue/125-a-hold-is-a-line-in-the-report into main 2026-09-30 06:47:57 +00:00
jschoubben f7a37ee4f3 Issue 125 is resolved: a hold is a line in the report, and it breaks "all well"
Two of the four surfaces the report named already carried it — the host
has reported Held since ADR 0100, and node show reads the machine's own
list with an `as of` beside it. Recorded as checked rather than assumed.

Two did not. The apply line counted what it applied and said nothing
about the difference; status read the mesh's take-time listing, so a
module assigned after it showed nothing at all.

Both now say it, and the part that carries the weight: a hold suppresses
"all doing what they were told, all heard from, running what the mesh
would send them". That sentence was true for the whole outage, and acting
on it is what stopped the predecessor's proxy. Being adopted still does
not suppress it — a mode somebody chose is not a half-finished action.

Status does not call a hold a fault, deliberately. It is correct
behaviour, and a reader trained to see red for something the mesh did
right stops reading.
2026-09-30 08:47:36 +02:00
jschoubben 601d004fcb Merge pull request 'Issue 129: three machines of four trust the mesh, not one' (#201) from issue/129-three-machines-not-one into main 2026-09-30 00:30:30 +00:00
jschoubben d009c3efef Issue 129: three machines of four trust the mesh, not one
Extended after the first was proven. Every converged machine now holds
the anchor and verifies an internal name with a plain client; before,
the two unassigned ones answered 'unable to get local issuer
certificate' and held no entry for the mesh.

ace is excluded on purpose: it is adopted, so a module assigned there is
held rather than run, which is right and is not trust.

Both the resolution and 0147's insight said one machine of four, which
was true for about twenty minutes.
2026-09-30 02:30:23 +02:00
jschoubben 5036b927b9 Merge pull request 'Issue 129 is resolved: a workstation trusts the mesh, and stops when told to' (#200) from issue/129-a-machine-trusts-the-mesh into main 2026-09-30 00:18:51 +00:00
jschoubben 1f72e82b84 Issue 129 is resolved: a workstation trusts the mesh, and stops when told to
Registered ca-trust from the catalogue — it was merged and had never been
registered, which is why "assign it to one machine" had no module to name
— assigned it to the workstation, and verified.

Verified in the form ADR 0147 prescribes, against the authority's own API
so the handshake needs nothing else in the mesh to be right: 200, issuer
Mesh Internal CA, Verify return code 0. Four routed internal names verify
too, and `git ls-remote https://…` works, which is the consequence the
report named.

Removal exercised for the first time. Unassign and push removes the
anchor, empties the trust store of the mesh's authority, and returns the
plain client to the original error; assigning again restores it. That is
the half 0147 claimed and nothing had shown.

One thing it found that is not in the module: removal works only because
the host removes the service before the script. Stopping the unit is what
deletes the certificate and refreshes the bundles, and it needs the script
to still exist. The symmetry rests on an ordering nothing states.

0147's "written, and not yet run" now carries a progressive insight
saying it has run, where, and that it ran on one machine of four.
2026-09-30 02:18:21 +02:00
jschoubben 76fbe323ea Merge pull request 'Issue 118 is resolved: it was issue 135, and umami is healthy' (#199) from issue/118-is-135-and-is-resolved into main 2026-09-29 23:54:39 +00:00
jschoubben 295bf2f3ab Issue 118 is resolved: it was issue 135, and umami is healthy
Verified on the machine: umami has zero restarts, applies all 26
migrations, reports the store up to date, and umami.novox.be answers 200
where it had answered 502 since 2026-09-25.

It was the same fault as 135, filed three days earlier and diagnosed
without either record noticing the other. 135's container IS umami — it
is named in 135's own evidence table, holding novox.internal:10.42.0.1
after the overlay range moved. The dial appeared to succeed and the first
real query timed out because the name pointed at an address that no
longer existed; the store, the path and the credential were all fine.

118's own reasoning is kept as a warning, because it is careful and
wrong: it argued path-MTU and conntrack, and it named the move that would
have found it — compare umami against a container created that week —
and did not make it. A stale name presents as a network fault, which is
why ADR 0148 stops copying names into containers rather than detecting
when the copies go bad.

Also: 118's located-in named mesh-catalog modules/umami, which was never
at fault; it names mesh-host's comparison now. And 135 gains the pointer
to 118, which is the back-reference I have now missed three times.
2026-09-30 01:54:19 +02:00
jschoubben e1b74810a8 Merge pull request 'Issue 129 is live and reproduced, and needs three steps rather than one' (#198) from issue/129-and-what-reproducing-it-found into main 2026-09-29 23:30:48 +00:00
jschoubben e41eed0852 Issue 129 is live and reproduced, and needs three steps rather than one
The certificate is genuine, from Mesh Internal CA, and nothing on the
workstation trusts it — verbatim the error the report gives. The public
name on the same proxy verifies cleanly, which puts the fault exactly
where the report puts it.

What is in the way is not an assignment. `ca-trust` is merged in the
catalogue and has never been registered with the mesh — 39 of 76
manifests are — so there is no module to assign. It dry-runs clean and
needs no artifact built.

Two findings from reproducing it, both their own issues:

157 — every routed name is published with an `.internal` alias that
nothing serves. The hosts file says keycloak.novox.be.internal; the proxy
serves keycloak.novox.internal and refuses the other by name. The first
three names I tried came from the hosts file and failed with a TLS alert
rather than a verification error, which pointed at a regression that had
not happened.

158 — the proxy re-logs all 52 routes every two seconds, 31 times a
minute. The one line that explained 157 sat between two of them.

Also recorded, because it was nearly filed as a defect and is not one:
step-ca publishes roots as /roots.pem, which is PEM, so ca-trust's fetch
and its refuse-a-non-certificate guard are both right. Its other endpoint
/roots returns JSON that contains the text the guard greps for, so the
guard is sound only because of which path is published.
2026-09-30 01:30:24 +02:00
jschoubben ba30286896 Merge pull request 'The pointers back from what yesterday's records changed, which I missed twice' (#197) from decision/the-pointers-back-from-what-these-narrow into main 2026-09-29 22:47:45 +00:00
jschoubben 53b94c51bb The pointers back from what yesterday's records changed, which I missed twice
Three records were left describing a mechanism a new record had moved,
and a reader arrives at them by following a citation: 0066 still said a
routed name is written into every container after 0148 replaced that with
resolution; 0016 still read as though the lab were the test bed after
0149; and issues 109 and 135 said nothing about 0148 ending the copying
that 135's own fix made comparable. Each was a citation leading to the
wrong answer in a record that was not wrong about anything it decided.

This is the second time in one session. The playbook rule I added last
round did not stop it, so the convention is now written where the record
conventions live, with the shape to use and three worked examples — and
with the honest note that it is NOT machine-checked and cannot be from
`extends:` alone: 102 records extend another, 87 have no back-reference,
and that is correct, because extending usually means building on a
context. Making it mechanical means a record declaring the relationship
in frontmatter, which is a schema change and is not mine to decide.

Also: designs 18 and 20 claimed `updated:` dates from before I edited
them, and 117's `fixed-by` gained the commit beside the record.
2026-09-30 00:47:19 +02:00
jschoubben 83791f0921 Merge pull request 'The four open design questions, answered: ADRs 0148, 0149, 0150, and 0114 accepted' (#196) from decision/0148-the-meshs-names-are-resolved-not-copied into main 2026-09-29 22:39:38 +00:00
jschoubben bf4d4e2e7b The three parked questions are answered: 0114 accepted, 0068 superseded, 117 settled
**0114 accepted.** The separation it draws — a consumer's resource and a
credential that reaches it are different things — is a data-loss rule,
and a record naming one should not sit unresolved while the code that
could hit it is written. Not built, and accepting it schedules nothing;
accepted-and-not-built is where 0141 and 0142 already are.

**0068 superseded by 0149, the live mesh is the test bed.** Never built,
and contradicted by practice that was written down nowhere but a handoff
note. The faults that cost the most are faults of a mesh that already
exists — bound consumers, containers made against an older roster, an
adopted machine — and a bed is by construction a mesh that does not.
Issue 156 settles it: that change was verified honestly, on the only path
where it cannot fail. The lab is not retired; raising a mesh from bare is
now its whole job.

**117 answered by 0150.** A module's own code runs as supervised
processes under the module's one account. 0047's argument was for a
runtime per module, not for a container — a unit satisfies it and runs as
an account. The invariant is the account, not the process count: its
worry was a second identity to scope and seal, and processes sharing one
account create none. Designs 18 and 20 now cite it and 0047 carries a
dated pointer, which closes the third disagreement — that neither design
knew the record existed.
2026-09-30 00:39:09 +02:00
jschoubben ec42ee0846 ADR 0148: the mesh's names are resolved, not copied into every container
Answers issue 151. Copying the roster into each container made the roster
part of each container's identity, so one name moving replaced every
container in the mesh — and it never stopped the staleness it was for,
since a copy taken at creation is stale the moment the roster moves (109,
135).

A container resolves through its machine's resolver instead, and nothing
is copied. Staleness stops being possible rather than detected, and a
name's blast radius becomes nothing.

Scoping each container to the names it binds was the close call and is
rejected: it contradicts anything-calls-anything, and leaves the roster
in the digest so the churn returns for a widely-bound name.

Gated on issue 110 — a container on the runtime's default network has no
DNS at all today. Removing the copy first reintroduces 109 and 135
silently on a live mesh. 151 stays open until the code lands; design 08's
file-not-resolver passage is narrowed to the machine's own roster.
2026-09-30 00:34:30 +02:00
jschoubben 5a3dee9e9e Merge pull request 'The records pointed at branches that no longer exist, and two fixes had no sequel' (#195) from issue/pointers-that-resolve-to-nothing into main 2026-09-29 22:29:02 +00:00
jschoubben 47909c6b71 The records pointed at branches that no longer exist, and two fixes had no sequel
Three issues named the branch that fixed them, and a branch is deleted
when it merges — so every `fixed-by:` was a pointer that resolved to
nothing by the time anyone followed it. They name commits and pull
requests now, and playbook 03 says to.

Two records were missing the thing a reader arrives for. 146 did not say
that one of its fixes crash-looped the control plane on a running mesh,
which is the whole reason the delivery subject carries the stream and the
raise path was the only one exercised. 151 did not say that 152 removed
the false reasons its roster moved, or that it stays open for the real
ones.

ADR 0080 enumerates what cycle.py enforces and named four things; it
enforces five. A progressive insight names the fifth — the decision
stands, the list had gone stale. The checks README and playbook 03 gained
the same rule, and 155 points at all three.
2026-09-30 00:28:35 +02:00
jschoubben 7e5edab8da Merge pull request 'Issue 156: the wider grant has landed, and the notes the fix printed' (#194) from issue/156-the-grant-has-since-landed into main 2026-09-29 22:19:35 +00:00
jschoubben 3649f82204 Issue 156: the wider grant has landed, and the notes it printed
Measured on the broker's own config after the fixed controller composed
it: every node is now allowed _DELIVER.<node>.> as well as the bare
subject. That grant was missing when this was diagnosed, which is why
re-making the consumers would have silenced every machine.

Also records the five consumers it kept and reported, including the build
machine's worker, which is bound the same way and was not anticipated.
2026-09-30 00:18:57 +02:00
jschoubben b400ce2c54 Merge pull request 'Issue 156: moving a consumer's delivery subject stops a running mesh' (#193) from issue/156-a-consumer-that-works-is-not-replaced into main 2026-09-29 21:57:00 +00:00
jschoubben 995c8cb266 Issue 156: moving a consumer's delivery subject stops a running mesh
146's change is correct on a mesh being raised and fatal on one that is
running: the server will not move a push consumer's delivery subject
while a subscriber is bound, and a node is bound to its declaration
consumer the whole time it is up. Reproduced before fixing; an earlier
version of the test unsubscribed first and passed against the broken
code.

The consumer that works is kept and the assertion says so. Not re-made:
the nodes are granted the bare subject only, so re-making would have
silenced every machine.
2026-09-29 23:56:31 +02:00
jschoubben f89aef992d Merge pull request 'Two records shared a number, twice, and every check passed' (#192) from issue/two-records-share-a-number-and-nothing-says-so into main 2026-09-29 21:39:09 +00:00
jschoubben b967ef7be3 Two records shared a number, twice, and every check passed
Two machines filing issues in the same hour both read main correctly and
both took "the next free number". main lags every open pull request —
seven that evening — so they collided twice. The second collision
reached main with records, cycle and index all reporting success.

cycle.py now refuses a tree where two issue folders share a leading
number, and names both. Proven by adding a duplicate and watching it
fail. The colliding records become 153 and 154, renumbered in the branch
that lands last, because renumbering a branch whose author is still
pushing only moves the race.

The check catches a collision; it does not prevent one. Taking a number
still means reading the open pull requests as well as main — issue 155
says so.
2026-09-29 23:38:48 +02:00
jschoubben e417906241 Merge pull request 'Issue 150: a machine's own network is not a reach' (#190) from issue/150-a-machines-own-network-is-not-a-reach into main 2026-09-29 21:37:41 +00:00
jschoubben b1bf895688 Merge pull request 'Issue 149: an adopted machine's data cannot be placed where it is' (#189) from issue/149-adopted-data-cannot-be-placed-where-it-is into main 2026-09-29 21:37:34 +00:00
jschoubben 0d64677c70 Merge pull request 'Issue 152: a node the mesh could not read withdrew its names from every machine' (#191) from fix/152-a-lookup-failure-is-not-an-absence into main 2026-09-29 21:37:16 +00:00
jschoubben 04205c1dc8 Merge pull request 'Issues 147 and 148: a route before its module is taken; a new name recreates every container' (#188) from issue/147-148-found-migrating-ace into main 2026-09-29 21:37:09 +00:00
jschoubben 9dc49cd831 Merge pull request 'Grooming: five issues were fixed and never closed, and one is not' (#187) from grooming/stale-issues into main 2026-09-29 21:37:04 +00:00
jschoubben 66b413a076 Merge pull request 'Issue 140 is resolved, and was resolved before it was read again' (#186) from issue/140-resolved into main 2026-09-29 21:36:57 +00:00
jschoubben fcba05fed9 Merge pull request 'Issue 146: a first node now enrols, and is enrolled twice' (#185) from issue/146-diagnosis into main 2026-09-29 21:36:42 +00:00
jschoubben 18f37c25b2 Issue 152: the loop is metastable, and it cleared at 23:11
It ran 22:46-23:11, five full replacements, and stopped when a pass
happened to read the store during a window it was up. The record said
nothing about it stops; that was wrong. Exiting by luck is the finding,
not a mitigation.
2026-09-29 23:27:51 +02:00
jschoubben 3c2b4fc6b6 Issue 152 is fixed: a lookup failure is no longer an absence
The three gatherers pass over a node whose set does not compose, and
raise anything else. 151 stays open: this removes the false reasons a
roster changes, not the fact that a real change still replaces every
container.
2026-09-29 23:26:20 +02:00
jschoubben 72eaf52867 Issue 152: a node whose plan will not compose withdraws its names from every machine
ace numbered its two records 147 and 148, which this repository already
uses; they become 150 and 151, as 127 became 149.

The new record is why the control node could not stop applying: the
roster alternates between two values because routeNamesInTheMesh
swallows a per-node plan failure, and the roster is part of every
container's identity. The loop closes through the control plane's own
store, which each pass replaces.
2026-09-29 23:13:54 +02:00
jschoubben 741625e725 Merge remote-tracking branch 'origin/issue/147-148-found-migrating-ace' into issue/the-roster-flicker-recreates-every-container 2026-09-29 23:12:57 +02:00
jschoubben c3730b9a23 Issue 150: a machine's own network is not a reach
ADR 0138's reach is internal, public or both. ace's LAN devices (an IoT
switch on mosquitto, unifi access points, plex clients) are none of them;
the only reach that admits them is public, which states the wrong thing and
opens the port to the internet on any machine with a public address.
2026-09-29 23:06:08 +02:00
jschoubben 3a59099c81 Issue 149: an adopted machine's data cannot be placed where it is
ADR 0112 decided that an assignment places a directory and says where an
access's data is; the control plane implements neither. ace's media stack
(a 40 TB ZFS library, plex state on a second disk) cannot be expressed
without writing ace's paths into manifests.
2026-09-29 23:03:11 +02:00
jschoubben 3bd6f34de3 Issues 147 and 148: a route before its module is taken; a new name recreates every container
Both found migrating the first module on ace (searxng), adopted with route-adapter:
147 — assign is not a no-op for a routed module: its route reaches the predecessor's
proxy while its container is held, and the name serves a dead backend until take.
148 — every push that moved the mesh's names replaced every container on novox, twice,
the control plane's own store included.
2026-09-29 23:01:51 +02:00
jschoubben ec8676c225 Issue 140 is resolved, and was resolved before it was read again
Written 2026-09-28, answered the same week by mesh-controller bdf965d and
c68d3a7, and never closed — so the mesh's own account said reach was declared
nowhere while three readers were reading it: the filter, the proxy's names, and
each authority's host policy. The resolution names them and what checks each.

What is left is retiring the older per-port keys the block replaces, which is
not a gap in what reach can say.
2026-09-29 22:19:24 +02:00
143 changed files with 7004 additions and 237 deletions
+3 -1
View File
@@ -54,4 +54,6 @@ whose failure has never been observed is a guess about its own correctness.
The development cycle, checked ([ADR 0080](../../02-DECISIONS/0080-the-development-cycle-is-checked.md)):
a to-be design names a decision, an in-progress/implemented design names its owning code, a
located/fixed issue names its owner, a fixed/resolved issue says what fixed it, a graduated
research overview says what it became. `python3 00-META/checks/cycle.py`
research overview says what it became, and no two issue records share a number (issue 155 — the
number is how a record is cited, and `main` lags every open pull request, so two people reading it
allocate the same one). `python3 00-META/checks/cycle.py`
+23 -1
View File
@@ -14,7 +14,8 @@ What is enforced:
its owning code (`code:`) -- no development without a design that says where.
issues a known `status:`; once `located`, `located-in:` names the owner;
once `resolved`, `fixed-by:` says what fixed it (prose counts --
"nothing, the capability existed" is an answer).
"nothing, the capability existed" is an answer). And no two records share a
number -- the number is how a record is cited.
research a known `status:`; a `graduated` overview says what it `became:`, and every
target it names exists.
decisions every accepted record is REACHABLE from the cycle: cited by a design doc's
@@ -109,6 +110,27 @@ def main():
"without a design that says where" % status)
# ---- issues ------------------------------------------------------------------------
# Two records may not share a number. Numbers are taken as "next free after main", and work
# sits on unmerged branches for days -- so two people reading the same main allocate the same
# number, and nothing said so. It happened twice in one evening between two machines, and the
# second collision landed on main with all three checks passing (issue 155). An issue number is
# how every other record cites this one; two records answering to it means a pointer that
# resolves to whichever the reader happened to open.
seen = {}
for folder in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", ""))):
name = os.path.basename(os.path.normpath(folder))
number = name.split("-", 1)[0]
if not number.isdigit():
continue
if number in seen:
bad(os.path.join("04-ISSUES", name),
"is numbered %s, and so is %s -- an issue number is how it is cited, and two "
"records answering to one means a citation that resolves to whichever the reader "
"opened. Take the next free number across main AND every open pull request"
% (number, seen[number]))
else:
seen[number] = name
for path in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md"))):
front = frontmatter(path)
if front is None:
+17 -2
View File
@@ -52,8 +52,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,6 +78,18 @@ 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
+14 -1
View File
@@ -21,7 +21,12 @@ incident someone must **clear**.
## Steps
1. Take the next free number. Create `04-ISSUES/NNN-short-name/00-report.md`:
1. Take the next free number — **across `main` and every open pull request**, not `main` alone.
Work sits on unmerged branches for days, so two people both reading `main` allocate the same
number; it happened twice in one hour between two machines, and the second collision reached
`main` with every check passing (issue 155). `cycle.py` now refuses two records sharing a number,
which catches a collision but does not prevent one. Create
`04-ISSUES/NNN-short-name/00-report.md`:
```yaml
---
@@ -42,6 +47,14 @@ incident someone must **clear**.
## Rules
- Closed issues are never deleted — they are the mesh's symptom-to-component memory.
- `fixed-by:` names something that will still exist: a commit or a pull request, never a branch. A
branch is deleted when it merges, so a branch name there is a pointer that resolves to nothing by
the time anybody follows it.
- A fix that turns out to have broken something else is written back into the record that asked for
it, pointing at the new issue. Somebody arriving at a record to learn why the code is the way it
is must not have to already know there was a sequel.
- Renumbering a collision happens once, in the branch that lands last. Renumbering a branch whose
author is still pushing only moves the race.
- An issue whose answer is a general lesson should also be written to the knowledge base, so
the next person searching a symptom finds it. Both, not either.
- `status: wontfix` is legitimate and requires a sentence saying why.
+8
View File
@@ -13,6 +13,14 @@ decisions taken over three days; the reasoning is kept, the fragmentation is not
The environment a change is run against before it reaches real machines.
> **Still the lab, no longer the test bed — 2026-09-30, by [ADR 0149](0149-the-live-mesh-is-the-test-bed.md).**
> Everything here stands. What changed is what the lab is *for*: a change is verified against the mesh
> that is running, because the faults that cost the most are faults of a mesh that already exists —
> bound consumers, containers made against an older roster, an adopted machine — and a bed is by
> construction a mesh that does not. Raising a mesh from bare is now the lab's whole job, which is the
> one thing the live mesh cannot be asked to do. 0149 also supersedes
> [ADR 0068](0068-the-lab-takes-requests.md), which extended this one and was never built.
## A node in the lab is a virtual machine
It boots a stock Linux image, runs the real install, and becomes a node. **It is not a model of
@@ -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
@@ -26,6 +26,14 @@ runtime is per-module, not per-node, and treating the audit-logger as special le
modules' code with nothing to run it: the conversion produced tools and events that, as it stands,
never execute.
> **The hosting form is settled elsewhere — 2026-09-30.** Where this record says "a container", read
> [ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md): a module's own
> code runs as supervised processes under this record's one account. Nothing else here changes — the
> per-module runtime, the per-tool key and the single scoped account are the argument this record made
> and they are why 0150 goes the way it does. The note is here because two design documents chose the
> other form without knowing this record existed
> ([issue 117](../04-ISSUES/117-a-modules-own-code-is-a-container-and-a-process/00-report.md)).
## Decision
### A module with tools or events runs a process of its own
@@ -80,6 +80,15 @@ reaching the routed name, which the clause above has just made resolvable inside
three are one decision: **compose the name, propagate it, certify it** — each is meaningless without
the one before it.
> **The mechanism changed — 2026-09-30, by [ADR 0148](0148-the-meshs-names-are-resolved-not-copied-into-containers.md).**
> A routed name still reaches every asker in the mesh, which is what this record decided and it stands.
> It no longer reaches them by being written into each declared container: copying the roster in made the
> roster part of every container's identity, so one name moving replaced every container in the mesh
> ([issue 151](../04-ISSUES/151-a-new-name-recreates-every-container-in-the-mesh/00-report.md)). A
> container resolves through its machine's resolver instead. The consequence below — that an internal
> issuer's challenge needs the routed name resolvable inside the mesh — holds unchanged, by the means the
> machine itself already uses.
## Consequences
- **Lab-versus-production is one node-level `public-domain` setting**, not an override on every
+8 -1
View File
@@ -1,14 +1,21 @@
---
topic: building it
status: proposed
status: superseded
date: 2026-09-12
deciders: jochen
reconstructed: false
extends: 0016-the-lab.md
superseded-by: 02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md
---
# 68. The lab takes requests, one at a time, and runs each from its own copy
> **Superseded — 2026-09-30, by [ADR 0149](0149-the-live-mesh-is-the-test-bed.md).** Never built. The
> live mesh became the test bed, because the faults that cost the most are faults of a mesh that
> already exists — bound consumers, containers made against an older roster, an adopted machine — and a
> bed is by construction a mesh that does not. The rule worth keeping from below is that a run reads a
> copy that is not anybody's working tree.
## Context
**The lab is exclusive hardware, and today a person holds it.** Raising a scenario takes over
@@ -35,6 +35,16 @@ The development cycle is enforced mechanically, to the extent frontmatter can ca
capability existed" is an answer).
- **No silent graduation** — a `graduated` research overview says what it `became:`, and the
targets exist.
- **No two records answering to one number** — added 2026-09-30; see the insight below.
> **Progressive insight — 2026-09-30.** The list above named four things `cycle.py` enforces, and
> now names five. Nothing enforced that two issue records hold different numbers: two machines
> filing issues within one hour both read `main`, both took "the next free number", and collided
> twice — the second collision reaching `main` with `records.py`, `cycle.py` and `index.py` all
> reporting success (04-ISSUES/155). A number is how every other record cites one, so two records
> answering to it is a citation that resolves to whichever folder the reader opened. `cycle.py`
> refuses it now. The decision here stands exactly as written: this is one more thing frontmatter
> and file names can carry, found by its absence rather than by reasoning.
[`00-META/checks/cycle.py`](../00-META/checks/cycle.py) refuses violations, beside `records.py`
and `index.py`; all three run before any merge here. What frontmatter cannot see — that code work
@@ -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 —
@@ -1,6 +1,6 @@
---
topic: what runs on it
status: proposed
status: accepted
date: 2026-09-26
deciders: jochen
reconstructed: false
@@ -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
@@ -189,6 +197,23 @@ On acceptance, each of these is amended by this record, not edited:
credential is no longer all-or-nothing with a window. It overlaps, with each step confirmed.
A single-party credential is staged, not replaced.
## Accepted, 2026-09-30, and not scheduled
Accepted as written. The separation it draws — a consumer's *resource* and a *credential that reaches
it* are different things with different lifecycles — is the part that had to be settled, because the
alternative is what the record was written against: retiring a credential taking the data it reached
with it. That is a data-loss shape, and a record that names it should not sit unresolved while the
code that could hit it is being written.
**It is not built, and accepting it does not schedule it.** The SDK's provisioner adapter is still
`create` / `remove` / `holds` rather than the four operations above, and no provider implements the
two-credential rotation. Accepted-and-not-built is an ordinary state here — 0141 and 0142 are both in
it — and it is the honest one: leaving this `proposed` made it invisible to anyone reading what the
mesh has decided, while changing nothing about what runs.
The work it implies belongs with the provisioner contract, beside
[issue 124](../04-ISSUES/124-a-consumer-cannot-be-told-what-its-provider-derived/00-report.md).
## Consequences
- **Every credential provider's adapter changes**, in two steps. The first separates *retire a
@@ -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.
@@ -109,6 +109,23 @@ understated as none. The remaining work is a way to build the host and a way to
path, and until both exist nothing delivers a version and every machine takes the fallback — which is
what every machine does today.
> **Progressive insight — 2026-09-30. Both of those exist now.** The paragraph above named two missing
> things and they are built: a Go toolchain, based on a new `mesh-tools-go` module so the compiler is
> named and not pinned, and `${version}` in any value of a resource that uses an archive or a bundle.
> The mesh compiles its own host and publishes it to its own registry, measured — a statically linked
> stripped binary, fetched back out and run. **The cost was larger again than this note said**: three
> more things in the path assumed one language or one shape, and a fourth was in the base image.
> The account is [issue 142](../04-ISSUES/142-the-host-is-the-one-thing-the-mesh-does-not-deliver/01-progress.md).
>
> The version in a path is the artifact's **digest**, not the commit this note's own wording would
> suggest. Two builds of one commit are meant to be the same bytes, so a content-addressed version
> means an unchanged build keeps the path it had; a commit-named one would move for an identical binary
> and recreate everything reading it.
>
> **Still nothing delivers a version to a machine.** The host is a module and builds, and declares no
> resources, so the bundle sits in the registry and no machine is asked to take it. That is the next
> piece, and the decision above is unchanged by any of this.
## Consequences
- **The host becomes a build target and a module** — a module whose resource is the next host, applied
@@ -113,6 +113,36 @@ the authority it was bound to, checked in the control plane's own test suite —
that verified anything. That is a weaker thing than the paragraph above describes, and it stays
written this way until the bed runs.
> **Progressive insight — 2026-09-30. It has now been run, on the live mesh rather than in the bed.**
> The paragraph above said nothing had verified anything, and something has. The module was registered
> from the catalogue, assigned to a workstation, and checked in the form this section prescribes — the
> authority's own API, so the handshake needs nothing else in the mesh to be right:
>
> ```
> $ curl -sS -o /dev/null -w '%{http_code}' https://<the authority>:9000/health
> 200
> subject=CN=Step Online CA
> issuer=O=Mesh Internal CA, CN=Mesh Internal CA Intermediate CA
> Verify return code: 0 (ok)
> ```
>
> **Both halves.** Unassigning and pushing removed the anchor, emptied the trust store of the mesh's
> authority, and returned the plain client to *unable to get local issuer certificate* — then assigning
> again restored it. The negative half is what distinguishes the anchor working from something else
> having trusted it, and it is the half nothing had ever exercised.
>
> **One thing this found that is not in the module.** The removal only works because the *host* removes
> the service before the script: stopping the unit is what deletes the certificate and refreshes the
> bundles, and it needs the script it calls to still exist. Nothing in the module states that ordering;
> the symmetry this record claims rests on it.
>
> Run on the live mesh because that is where a change is verified now
> ([ADR 0149](0149-the-live-mesh-is-the-test-bed.md)), and the bed still cannot raise a foundation. The
> evidence is [issue 129](../04-ISSUES/129-nothing-makes-a-machine-trust-the-meshs-authority/02-resolution.md).
> Extended the same day to every converged machine — `novox`, `g14` and `shanks` each hold the anchor
> and verify with a plain client. `ace` is excluded on purpose: it is adopted, so a module assigned
> there is held rather than run, which is right and is not trust.
## Consequences
The predecessor's authority can be retired from a machine once this module is assigned to it,
@@ -0,0 +1,176 @@
---
topic: the tiers
status: accepted
date: 2026-09-30
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0066-public-routing-is-name-agnostic.md
---
# 148. The mesh's names are resolved, not copied into every container
## Context
The mesh gives every container it declares the whole roster of mesh names as entries written into
the container's own hosts file at creation
([design 08 §2](../03-DESIGN/01-to-be/08-connectivity.md),
[ADR 0066](0066-public-routing-is-name-agnostic.md)). A container takes those entries once and never
looks again.
Three issues are the same fact arriving three times.
**A container keeps the address it was made with.** Adopting the predecessor's tunnel moved the hub's
private address; the declaration followed it within one push and nothing on the machine did. The
forge's container held the old address, lost its database, reported healthy while its existing
connections lasted, and then the public name went down
([issue 109](../04-ISSUES/109-a-container-keeps-the-address-it-was-made-with/00-report.md)).
**The same fault, four days later, undetected for five days.** One container had restarted 2286 times
against a database it could no longer find, while the mesh reported the machine as doing what it was
told. Inside it, `novox.internal` was an address that had not existed for five days
([issue 135](../04-ISSUES/135-a-containers-mesh-names-are-not-compared/00-report.md)). Forty-eight
other containers were current, none of them corrected — each had been recreated for some other
reason and picked up the roster on the way.
135 was fixed by putting the roster into the digest the host compares a container against, so a
container whose names moved is recreated like one whose image moved. **That made the roster part of
every container's identity**, which is the third arrival:
**One name moving replaces every container in the mesh.** Migrating one small module on one machine
took four routine actions; each changed the roster, and each replaced every container on the control
node — its own store, the registry, the edge proxy, the forge, the directory, mail. The control plane
was unreachable twice while its own store came back through crash recovery
([issue 151](../04-ISSUES/151-a-new-name-recreates-every-container-in-the-mesh/00-report.md)). None of
the replaced containers had anything to do with the module being migrated, or with its machine.
The blast radius of a name is now every container that carries the list, which is all of them. The
node-by-node migration ahead adds names one module at a time — on one machine alone that is around
twenty-five — and each would be a full restart of every service on the hub.
## Considered Options
**1. Keep the roster in every container and accept the churn.** Rejected. It is not a cost that can
be paid down: the mesh gets more names as it grows, and every name costs a restart of everything.
A rollback costs another.
**2. Scope each container's entries to the names it actually binds.** A container is given the names
of the things it declared a requirement on, so a name's blast radius is its consumers. Tidy, needs no
new mechanism, and keeps 135's guarantee exactly.
Rejected, and this is the close one. It contradicts the standing intent that **anything on the mesh
can call anything on it** — three cases, same machine, the private network, the public network, and
no fourth. Scoping resolution to declared couplings makes a name reachable only where the mesh was
told in advance that it would be wanted, and a person debugging inside a container would find names
missing that exist everywhere else on the machine. It also leaves the roster in the digest, so the
churn returns the moment a widely-bound name moves — smaller, not gone.
**3. Resolve at lookup time through the machine's resolver, and copy nothing.** Chosen.
## Decision
**A container resolves the mesh's names through its machine's resolver, at the moment it asks. No
mesh name and no mesh address is written into a container, and none is part of a container's
identity.**
The three consequences that make this worth doing:
- **Staleness stops being possible**, rather than being detected. 109 and 135 are not bugs that were
fixed; they are a shape that no longer exists. A name that moves is answered differently by the next
lookup, in every container, with nothing recreated and nothing restarted.
- **A name's blast radius becomes nothing.** Assigning a module on one machine does not touch a
container on another.
- **Anything can still call anything**, which option 2 gave up. The resolver answers every mesh name to
every asker on the machine, exactly as it answers the machine itself.
**The resolver is a machine-level process, not a container** — one of the modules that is not a
container at all — so a container depending on it is not the circularity it would be if the mesh's
own store had to resolve a name through something the store's own runtime had to start first.
**What a module declares for itself is untouched.** Entries a manifest asks for are the module's own,
stay in the container, and stay in its identity: they are part of what the module *is*, they do not
move when the mesh's roster does, and the mesh does not know what they mean.
**The machine's own roster file is untouched.** It is a file, rewritten in place, read by processes and
people; nothing restarts when it changes. It is only the *copy into each container* that this ends.
### The order this lands in, which is not a preference
**Nothing may stop copying names until resolution works from a container.** Removing the copy first
reintroduces 109 and 135 — silently, and on a live mesh, which is exactly how both were found.
1. **A container on any network can reach the resolver.** Today a container on the runtime's default
network asks from an address the converged filter drops, so it has no DNS at all
([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.
3. **Then, and only then, the roster leaves the declaration and the digest.**
Until step 3 the mesh keeps copying, and keeps comparing. 151 stays open until step 3 lands; it is not
closed by this record, only answered by it.
## How this is checked
- **A container resolves a name that moved, without being recreated.** Move a name the mesh serves;
from a container that was running before the move and has not been touched since, the name answers
with the new address. This is the one 109 and 135 would both have failed.
- **A name's blast radius is nothing.** Add a routed name on one machine; no container on any other
machine is recreated. The apply report on each machine says nothing changed. This is 151.
- **Anything calls anything.** From a container on any machine, every `<node>.internal` name and every
routed name the mesh serves resolves — including names the module never declared a requirement on,
which is the guarantee option 2 would have given up.
- **On every network the runtime offers.** The first three hold for a container on the runtime's
default network as well as one on a declared network, because the default network is the case that
has no DNS today.
- **No mesh name is in a container's spec.** A test asserts the digest a host computes for a container
does not move when the mesh's roster does, and does move when the module's own declared entries do.
## Consequences
- **The resolver becomes load-bearing for every container**, where before it was load-bearing for the
machine. This is a real cost and is accepted: a resolver that is down is a machine that cannot
resolve, which is already true of the machine itself, and is a smaller event than a roster change
destroying and recreating every container on the machine.
- **Design 08's "a file rather than a resolver" no longer describes containers.** It was written when
the mesh had no resolver and it gave the right answer then. The reasoning it rested on — every Linux
has a hosts file, no package needed — was already overtaken by names a hosts file cannot express:
service names and wildcards under `<node>.internal`, which is why the resolver was built.
- **ADR 0066's mesh-wide propagation is kept and its mechanism changes.** A routed name still reaches
every asker in the mesh; it reaches them through the resolver rather than by being written into each
container. The consequence 0066 records — that an internal issuer's challenge needs the routed name
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
person debugging in a hand-started container resolving the same names as everything else is the
behaviour worth having, and it is what "anything can call anything" means.
## References
- [issue 151](../04-ISSUES/151-a-new-name-recreates-every-container-in-the-mesh/00-report.md) — one name replaces every container; the question this answers
- [issue 135](../04-ISSUES/135-a-containers-mesh-names-are-not-compared/00-report.md) — the roster put into the digest
- [issue 109](../04-ISSUES/109-a-container-keeps-the-address-it-was-made-with/00-report.md) — the first arrival
- [issue 110](../04-ISSUES/110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/00-report.md) — the prerequisite
- [ADR 0066](0066-public-routing-is-name-agnostic.md) — routed names propagate mesh-wide; extended here
- [design 08 §2](../03-DESIGN/01-to-be/08-connectivity.md) — the file-not-resolver reasoning this narrows
@@ -0,0 +1,106 @@
---
topic: building it
status: accepted
date: 2026-09-30
deciders: jochen
reconstructed: false
supersedes: 02-DECISIONS/0068-the-lab-takes-requests.md
---
# 149. The live mesh is the test bed
## Context
[ADR 0068](0068-the-lab-takes-requests.md) proposed that the lab accept queued requests
— a bed and a commit — answer them one at a time from a copy it owns, and expose that through tools so
an agent could start a run and come back to it. It has been `proposed` since 2026-09-12 and nothing was
built.
What happened instead is that the mesh became the thing under test. It runs on four machines; every
fault worth finding in the last month was found on them, and none was found in a bed:
- a container holding an address that had not existed for five days, on the control node
([issue 135](../04-ISSUES/135-a-containers-mesh-names-are-not-compared/00-report.md));
- a machine reading healthy for eleven hours while no module could reach another
([issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md));
- one name replacing every container on the hub
([issue 151](../04-ISSUES/151-a-new-name-recreates-every-container-in-the-mesh/00-report.md));
- a consumer assertion that is correct on a mesh being raised and fatal on one that is running
([issue 156](../04-ISSUES/156-moving-a-consumers-delivery-subject-stops-the-control-plane/00-report.md)).
The last is the one that settles it. That change was exercised on the raise path — which is what a bed
*is* — and the raise path is the only path on which the fault cannot appear. A bed raises a mesh; it
does not have a mesh that has been running for weeks, with consumers already bound, containers created
against an older roster, and an adopted machine carrying a predecessor's configuration. **The faults
that cost the most were all faults of a mesh that already exists**, and a bed is by construction a mesh
that does not.
Lab runs are also expensive in a way that changed the behaviour around them: each costs a build and
several minutes, so they were batched, and a batched test is one whose result arrives after the next
three changes were already written.
## Considered Options
**1. Build 0068 as proposed.** Rejected. It answers a question nobody is asking: the bottleneck was
never that a person had to sit at the lab, it was that a bed cannot hold the state the faults live in.
Queueing and tooling a mechanism that finds the wrong class of fault faster is not an improvement.
**2. Leave 0068 `proposed`.** Rejected, and it is why this record exists rather than nothing. A record
that contradicts current practice and sits unresolved is worse than either answer: it reads as intent
to anyone who finds it, and the practice it contradicts is written down nowhere but a handoff note.
**3. Record that the live mesh is the test bed, and supersede 0068.** Chosen.
## Decision
**A change is verified against the mesh that is running.** Not because a bed would be unwelcome, but
because the state that breaks things is state a bed does not have: containers made against an older
roster, consumers already bound, an adopted machine, a store with weeks of history.
**A change that can only be exercised on the raise path is not verified.** If the only test available
raises a fresh mesh, the record says so, and says which case was therefore not covered. The words
"exercised on a fresh mesh" are a statement about coverage, not a pass.
**The lab is not retired**, and [ADR 0016](0016-the-lab.md) stands. It remains the place to raise a
mesh from bare, which is the one thing the live mesh cannot be asked to do and the one thing a bed does
better than anything else. What this record removes is the lab as the *default* answer to "is this
change good", and with it 0068's queue, tools and request protocol.
**Accuracy over a green run.** An honest failure on the live mesh beats a pass in a bed that could not
have failed — and a change that is risky on the running mesh is a reason to make the change smaller,
not a reason to test it somewhere it cannot break.
**What 0068 got right is kept as a rule, not a mechanism:** a run reads a copy that is not anybody's
working tree. Every run of the lab that mattered was pinned to a checkout rather than a worktree, and
the ones that were not produced results about code nobody had written down.
## How this is checked
- **A record that says a change was verified says on what.** Where it was a fresh mesh, it says which
case is uncovered. This is the clause that would have caught 156: its change was verified, honestly,
on the only path where it works.
- **The lab is not in the path of a merge.** No check, playbook or handoff requires a bed to have run.
- **0068 is unreachable as intent.** Its status is `superseded` and it names this record, so a reader
arriving at the queue design finds out immediately that it was not built and why.
## Consequences
- **A fault can be introduced on the machines that serve.** This is the cost, it is real, and it was
paid twice in one evening — a control plane crash-looping for half an hour, and every container on the
hub recreated five times. Both were found in minutes because they were live, and both would have
passed a bed.
- **There is no pre-merge gate beyond the repositories' own suites.** `make check` and the three hq
checks are what stands between a change and the machines, which raises what those suites are worth
and makes a test that cannot fail a genuine defect rather than an untidiness.
- **Raising a mesh from bare is now the lab's whole job**, and is exercised deliberately rather than
as a side effect of testing something else. The foundation work
([issue 146](../04-ISSUES/146-the-foundation-cannot-be-raised-on-the-bus-the-mesh-runs-on/00-report.md))
is that job, and it is also the proof that the mesh can make another of itself.
- **An agent cannot hand a run to a queue and come back**, which 0068 would have given. In practice it
watches a push and reads the machines, which is what happened anyway.
## References
- [ADR 0068](0068-the-lab-takes-requests.md) — superseded by this
- [ADR 0016](0016-the-lab.md) — the lab, which stands
- [issue 156](../04-ISSUES/156-moving-a-consumers-delivery-subject-stops-the-control-plane/00-report.md) — correct on the raise path, fatal on a running mesh
@@ -0,0 +1,113 @@
---
topic: what runs on it
status: accepted
date: 2026-09-30
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md
---
# 150. A module's own code runs as supervised processes under the module's one account
## Context
The repository answers "what runs a module's own code" two ways and reconciles them nowhere
([issue 117](../04-ISSUES/117-a-modules-own-code-is-a-container-and-a-process/00-report.md)).
[ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md) is accepted and
says **a container** — "the tool runtime carrying that module's compiled code" — and "one module, one
process, one account". Two `proposed` design documents say a **`process`** resource running an argv,
supervised by the machine, and one of them declares *four* of them for a single module and presents
four as the point. Neither design document names 0047 in its `decisions:`, and the string `process`
as a resource type appears in no decision record at all. The thing as built is the container.
Two things have happened since 0047 was written that bear on it directly.
[**ADR 0142**](0142-the-mesh-delivers-its-own-components-as-binaries.md) decided that the mesh's own
components are binaries on the machine rather than container images, and
[issue 114](../04-ISSUES/114-should-the-controller-be-a-container-or-a-process/00-report.md) was closed
by it. That settled the mesh's components and deliberately said nothing about a module's.
And the standing definition of a module hardened: **a module is software that delivers one or more
services, and a module is not a container.** It may deliver them as a container, an installed package
with a unit, a binary, or configuration files; 61 of 73 happen to use a container and 11 do not,
including the resolver, sshd and fail2ban. A rule that a module's *own code* must be a container makes
the one kind of module the mesh writes itself the only kind that has no choice.
## Considered Options
**1. Hold 0047 as written: a container.** Rejected. Its own reasoning does not require one. What 0047
argued for was a runtime **per module** rather than one for the whole node, because a node-wide runtime
could not hold a per-module broker account and per-module runtimes competing on one tool key would each
be handed calls for tools they do not have. A supervised unit per module satisfies that argument
exactly — it is per module, and a unit runs as an account. The container was the mechanism to hand, not
the conclusion.
**2. Let each design document choose.** Rejected; that is the present state and it is what issue 117
reports. A module author reading the guide writes four processes; a module author reading the record
writes a container; nothing tells either that the other exists.
**3. Settle the hosting form as a supervised process, and settle the count separately.** Chosen.
## Decision
**A module's own code runs as one or more supervised processes on the machine, under the module's single
account.** Where [ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md) says
"a container, the tool runtime carrying that module's compiled code", read this record. Everything else
0047 decided stands untouched: a tool is served on its own key, only the module that serves it answers,
and the module's account is scoped to exactly its tool keys.
**The invariant is the account, not the process count.** 0047's "one module, one process, one account"
carried its weight in the last clause. Its stated worry about a second process was "not a second one to
scope and seal" — a second *identity* to grant, seal a secret to, and scope on the bus. Several
processes sharing the module's one account create no second identity, so nothing further is scoped or
sealed, and a module may therefore declare as many as its work has shapes: events, tools, a
provisioner, a scheduled ingest. **What a module may not have is two accounts.**
**A module that delivers its service as a container still does.** This record is about the code the
module itself carries — its tools, its events, its provisioner — and not about the software it delivers.
A module wrapping a third-party image wraps a third-party image.
**Why supervised by the machine rather than by the mesh:** it is the same answer ADR 0142 gave for the
mesh's own components, for the same reason. A unit the machine restarts needs no image, no registry
pull and no runtime to be up before the mesh's own code can run — which matters most for exactly the
modules whose code the mesh cannot start any other way.
## How this is checked
- **No design document describes a hosting form for a module's own code without citing this record.**
Designs 18 and 20 name it in `decisions:`; this is the gap issue 117's third point reports, and
`cycle.py` already enforces that a to-be design names its decisions.
- **A module declaring several processes resolves to one account.** A test composes a module with more
than one process resource and asserts the mesh mints exactly one broker account for it, scoped to that
module's tool keys and nothing else — which is 0047's invariant stated as an assertion rather than a
sentence.
- **A module's own code does not require the container runtime.** A machine with no container runtime
can still run a module whose code is its own, which is the claim that separates this from option 1 and
is checkable on a machine that has one by asserting the declaration names no image for it.
## Consequences
- **The sidecar port stops being needed.** [ADR 0029](0029-a-network-is-a-shape-because-an-action-cannot-be-undone.md)
records that "anything that is a service plus a sidecar currently has to publish a port to talk to
itself", and the host's `network` shape exists partly for it. A process beside the service on the same
machine reaches it without publishing anything, so that pressure goes.
- **Something must supervise, and it is the machine.** This adds a unit per module's code to what the
host writes and owns. The mesh already writes and owns units — `nftables` proves a module can write one
and run it — so the mechanism exists; the count grows.
- **A module's code is delivered, not pulled**, which puts it behind the same gap as the host's own
delivery ([ADR 0141](0141-the-host-delivers-its-own-successor.md), not built): nothing yet delivers a
version of a module's binary to a machine. A container's code arrives by `docker pull`, and this does
not. **This is the cost of the decision and it is not paid**; until delivery exists, a module whose code
is its own is a module somebody places by hand.
- **Issue 117 is answered and its three disagreements close differently:** container-or-unit is decided
here; one-process-or-several is decided here as several under one account; and whether the record was
consulted is fixed by designs 18 and 20 naming this one.
## References
- [issue 117](../04-ISSUES/117-a-modules-own-code-is-a-container-and-a-process/00-report.md) — the contradiction this answers
- [ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md) — extended; its "a container" clause is settled here
- [ADR 0142](0142-the-mesh-delivers-its-own-components-as-binaries.md) — the same answer for the mesh's own components
- [ADR 0141](0141-the-host-delivers-its-own-successor.md) — the delivery this depends on and which is not built
- [ADR 0029](0029-a-network-is-a-shape-because-an-action-cannot-be-undone.md) — the sidecar port this relieves
@@ -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,160 @@
---
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.
## 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,133 @@
---
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.
## 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,138 @@
---
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 |
## 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
+43 -2
View File
@@ -48,6 +48,31 @@ form above and dated no earlier than the record's own `date:` — an unmarked ed
violation the reviewer looks for in the diff, and a marked one is legible in the record itself.
The git history is the backstop, not the record of intent; the note is the record of intent.
## A pointer back from what a record changes
A new record naming an old one is not enough. **Where a record changes a mechanism an older record
states — without reversing the decision, so no supersession — the older record gets a dated note
saying where its mechanism now lives.** A reader arrives at the old record by following a citation,
and finds text that is still the decision and no longer the method; nothing in it says a later record
moved the method, and the new record is not in their hands.
> **The mechanism changed — YYYY-MM-DD, by ADR NNNN.** What still stands, what moved,
> and why.
Three examples of the shape, all found by being missed: ADR 0066 still described a routed name being
written into every container after 0148 replaced that with resolution; ADR 0047 still said a module's
code runs in a container after 0150 made it a supervised process; and ADR 0016 still read as though the
lab were the test bed after 0149 said the live mesh is. Each was a citation leading to the wrong
answer, in a record that was not wrong about anything it decided.
**This is not machine-checked, and it cannot be from `extends:` alone.** 102 records extend another and
87 name a parent that does not mention them, which is correct: extending usually means building on a
context, and a one-directional pointer is the right shape for that. What needs a note is the narrower
case where the parent's own text has gone stale, and which case that is, is a judgement — so it is a
rule for the author and the reviewer, and the diff is where it is caught. Making it mechanical would
mean a record declaring the relationship in its frontmatter, which is a change to the record schema and
has not been decided.
The records run in the order the decisions were taken, oldest first.
**Every decision is a record.** There is no ledger and no index file — if a decision is worth
@@ -143,6 +168,15 @@ 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)
### Its tiers, from the bottom up
@@ -174,6 +208,8 @@ python3 00-META/checks/index.py fail if stale
- **0108** — [A route carries the policy applied to a request, and names a secret rather than holding one](0108-a-route-carries-the-policy-applied-to-a-request.md)
- **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
@@ -208,7 +244,7 @@ python3 00-META/checks/index.py fail if stale
- **0110** — [A seat is held by one assignment, from a closed set, and it may deliver a provision](0110-a-seat-is-a-module-assignment-from-a-closed-set.md)
- **0112** — [A module definition names no node, no mesh and no path: everything it needs is a requirement the mesh resolves](0112-a-module-definition-names-no-node-mesh-or-path.md)
- **0113** — [The vault makes every shared secret, a provider makes resources and data, and the mesh carries both](0113-the-vault-makes-every-secret.md)
- **0114** — [A credential two parties hold rotates over two credentials; one a single party holds rotates in place, staged; and retiring a credential never removes what it reached](0114-a-shared-credential-rotates-over-two-credentials.md) *(proposed)*
- **0114** — [A credential two parties hold rotates over two credentials; one a single party holds rotates in place, staged; and retiring a credential never removes what it reached](0114-a-shared-credential-rotates-over-two-credentials.md)
- **0115** — [One assignment of a module per node: the module's name is the assignment's identity](0115-one-assignment-of-a-module-per-node.md)
- **0117** — [A machine's uplink is a seat: the mesh configures the manager, never the link](0117-a-machines-uplink-is-a-seat.md)
- **0118** — [Undeclaring removes what the mesh made, and gives a unit back the state it was found in](0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md)
@@ -228,6 +264,9 @@ python3 00-META/checks/index.py fail if stale
- **0145** — [A module checks what the mesh claims is reachable, and it checks itself](0145-a-module-checks-what-the-mesh-claims-is-reachable.md) *(superseded)*
- **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)
### How it is built
@@ -239,7 +278,7 @@ python3 00-META/checks/index.py fail if stale
- **0016** — [The lab](0016-the-lab.md)
- **0037** — [Where a module lives](0037-where-a-module-lives.md)
- **0039** — [What the SDK holds, and what it refuses](0039-what-the-sdk-holds-and-refuses.md)
- **0068** — [The lab takes requests, one at a time, and runs each from its own copy](0068-the-lab-takes-requests.md) *(proposed)*
- **0068** — [The lab takes requests, one at a time, and runs each from its own copy](0068-the-lab-takes-requests.md) *(superseded)*
- **0069** — [A module is a repository and a path within it](0069-a-module-is-a-repository-and-a-path.md)
- **0076** — [The SDK is a published package, and the toolchain resolves it by version](0076-the-sdk-is-a-published-package.md)
- **0082** — [The registry is reached by name, and the overlay is its security](0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md)
@@ -248,6 +287,7 @@ python3 00-META/checks/index.py fail if stale
- **0097** — [A vendor image is a declared build input, and a recipe fetches nothing undeclared](0097-a-vendor-image-is-a-declared-build-input.md)
- **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)
### How it is checked
@@ -268,5 +308,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
+11 -1
View File
@@ -2,8 +2,9 @@
layer: to-be
status: in-progress
code: [mesh-host]
updated: 2026-09-29
updated: 2026-10-01
decisions:
- 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 +154,15 @@ 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.
**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
+39 -2
View File
@@ -7,8 +7,10 @@ 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-29
updated: 2026-09-30
decisions:
- 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
@@ -289,6 +291,31 @@ hosts file by the runtime. That extends the file decision rather than overturnin
mesh and not chosen by a module: a module that listed the machines would go stale the day one
joins, and a module that did not would be one whose containers cannot reach anything by name.
**Superseded for containers by [ADR 0148](../../02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md)
(2026-09-30).** Everything above still describes the machine's own roster file, which is how it works
and how it will keep working. It no longer describes containers.
Copying the roster into each container made the roster part of each container's identity, so one name
moving replaced every container in the mesh — a module assigned on one machine restarted the store, the
registry, the edge and mail on another
([issue 151](../../04-ISSUES/151-a-new-name-recreates-every-container-in-the-mesh/00-report.md)). And it
did not stop the staleness it was meant to: a copy taken at creation is stale the moment the roster
moves, twice found as a container holding an address that had not existed for days
([issues 109](../../04-ISSUES/109-a-container-keeps-the-address-it-was-made-with/00-report.md)
and [135](../../04-ISSUES/135-a-containers-mesh-names-are-not-compared/00-report.md)).
**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. 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,
not a list of containers.
**The boundary, which is deliberate and worth stating:** *declared* containers. A container
somebody starts by hand is not the mesh's to configure, and reaching into every container on a
machine — declared or not — is what a nameserver in `resolv.conf` would be for.
@@ -299,6 +326,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
@@ -374,7 +409,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
+13 -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-01
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,17 @@ 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 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.
@@ -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
+33 -5
View File
@@ -5,8 +5,10 @@ code:
- mesh-controller cmd/mesh-builder
- mesh-controller internal/builder
- mesh-catalog modules/builder
updated: 2026-09-29
updated: 2026-10-01
decisions:
- 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.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
- 02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md
@@ -185,7 +187,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
@@ -232,10 +235,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 | ✅ |
@@ -265,6 +268,31 @@ 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 builder compiles the languages the mesh is written in
*2026-09-29 —
+2 -1
View File
@@ -5,8 +5,9 @@ code:
- mesh-catalog modules/showcase
- mesh-controller internal/builder
- mesh-sdk src
updated: 2026-09-21
updated: 2026-09-30
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/0074-the-wire-is-specified-not-the-types.md
+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
+21 -2
View File
@@ -7,8 +7,10 @@ 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-01
decisions:
- 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 +82,18 @@ 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-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 +147,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 +373,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-09-30
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
@@ -136,6 +137,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 +178,34 @@ 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.
## 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
@@ -379,7 +414,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,8 +1,11 @@
---
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-01
decisions:
- 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
@@ -14,10 +17,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 +31,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 +69,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 +112,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 +133,44 @@ 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.
## Why now, and why not yet
**Why it matters:** when HAL retires, the generators that keep `~/.ssh`, shell config and the
@@ -137,7 +179,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 +198,18 @@ 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.
## 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 +218,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
@@ -13,6 +13,7 @@ code:
- mesh-catalog modules/mesh-catalog
updated: 2026-09-28
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
@@ -120,6 +121,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,
@@ -1,9 +1,12 @@
---
layer: to-be
status: designed
code: []
updated: 2026-09-28
status: implemented
code: [mesh-controller, mesh-tools]
updated: 2026-10-01
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/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 +81,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 +115,12 @@ 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.
## 7. Versioning
A seat's tools are an interface and change like one. Additive within a version. A change that would
@@ -120,6 +139,36 @@ 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.
## 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
+143
View File
@@ -0,0 +1,143 @@
---
layer: to-be
status: implemented
code: [mesh-catalog, mesh-tools, mesh-controller]
updated: 2026-10-01
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)).
## 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.
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)
+1 -1
View File
@@ -38,7 +38,7 @@ 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) |
@@ -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,7 +1,7 @@
---
status: open
status: located
opened: 2026-09-22
located-in: []
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
fixed-by:
amended-design:
---
@@ -35,3 +35,8 @@ 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`.
@@ -1,8 +1,8 @@
---
status: open
status: resolved
opened: 2026-09-22
located-in: []
fixed-by:
located-in: [mesh-controller internal/link/protocol.go (the report field that was missing), internal/inventory, cmd/mesh-controller (node show and status)]
fixed-by: mesh-controller 7683ba8, corrected by 1d9c102
amended-design:
---
@@ -0,0 +1,143 @@
# 087 — resolved: the mesh knows which host runs a machine
*2026-09-30.*
## The field existed and was thrown away on arrival
The machine has reported its host version since
[ADR 0141](../../02-DECISIONS/0141-the-host-delivers-its-own-successor.md) — `Host` on the report, with
a comment saying why it must be there: *"without it nothing can say a machine is behind."*
**The controller's own copy of the report did not have the field.** Two structs describe one message,
one on each side of the wire, and only the sending side had it — so it unmarshalled into nothing and the
mesh could not answer a question the machine had been answering for a week. That is the whole of this
issue's mechanism, and it is worth stating plainly because neither side was wrong on its own.
## What it says now
`node show` names it per machine:
```
last heard from here
host 2026-09-30-0214
```
`not reported — this machine has not said since the mesh began keeping it` where the mesh has not been
told, because a machine that has not said is a different thing from a machine running nothing.
`status` names the machines that are behind another:
```
1 machine(s) run an older host than another machine does:
ace 2026-09-29-0113
the newest any machine reports is 2026-09-30-0214. A host refuses a declaration carrying a
field it does not know, whole — so a new field reaches these machines last
```
## Disagreement, not staleness, and that is deliberate
The open questions asked whether the controller should refuse to send a declaration a node cannot
parse. It cannot yet, honestly: **nothing delivers a host version** (ADR 0141 is accepted and not
built, which is [issue 142](../142-the-host-is-the-one-thing-the-mesh-does-not-deliver/00-report.md)),
so the mesh holds no canonical current version and "behind" has no fixed point to be behind.
What it can say truthfully is that these machines do not all run the same host, and which is newest of
the ones it has been told about. That is the fact that matters before a declaration gains a field: **the
oldest host in the mesh is what the mesh may send.**
Two deliberate refusals to guess:
- **A machine that has reported nothing is not called behind.** It may be running anything. `node show`
says it has not said, per machine, which is the honest form.
- **Versions compare as strings.** That suits the timestamps and commits this mesh uses and is wrong
for a scheme where `10` sorts before `9`. Said in the code at the place that would have to learn,
rather than left as a surprise.
## What I shipped first was wrong, and the mesh said so within the hour
The first version reported *"N machine(s) run an older host than another machine does"* and worked out
which by comparing versions as strings. **A host reports its version as a commit, and commits have no
order.**
On the live mesh, with the adopted machine pushed for the first time:
```
3 machine(s) run an older host than another machine does:
g14 04a27ca
novox 04a27ca
shanks 04a27ca
```
Those three run the **newer** host — installed 09:18, against the adopted machine's 01:13. `ced54d4`
sorts above `04a27ca` and that is all it means. An arbitrary lexicographic result, presented as a fact,
about the one thing this record exists to make trustworthy.
The code carried a caveat saying versions compare as strings and that this "is enough for the timestamps
and commits this mesh uses". That was the error, written down and not noticed: it is enough for
timestamps and it is **meaningless** for commits, and the mesh reports commits.
It now reports the split and claims no ordering:
```
4 machine(s) do not all run the same host:
04a27ca g14, novox, shanks
ced54d4 ace
a host refuses a declaration carrying a field it does not know, whole — so the mesh may
send only what every one of these understands. Which of them is newer is not readable
from a commit; that needs a version the host reports as ordered
```
More useful as well as more honest — the reader sees who is on which side of the split, which is what
decides whether a field can be sent — and it leaves the ordering where it belongs: with the host, which
would have to report something ordered for anybody to have it.
**This is [issue 145](../145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md)
arriving by my own door**, an hour after closing it: a report that confidently says the opposite of the
truth is worse than one that says less, because it trains a reader to distrust the whole surface.
## Measured on the mesh, and one limitation it exposed
All four machines run the identical host binary — same digest, installed within eighteen seconds of each
other — and at first only one reported its version. The other three said *not reported*, which read as a
difference between machines where there was none.
**A machine states its host version only when the mesh sends it a declaration.** The report is published
after an apply; the periodic reconcile that runs every five minutes publishes nothing, because it is the
machine keeping itself as declared rather than answering anything. So a machine that is current and idle
never says, and the mesh cannot distinguish that from a machine running something ancient.
Confirmed by pushing: before, `not reported`; after, `04a27ca` — the same version the machine that had
been pushed already reported.
```
novox host 04a27ca
shanks host 04a27ca
g14 host 04a27ca
ace host not reported — this machine has not said since the mesh began keeping it
```
`ace` has not been pushed since the field existed; it is adopted and parked.
**This is enough for the purpose and not enough for the claim.** For deciding whether a new declaration
field is safe it is sufficient, because pushing is what the mesh is about to do anyway and the answer
arrives with the act. For *knowing what the mesh is running*, it is not: a long-idle machine's entry is
as old as its last push, and the honest reading of `not reported` is "nobody has asked recently" rather
than "this machine is silent". The words say the first, which is why they are those words.
Making a heartbeat carry it would close the gap and is a change to what a heartbeat is — a bare word
that the node is there, deliberately carrying nothing else. Left alone rather than widened in passing.
## The open questions, answered as far as they can be
- *Should a node report the version of its host?* It already did. The gap was the reading.
- *Should the mesh refuse to send a field no node understands yet, or refuse per node and say so?*
Neither, yet — refusing needs the mesh to know which fields need which version, which is the third
question below and is not answered here. What it does is make the disagreement visible before
somebody adds a field.
- *Is there a general shape — a declaration saying which version of the host it needs?* Still open, and
now cheaper to answer: the versions are recorded, so a minimum-version field on a declaration has
something to compare against. It belongs with
[issue 107](../107-a-declaration-carries-no-order/00-report.md), which wants to add a field and is the
first thing this makes safe.
@@ -1,7 +1,7 @@
---
status: open
status: located
opened: 2026-09-22
located-in: []
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
fixed-by:
amended-design:
---
@@ -48,3 +48,8 @@ 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`.
@@ -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,7 +1,7 @@
---
status: open
status: located
opened: 2026-09-23
located-in: []
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
fixed-by:
amended-design:
---
@@ -58,3 +58,8 @@ 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`.
@@ -1,7 +1,7 @@
---
status: open
status: located
opened: 2026-09-23
located-in: []
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
fixed-by:
amended-design:
---
@@ -75,3 +75,8 @@ 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`.
@@ -1,7 +1,7 @@
---
status: open
status: located
opened: 2026-09-23
located-in: []
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
fixed-by:
amended-design:
---
@@ -63,3 +63,8 @@ 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`.
@@ -1,7 +1,7 @@
---
status: open
status: located
opened: 2026-09-23
located-in: []
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
fixed-by:
amended-design:
---
@@ -60,3 +60,8 @@ 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`.
@@ -1,7 +1,7 @@
---
status: open
status: located
opened: 2026-09-23
located-in: []
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
fixed-by:
amended-design:
---
@@ -65,3 +65,8 @@ 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`.
@@ -1,7 +1,7 @@
---
status: open
status: located
opened: 2026-09-23
located-in: []
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
fixed-by:
amended-design:
---
@@ -61,3 +61,8 @@ 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`.
@@ -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,8 +1,8 @@
---
status: located
status: resolved
opened: 2026-09-23
located-in: [mesh-controller internal/link, mesh-host internal/link]
fixed-by:
fixed-by: mesh-host PR 59 (the host refuses an older sequence and drains by it), mesh-controller PR 160 (each send is numbered under the node's hold) — measured 2026-09-30, 02-resolution.md
amended-design:
---
@@ -40,3 +40,16 @@ exists there and is thrown away at the wire.
separate genesis-digest branch is needed on the host?
- Is a sequence enough, or does a mode change deserve its own marker, so a replayed converged
declaration is refused by mode as well as by order?
## What has since made this safer to do (2026-09-30)
Adding a `sequence` to a declaration is adding a field, and a host refuses a declaration carrying a
field it does not know — whole. That was
[issue 087](../087-the-controller-cannot-tell-a-host-is-too-old/00-report.md), and it is resolved: the
mesh now records which host each machine reports and `status` names every machine running an older one
than another does.
So the flag day is visible before it is walked into, which it was not when this was filed. It does not
make the field free: **the oldest host in the mesh is still what the mesh may send**, and one machine of
four is behind today. A sequence that an old host refuses takes that machine out of the mesh's reach
entirely — worse than the replay it prevents, which has never been observed.
@@ -0,0 +1,76 @@
# 107 — diagnosis: the fix is a flag day, and it should wait for delivery
*2026-09-30. Read, measured, and not built — deliberately.*
## The premise is confirmed
A host parses a declaration with unknown fields refused, and the code says why rather than leaving it
to be inferred:
> `DisallowUnknownFields` is the whole point rather than strictness for its own sake: a field the host
> does not know is a thing the control plane believes it asked for.
So adding `sequence` and `supersedes` is not an additive change. **Any host that has not been upgraded
refuses the whole declaration and applies nothing** — which is exactly the behaviour that keeps a
half-understood declaration off a machine, and exactly what makes this expensive.
## What has changed since this was filed
[Issue 087](../087-the-controller-cannot-tell-a-host-is-too-old/00-report.md) is resolved: the mesh now
records the host version each machine reports and `status` names every machine running an older host
than another does. The flag day is visible before it is walked into, which it was not on 2026-09-23.
That makes the cost measurable rather than hypothetical, and the measurement is the reason this is not
being built today.
## Why it waits
**One machine of four runs an older host, and it cannot be upgraded.** `ace` is adopted, deliberately
parked until the network and module-assignment work is settled, and **nothing delivers a host version at
all** — [ADR 0141](../../02-DECISIONS/0141-the-host-delivers-its-own-successor.md) is accepted and not
built, which is [issue 142](../142-the-host-is-the-one-thing-the-mesh-does-not-deliver/00-report.md).
Every machine takes a hand-placed binary.
So shipping the field means, in order: place a host by hand on three machines, unpark the fourth, place
it there too, and only then turn the controller half on. A machine missed in that sequence is a machine
the mesh cannot send anything to at all — not degraded, unreachable.
**And the fault it prevents has never been observed.** The record says so itself: *"Not observed;
constructed from the code, and narrow."* It needs a backlog of more than sixteen declarations queued
across a `converge`/`adopt` pair, or a broker slow enough to split one, and the host already applies the
newest of a drained batch and refuses a declaration that is not the last by digest.
**Trading a machine's reachability for a replay nobody has seen is the wrong way round.** The right
order is [issue 142](../142-the-host-is-the-one-thing-the-mesh-does-not-deliver/00-report.md) first —
when the mesh can deliver a host, a declaration field costs a rollout instead of an expedition — and the
work order already puts that in its last group, as the proof that the mesh can make another of itself.
## What the open questions look like now
- *A per-node `sequence` under the controller's node hold, and `supersedes` as the previous digest?*
Still the right shape. The controller already holds the lock and already records each send, so the
order exists and is thrown away at the wire — unchanged since this was filed.
- *Genesis signing its bundle as sequence zero?* Yes, and it is the cheaper half: the bundle is written
by the host that will read it, so it has no flag day of its own.
- *Is a sequence enough, or does a mode change deserve its own marker?* A sequence alone does not stop
a replayed *converged* declaration reaching a node that has since been returned to adopted, which is
the incident of issue 104 by another door and is what this record names as its real risk. It wants
both, and the second is the one worth having first.
- **And one this record did not ask:** should a declaration say which host version it needs? 087 makes
that comparable for the first time, and it is the general form of the answer — a field that announces
its own requirement, rather than a flag day per field, for ever.
## Status
Left `located`. The owner is unchanged, the shape of the fix is agreed, and the gate is
[issue 142](../142-the-host-is-the-one-thing-the-mesh-does-not-deliver/00-report.md) rather than anything
in this record. **This is a judgement about order, not a refusal** — it is cheap to overrule, and the
code is a day's work once a host can be delivered.
## The gate has opened (2026-09-30, evening)
The mesh delivers the host now — built by its own toolchain, published to its own registry, delivered
over the bus and started by the launcher, on all four machines
([issue 142](../142-the-host-is-the-one-thing-the-mesh-does-not-deliver/01-progress.md)). A declaration
field is a build and a push, not an expedition. The order this record asked for — hosts first, then
the controller — is now two commands and a status line that says when the first has finished.
@@ -0,0 +1,60 @@
# 107 — resolved: a declaration carries its order
*2026-09-30. Measured on the mesh.*
## What was done
**Hosts first, then the controller** — the order [issue 087](../087-the-controller-cannot-tell-a-host-is-too-old/00-report.md)
says a new declaration field needs, and now a build and a push rather than an expedition
([issue 142](../142-the-host-is-the-one-thing-the-mesh-does-not-deliver/01-progress.md)).
The host understands a `sequence` on a declaration and tolerates its absence: absent reads as "no
order claimed", not "first", so a controller that sends none is still understood and a host that kept
a declaration before it understood the field compares nothing. It refuses a declaration with a lower
sequence than the one it kept, whole, and says why; and the drain that picks one declaration from a
batch keeps the highest sequence rather than the last to arrive — which is the case the report
constructed, a backlog drained out of order.
The controller numbers each send: the next number for that node, taken under the node's hold, before
the body exists, so the number is inside what the mesh signs and a replayed older declaration cannot
borrow a newer one's.
## Measured
```
push shanks; push shanks
sequence in kept declaration: 2
node sequence
novox 2
shanks 2
ace (none — not sent since numbering)
g14 (none)
status: nobody "not running what the mesh would send them"
```
Both applies went through; neither was refused; the machine holding the earlier one accepted the later.
## The subtlety, which would have read every machine as behind for ever
The mesh decides a machine is behind by comparing the digest of what it **would** send against what it
**did** send. A number changes the bytes. So the read-only comparison composes with the number the
machine was *last* sent — not a fresh one — and is byte for byte what was sent when nothing else
changed. Without that, numbering would have made `status` name all four machines as out of date on
every reading, permanently.
## The open questions
- *A per-node `sequence` under the controller's node hold?* Yes, as described. **`supersedes` — the
previous digest — is not added.** A strictly-greater sequence gives the ordering; a chain of digests
would give continuity, which nothing here needs yet and which every re-composition would break.
- *Genesis signing its bundle as sequence zero?* Zero is "no order claimed", which is what the bundle
carries by carrying nothing. Same rule, no genesis branch.
- *A marker for a mode change?* Not needed for the incident it guards: a replayed converged declaration
reaching a node returned to adopted is already refused **by mode**, before this check runs.
## How it is checked
Host: an older sequence is refused, a newer or equal one is not, and no order claimed on either side
compares nothing; the drain keeps the highest sequence, and falls back to arrival when none is claimed.
Controller: a send carries its number inside the signed bytes, an unnumbered send is byte for byte what
it was before, and each node's counter is one higher per send and readable for the comparison.
@@ -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:
---
@@ -59,3 +59,27 @@ container is made with are the same kind of input, read once at creation, and ar
and is the stronger statement; it is also what the mesh's own resolver exists for.
- Either way: what tells an operator that a container is running with an address the node no longer
has? Nothing did.
## Answered at the cause (2026-09-30)
This was the first of three arrivals of one fact: a container is given the mesh's names when it is
created and never looks again, so a name that moves afterwards is wrong inside it for as long as it
runs. It arrived again as [issue 135](../135-a-containers-mesh-names-are-not-compared/00-report.md),
whose fix made the names comparable — and that fix made the roster part of every container's identity,
which arrived as [issue 151](../151-a-new-name-recreates-every-container-in-the-mesh/00-report.md).
[ADR 0148](../../02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md) ends the
copying: a container resolves through its machine's resolver at the moment it asks. The shape this
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.
@@ -1,8 +1,9 @@
---
status: open
status: resolved
opened: 2026-09-24
located-in: []
fixed-by:
located-in:
- mesh-catalog modules/dnsmasq (the runtime was never told; the resolver answered by interface)
fixed-by: mesh-catalog PR 175 (the runtime is reloaded and keeps its containers over a restart) and PR 176 (the resolver answers by address, so a query from a bridge is admitted) — measured 2026-09-30, 01-resolution.md
amended-design:
---
@@ -51,3 +52,22 @@ knows that is what the rule means.
the runtime's default one? That is a stronger rule and would have prevented 109 as well.
- What checks it? A converged bed with a container on the default network resolving a mesh name is
the missing assertion; nothing in the resolver's own beds covers the filter.
## What now depends on this (2026-09-30)
This stopped being a container-DNS inconvenience.
[ADR 0148](../../02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md) decides
that a container resolves the mesh's names rather than being given a copy of them, which is what stops
one name moving from replacing every container in the mesh
([issue 151](../151-a-new-name-recreates-every-container-in-the-mesh/00-report.md)) and what makes a
stale address impossible rather than merely noticed
([issues 109](../109-a-container-keeps-the-address-it-was-made-with/00-report.md)
and [135](../135-a-containers-mesh-names-are-not-compared/00-report.md)).
**That decision cannot land until this one does**, and not partly: a container on the runtime's default
network is the case with no DNS at all, and it is the case the mesh's own forge runs in. Two of four
machines also bind the resolver to loopback only, so the runtime hands their containers a public
resolver. Both halves are this issue.
*Later the same day: the second half was wrong, and the first had a different cause than the one above.
[01-resolution.md](01-resolution.md) has what was actually found.*
@@ -0,0 +1,64 @@
# 110 — resolved: a container on any network reaches the resolver, and is answered
*2026-09-30. Measured on the three converged machines; the adopted one holds its resolver module until it
is taken and is not covered.*
## What was actually wrong
Not what the report predicted. The report named the filter: a container on the runtime's default
network asks from a bridge address, and the converged filter admitted queries by source address only.
That was true when it was written and was fixed before this issue was ever tested — the filter admits
by the link a packet arrives on ([ADR 0144](../../02-DECISIONS/0144-anything-on-a-machine-may-call-anything-on-it.md)),
and a container's bridge is admitted whole. Tested on every machine: the query arrives, the filter
passes it.
Three other things were wrong, each hiding the next.
**The runtime had never been told.** The resolver module writes the runtime's `dns` key into the
runtime's own configuration file. The runtime reads that key when it starts and not on a reload, and on
two machines the runtime predated the file — so every container they started got a public resolver, and
`novox.internal` came back as not existing. Nothing reported this: the file was present and current,
the resolver ran, and a name not existing is a valid answer. Fixed in mesh-catalog PR 175: the module
also sets `live-restore` and reloads the runtime when its file changes, so the one restart the `dns` key
needs no longer stops every container. The restart is then the operator's, once per machine; done on
both today, with every running container kept.
**The resolver dropped the query.** With the runtime corrected, a container's query reached the resolver
— and got no answer, on every machine, including the one whose runtime had been right all along. The
socket was bound to the private address; the filter admitted the packet; dnsmasq received it and
discarded it without a line of log. Its configuration said `interface=mesh0`, and dnsmasq admits a
query by the interface it arrives on when told an interface: a container's query is addressed to the
private address but arrives on the runtime's bridge, and the bridge is not `mesh0`. Fixed in mesh-catalog
PR 176: the resolver is told the address to answer on, not the interface that carries it, and a query to
that address is admitted whatever bridge brings it. The bridges are the runtime's to name.
**The report's second half was wrong.** "Two of four machines bind the resolver to loopback only" was
an inference from the containers' behaviour, and the behaviour had the cause above. The resolver bound
the private address on all four; nothing had asked it there.
## What is verified
From a container on the runtime's default network, started by hand and given nothing, on each of the
three converged machines: `novox.internal` answers with the hub's private address, through the machine's
own resolver. That is the fourth check of
[ADR 0148](../../02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md) — "on every
network the runtime offers" — and its first step; the record's step 2 (the runtime told per machine, as
a file) was already how the module works. Step 3 may now begin.
## What checks it
By hand, today. Nothing in the mesh asserts that a container can resolve a mesh name: the resolver's
own tests cover what it answers, not who can ask. The check that would have caught all three faults is
the one the report asked for and 0148 lists — a container on the default network resolving a mesh name
— and it is not built. It belongs with the reachability check of
[issue 145](../145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md),
which is parked; until then this is a thing a person verifies after touching the resolver, the filter,
or the runtime's configuration.
## What this cost to find
The three faults produced one symptom — a container that cannot resolve — and each fix revealed the
next. The first was found by reading the runtime's own view of its configuration rather than the file;
the second by capturing the query on the bridge and finding it arrive and go unanswered; the third only
by admitting the first belief was wrong. A machine that had been believed to work all day had never
worked either.
@@ -1,8 +1,8 @@
---
status: located
status: resolved
opened: 2026-09-25
located-in: [hq, mesh-catalog modules/showcase, mesh-sdk src/tools/index.ts, mesh-tools]
fixed-by:
fixed-by: hq 83791f0 (PR 196) — ADR 0150: a module's own code runs as supervised processes under the module's one account; designs 18 and 20 now cite it, and ADR 0047 carries a dated note pointing at it
amended-design:
---
@@ -99,3 +99,27 @@ what the sidecar is has nowhere in the design layer to look, which is how this w
is mechanically checkable: the resource types a design doc names are a closed set, and every
member of it either appears in a decision or does not. Whether that check is worth writing is
part of this issue, not settled by it.
## Answered (2026-09-30)
[ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md)
settles all three disagreements, and the design documents win two of them:
1. **Container or unit — a supervised process.** ADR 0047's argument never required a container. It
argued for a runtime *per module*, because a node-wide one could not hold a per-module account and
per-module runtimes on one tool key would be handed calls for tools they do not have. A unit per
module satisfies that exactly, and a unit runs as an account. The container was the mechanism to
hand. [ADR 0142](../../02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md) had
already gone the same way for the mesh's own components, and a module is not a container.
2. **One process or several — several, under one account.** 0047's "one module, one process, one
account" carried its weight in the last clause; its stated worry was "not a second one to scope and
seal", which is about a second *identity*. Processes sharing the module's one account create none.
What a module may not have is two accounts.
3. **Whether the record was consulted — fixed rather than answered.** Designs 18 and 20 now name 0150
in `decisions:`, and 0047 carries a dated note saying where its hosting form was settled, so neither
door leads to the wrong answer any more.
**What this does not fix.** A module's code delivered as a binary is behind the same gap as the host's
own ([ADR 0141](../../02-DECISIONS/0141-the-host-delivers-its-own-successor.md), accepted and not
built): a container's code arrives by `docker pull` and this does not, so until delivery exists such a
module is one somebody places by hand. 0150 records that as the cost of the decision, unpaid.
@@ -1,8 +1,8 @@
---
status: located
status: resolved
opened: 2026-09-25
located-in: [mesh-catalog modules/umami]
fixed-by:
located-in: [mesh-host internal/apply (the spec comparison, via issue 135) — not mesh-catalog modules/umami, which this record first named and which was never at fault]
fixed-by: mesh-host e82789a (issue 135's fix) — recreating the container with a current roster ended it; verified live 2026-09-30
amended-design:
---
@@ -0,0 +1,79 @@
# 118 — resolved: it was issue 135, and it is over
*Verified on the machine, 2026-09-30.*
## It no longer happens
```
$ docker inspect -f '{{.RestartCount}}' umami
0
$ docker logs umami --tail 12
26 migrations found in prisma/migrations
No pending migrations to apply.
✓ Database is up to date.
▲ Next.js 16.3.4
✓ Ready in 0ms
$ curl -o /dev/null -w '%{http_code}' https://umami.novox.be/
200
```
No restart loop, no `prisma.$queryRaw()` timeout, and the public name that had answered `502` since
2026-09-25 answers `200`. The raw query that could not complete now runs twenty-six migrations and
reports the store up to date.
## What it was
**The same fault as [issue 135](../135-a-containers-mesh-names-are-not-compared/00-report.md), which
was diagnosed three days later without either record noticing the other.** 135's container is this
one, named in its own evidence table:
```
umami created 2026-09-23 novox.internal:10.42.0.1
mesh-catalog created today novox.internal:10.10.0.1
```
The mesh's overlay range had moved. Umami had been created before the move and held the store's name
at an address that no longer existed, while every container made after the move held the current one.
That is why the dial appeared to succeed and the first real query timed out, and why the same query
from the same network with the same credential answered in milliseconds — **what differed was the
name, not the path, not the credential and not the store.**
Today it holds `novox.internal:10.10.0.1`.
## Why this record did not find it
The report's own reasoning is worth keeping as a warning, because it is careful and it is wrong:
> A dial that succeeds and a query that times out, from a container on one network to a store on
> another, has the shape of a path-MTU or conntrack fault (large response packets dropped after the
> small handshake ones pass), or of the store accepting the TCP connection while the backend it
> proxies for is wedged.
Both are good hypotheses about a network path. Neither is the answer, and the record also names the
move that would have found it — *"what is known to differ for umami against every working consumer of
the same store tonight is nothing yet — that comparison is the first move"* — and then did not make it.
Comparing umami's hosts entries against any container created that week would have shown a five-day-old
address in one field.
**A stale name presents as a network fault.** That is the lesson, and it is the reason
[ADR 0148](../../02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md) stops
copying names into containers at all: not because detecting staleness is hard, but because it disguises
itself as something else for five days while every check reports success.
## What actually ended it
Issue 135's fix — `mesh-host` e82789a, *a container's mesh names are part of what it is* — put the
roster into the spec digest the host compares, so a container whose names moved is recreated like one
whose image moved. That recreated umami with a current roster and ended this.
That fix has since been superseded in turn, by 0148, because making the roster part of every
container's identity meant one name moving replaced every container in the mesh
([issue 151](../151-a-new-name-recreates-every-container-in-the-mesh/00-report.md)). So this record
closes on a fix that is itself on the way out — which does not make it less closed, and is worth saying
plainly rather than leaving a reader to find out.
## Not carried forward
The `502` had one other contributor worth recording as ruled out: a stale duplicate Traefik router for
this name, hand-authored in the predecessor's era beside the mesh-written one, removed on 2026-09-25
and — as the report says — changing nothing. It was not the cause and it is gone.
@@ -1,9 +1,9 @@
---
status: located
status: resolved
opened: 2026-09-25
located-in: [mesh-catalog modules, mesh-controller internal/catalogue]
fixed-by:
amended-design:
fixed-by: mesh-catalog PR 193 (the conversion), mesh-controller PR 170 (TestPlacedDirectoriesKeepTheirPaths, which proves it moved nothing); the placed-directory mechanism itself predates this in mesh-controller internal/catalogue/dir_into.go
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
---
# 119 — A module definition decides where its files live on the machine
@@ -133,3 +133,30 @@ not by any check.
- What identifies an assignment, if a module may be assigned to one node more than once?
- What would the contributions file carry instead of host paths, so a provider needs no
identical-path mount?
## Resolved, 2026-09-30 — the module's half; the mesh's half is issue 174
**A definition no longer decides where its own data lives.** Twenty-eight definitions that named their
data directories now place them: the module's root as `place: "."`, a sub-directory by its id, and every
host-side reference — bindings, secrets, own secrets, grants, receives, file paths, mounts, env-files —
as `${dir:<id>}`. Twenty-eight others had already been written that way. Five directories whose id is
not their last segment keep their path as a placement, which is the exception the design allows and
the reason nothing else has to move for them.
**Nothing moved, and a test says so.** The controller's `TestPlacedDirectoriesKeepTheirPaths` takes the
catalogue before and after, resolves every converted definition on the default root with the
controller's own rule, and compares it whole with the definition before it: identical for all
twenty-eight. So the retirement this record said was a data migration turned out not to be one, on
one condition — a node's default root is where the data already is, and every node's is — and the
machines see no change. A node that sets another root is the case this does not cover, and it does
not exist.
**What remains is not this record's.** The 232 host paths still in the catalogue are where the mesh
writes what it makes for a module, under `/var/lib/mesh/<module>`; design 27 says the mesh places
those itself, and it does not yet. That is [issue 174](../174-the-meshs-own-files-for-a-module-are-placed-by-the-definition/00-report.md). *Placed since later the same day: `place: "mesh"` — issue 174 is resolved.*
The defects this record listed under *where that has already gone wrong* are unchanged by this and
stay in 174's scope where they concern the mesh's files; the operator's shared data stays an access
([ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md)).
The manifest change lands with the catalogue's next merge; the rollout is a rebuild that changes no
machine, checked by comparing each machine's plan before and after.
@@ -1,9 +1,9 @@
---
status: located
status: resolved
opened: 2026-09-26
located-in: [mesh-controller internal/catalogue/declaration.go, mesh-catalog modules/keycloak, mesh-catalog modules/minio, mesh-catalog modules/nextcloud]
fixed-by:
amended-design:
fixed-by: mesh-controller PR 149 (a module is told the name it is served under), PR 169 (an operator's value, a context on the seat); mesh-catalog PR 188; ADR 0155
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
---
# 122 — A module cannot ask for its own public name, so three manifests wrote this mesh's names into the catalogue
@@ -114,3 +114,27 @@ particular to one installation, and also has nowhere to live but the definition.
ADR 0112 would put it), and if so what reads it — the provisioner, or the module's own values?
- What check would notice the next one? A definition naming a public domain is detectable in the
shape of the value, which is more than nothing, and less than a rule.
## Resolved, 2026-09-30
The open questions, answered in order. **A module names what it will be reached at** through the
binding of the route it contributes: `${bound:route:name}`, or `:name-<local>` for several
contributions, is the composed public name; `:internal-name` the private one (controller PR 149). The
identity provider, the object store's console and the automation tool's webhook now read it there.
**The scheme is the module's**, because it is true of the route the proxy terminates and every instance
wrote `https://` in front of the name. **An adopted resource's name is a setting** on the assignment;
so is every other operator's value — a mail domain, a site name, the address a proxy forwards from —
as `${setting:<key>}` in the file the software reads
([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)).
**The check that notices the next one** is the shape of the value, as this record guessed it would be,
and it is more than nothing: it found forty-two, and the catalogue passes it now
([issue 134](../134-a-definition-may-still-name-the-mesh/00-report.md)).
**What the rollout cost, 2026-09-30 evening.** The site module's rename from its domain to `website`
was a new module to the mesh, and two things the old assignment carried by name were lost: the
container still named the old network, and a port setting on the old assignment had hidden that
`listens` said one port while the container published another. The site answered 502 for about
twenty minutes across two one-line fixes (mesh-catalog PRs 190, 191). A module's rename is an
unassign and an assign, and everything the assignment held — settings, ports, its directory — is the
new module's to get again; the mesh says nothing about that today. The mail module's settings turned
out to reach every fact it contributes, which is [issue 173](../173-a-modules-settings-reach-every-fact-it-contributes/00-report.md).
@@ -1,9 +1,9 @@
---
status: located
status: resolved
opened: 2026-09-26
located-in: [hq 00-META/glossary.md, hq 02-DECISIONS/0075-two-stores-and-which-provides-what.md, mesh-controller internal/catalogue/seats.go, mesh-catalog modules/distribution]
fixed-by:
amended-design:
fixed-by: ADR 0156; mesh-controller migration 0048 (feat/the-artifact-store-seat-is-named-for-its-scope); mesh-catalog modules/distribution
amended-design: 03-DESIGN/01-to-be/26-the-seats.md
---
# 123 — The image registry is named after a role, and *artifact* is defined as one format
@@ -72,3 +72,15 @@ adopted, rather than on protocols.
exercise that leaves the code disagreeing?
- What check would keep the glossary honest — a definition tested against the kinds a definition may
actually declare, rather than restated by hand?
## Resolved, 2026-09-30
Read from what the store serves rather than from what it was called: both shapes of a kept reference
— an image and an archive's blob — go to the same registry by digest, which is exactly the provision
ADR 0075 defined. So the word was wrong and the seat's name was odd, and the provision was right.
[ADR 0156](../../02-DECISIONS/0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md):
*artifact* means what a build produces, of any of the four kinds; the seat is `mesh-artifact-store`
with the old name as its alias (one migration, ADR 0122's mechanism); the provision keeps its name.
The two-implementations question stays as 0075 answered it, with the day to retire the second server
named. The mechanical check the report asked for is the alias test and the glossary naming the same
four kinds as design 18's table.
@@ -1,7 +1,9 @@
---
status: open
status: resolved
opened: 2026-09-26
located-in: [mesh-host internal/apply, mesh-controller]
located-in: [mesh-host internal/link (the apply report line), mesh-controller cmd/mesh-controller (status and its all-well condition)]
fixed-by: mesh-host cbf5018, mesh-controller bfd983e
amended-design:
---
# 125 — a hold is not a line in the apply report, and an operator flew blind into an outage
@@ -0,0 +1,70 @@
# 125 — resolved: a hold is a line in the report, and it stops the mesh reading as well
*2026-09-30.*
## What the four surfaces say now
The report named four surfaces, none of which carried the one sentence that mattered. Two of them
already did by the time this was picked up, and two did not.
**1. The apply report says what it held** — this was missing, and is the line the operator was reading
when the count did not add up:
```
applied 330 resource(s), 16 held until their module is taken (route-proxy: 13, mailu: 3)
```
Grouped by module and ordered by name, because `take` acts on a module and that is the sentence an
operator needs. An apply that held nothing says nothing extra — a line reporting `0 held` on every
converged apply is one that stops being read.
**2. `status` counts holds, and a hold breaks "all well"** — this was missing. Status now says:
```
15 resource(s) are held as found, because their module was assigned and never taken — so it is
running none of what it declares:
novox route-proxy (13), mailu (2)
`take <node> <module>` compares what runs against what it declares, and runs it
```
**And the sentence that was the fault no longer prints.** "all doing what they were told, all heard
from, running what the mesh would send them" was *true* for the whole outage, and acting on it stopped
the predecessor's proxy. A hold now suppresses it; being adopted still does not, and the difference is
deliberate — adopted is a mode somebody chose and can leave alone, a module assigned and never taken is
a half-finished action with nothing left to finish it.
**3. `node show <node>` shows the node's own held list** — already true, and recorded here as checked
rather than assumed. It reads `held` from what the machine last reported, with an `as of` beside it, and
names each hold's kind, target, id, module, whether something other than the mesh has changed it, and
where an original was kept.
**4. The push's count** is unchanged and now interpretable, which was the ask: `sent 346` against
`applied 330, 16 held until their module is taken (…)` is a pair a reader can resolve without opening a
file on the machine.
## Where the data came from
**The host already reported it.** `Held` has been on the wire since [ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md),
and the controller already recorded it and showed it in `node show`. Nothing needed a new field, a new
message or a migration — which is why this is additive, and why the report's framing (*"the semantics
are consistent and right; the reporting is what let them be forgotten"*) was exactly right.
What was missing was that two surfaces never asked. Status read the mesh's take-time listing, so a
module assigned after that listing showed nothing at all; the apply line counted what it applied and
said nothing about the difference.
## One thing deliberately not done
**Status does not call a hold a fault.** It is correct behaviour, and a reader trained to see red for
something the mesh did right will stop reading. It is reported as work outstanding, with the command
that finishes it — and it withholds the all-well sentence, which is the part that carries the weight.
## How it is checked
- The apply line names the count and the module, and is empty when nothing is held (mesh-host).
- Status finds a hold from what the machine reported, end to end through the store.
- **A held module makes the all-well condition false**, asserted against the production condition
rather than a copy of it — that condition is now one named function for this reason.
- The JSON form carries a row per machine and module, and omits the field entirely when nothing is
held.
@@ -1,5 +1,5 @@
---
status: open
status: located
opened: 2026-09-26
located-in: [mesh-host internal/apply]
---
@@ -45,3 +45,8 @@ Instant renames both ways broke the circular dependency (forge needed for builds
builds needed for the push, push needed for the forge): data back to the old path,
old-spec forge started, artifacts rebuilt, data renamed forward, push. Nothing lost;
the install-page junk was discarded twice.
## Decided, 2026-10-01
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 5 and 7: every field compared; build says the policy. Building follows,
host first, then the controller's `take`.
@@ -1,7 +1,8 @@
---
status: located
status: resolved
opened: 2026-09-26
located-in: [mesh-catalog ca-trust]
located-in: [mesh-catalog modules/ca-trust]
fixed-by: the ca-trust module registered from the catalogue and assigned to a workstation; verified in both directions 2026-09-30 (02-resolution.md)
---
# 129 — nothing makes a machine trust the mesh's own certificate authority
@@ -1,45 +1,82 @@
# Diagnosis
# 129 — diagnosis
*2026-09-29.*
*2026-09-30, from the workstation the issue was opened on.*
## What was ruled out
## Still live, and reproduced exactly
**That something already carries the root and it is only misplaced.** It does not. The authority
serves its root at a path beside its ACME directory, and the one thing that fetches it — the route
proxy — puts it in a directory of its own and hands it to one program. Nothing has ever written
into a machine's trust store. Measured on three converged machines: the anchors present are the
predecessor's authority and a developer tool's local root, and on the machines where the
predecessor's was deliberately removed, every internal name fails verification.
The certificate is genuine, the authority is the mesh's, and nothing on the machine trusts it:
**That the private network could carry it, the way it carries the registry's trust.** That is what
the report proposed, and it was rejected on consideration rather than on difficulty
([ADR 0147](../../02-DECISIONS/0147-a-module-anchors-the-meshs-authority.md), option 1): being on
the network is what makes the registry *reachable* and is therefore the right trigger there, while
trusting an authority is a separate fact from being able to reach it. The anchor's directory and
the command that refreshes the extracted bundles are also one operating system's difference, which
is the host's half of the mesh and not the controller's.
```
$ openssl s_client -connect keycloak.novox.internal:443 -servername keycloak.novox.internal
subject=CN=keycloak.novox.internal
issuer=O=Mesh Internal CA, CN=Mesh Internal CA Intermediate CA
Verify return code: 20 (unable to get local issuer certificate)
**That it needs a new host resource type.** It does not, today. A file and a service say the whole
of it, which the packet filter already proves. The primitive becomes the right answer when a second
operating system is in play, and not before.
$ curl https://keycloak.novox.internal/
curl: (60) SSL certificate OpenSSL verify result: unable to get local issuer certificate (20)
```
## Where it belongs
`trust list` holds no entry for the mesh. The anchors present are two `mkcert` development roots and
the predecessor's lab root — the report's account of the trust store is unchanged.
A module in the catalogue: it requires `internal-acme-ca`, fetches the root over the mesh's own
network, installs it as a trust anchor, refreshes the machine's bundles, and — because being
unassigned stops its unit, and stopping the unit is what undoes it — takes both away again.
The public name on the same proxy verifies cleanly (`CN=keycloak.novox.be`, Let's Encrypt, return code
0), which places the fault exactly where the report puts it: not in the proxy, not in the authority,
and not in the certificate.
The owner is therefore `mesh-catalog`, module `ca-trust`, and nothing in the control plane.
**The name matters, and the report's "every HTTPS name the mesh serves internally" is too broad.** The
served internal name is `<label>.<node>.internal`. The hosts file also carries
`<label>.<public-domain>.internal`, which nothing serves and which fails differently — that is
[issue 157](../157-a-routed-names-internal-alias-is-served-by-nothing/00-report.md), found while
reproducing this, and it cost the first several minutes of this diagnosis.
## The module exists, and this stays open until a machine holds it
## The authority serves what the module needs
*2026-09-29.* `ca-trust` is in the catalogue and merged
([ADR 0147](../../02-DECISIONS/0147-a-module-anchors-the-meshs-authority.md)), and what it renders
is checked in the control plane's own suite: the script fetches from the authority it was bound to,
and the unit runs it both ways.
`step-ca` is up and healthy, and publishes `roots: /roots.pem` for both `acme-ca` and
`internal-acme-ca`. That endpoint returns PEM:
**No machine has been assigned it, and nothing has verified a name because of it.** The bed written
for that cannot run ([issue 146](../146-the-foundation-cannot-be-raised-on-the-bus-the-mesh-runs-on/00-report.md)),
and the live mesh has not been given the module. So the symptom this record opened on — every
internal name failing verification on every machine — is still true everywhere, and the record stays
`located` until it is not. Closing it on a module that exists would be closing it on an intention.
```
$ curl -sk https://127.0.0.1:9000/roots.pem
-----BEGIN CERTIFICATE-----
MIIBvzCCAWWgAwIBAgIQYa2CkdJk16JyG/dVy2qoEzAKBggqhkjOPQQDAjA+…
```
So `${bound:internal-acme-ca:roots}` in the `ca-trust` module composes to a URL that returns a
certificate, and the module's own check — refuse a body that is not one — is checking the right thing.
Worth recording because it was nearly filed as a defect: step-ca *also* serves `/roots`, which returns
`{"crts":["-----BEGIN CERTIFICATE-----\n…"]}`. That body contains the literal text the module greps
for, so had the module used `/roots` it would have installed JSON into the anchors directory and
reported success. It does not use it. The guard is sound only because the published path is the PEM
one, which is worth knowing before anybody changes either.
## What is actually in the way
**The module is not registered.** The report and the work plan both say it exists and is merged, which
it does — `mesh-catalog modules/ca-trust`, on `main`. But the mesh has never been told about it:
```
$ mesh-controller module list | grep -iE 'ca-trust|step-ca'
step-ca 1 built 67f5f4cf on novox
```
39 of the catalogue's 76 manifests are registered. `ca-trust` is one of the 37 that are not, so it
cannot be assigned to anything — "assign it to one machine" has no module to name.
A dry run confirms it registers cleanly and needs no artifact built: it declares a directory, a script,
a unit and a service, and no image.
```
$ mesh-controller build <catalogue> --path modules/ca-trust --dry-run
… the manifest, parsed and validated
```
## So the remaining work is three steps, not one
1. **Register it** — build it from the catalogue, which pins nothing because it has no artifacts.
2. **Assign it** to a machine. The workstation this was observed on is the honest first choice: it is
where a person meets the fault, and it is where the check can be made with a plain client.
3. **Verify** `curl https://<label>.<node>.internal/` with no flags, and `trust list` naming the mesh.
Then removal, which the module declares and nothing has exercised: unassigning must take the anchor
away and refresh the bundles ([ADR 0147](../../02-DECISIONS/0147-a-module-anchors-the-meshs-authority.md)),
and that is the half most likely to be wrong, because it is the half nobody reaches by accident.
@@ -0,0 +1,104 @@
# 129 — resolved: the workstation trusts the mesh, and stops when told to
*Done and measured on the machine, 2026-09-30.*
## What was done
Three steps, not the one the plan expected — the module was merged and had never been registered
(see [the diagnosis](01-diagnosis.md)):
1. **Registered** `ca-trust` from the catalogue. No artifact to build: it declares a directory, a
script, a unit and a service, and no image.
2. **Assigned** it to the workstation the issue was opened on, and pushed.
3. **Verified** with a plain client, then **unassigned and pushed again** to exercise removal, then
assigned and pushed once more.
The host's own account of arriving:
```
created ca-trust.state (/var/lib/ca-trust)
created ca-trust.anchor (/var/lib/ca-trust/anchor)
created ca-trust.unit (/etc/systemd/system/mesh-ca-trust.service)
updated ca-trust.trust (mesh-ca-trust.service): boot disabled to enabled, stopped to running
```
## It works, by the check the report asked for
The report's own reproduction, with no flags and nothing installed by hand:
```
$ curl -sS -o /dev/null -w '%{http_code}' https://git.novox.internal/
200
$ openssl s_client -connect git.novox.internal:443 -servername git.novox.internal
issuer=O=Mesh Internal CA, CN=Mesh Internal CA Intermediate CA
Verify return code: 0 (ok)
$ trust list | grep -A2 Mesh
label: Mesh Internal CA Root CA
trust: anchor
category: authority
```
Four internal names, all verifying: `git` 200, `umami` 200, `keycloak` 302, `drive` 302. Before this,
every one of them was `curl: (60) … unable to get local issuer certificate (20)`.
**And the consequence the report named specifically**: git over HTTPS to the mesh's forge, which it said
had forced the working clone URL to be ssh-only.
```
$ git ls-remote https://git.novox.internal/novox/hq.git HEAD
76fbe323ea3401fcdadbf500c61bd3fa5a0a8603 HEAD
```
## Removal is symmetric, which nothing had ever shown
[ADR 0147](../../02-DECISIONS/0147-a-module-anchors-the-meshs-authority.md) says the module anchors the
authority *and takes it away again*. That half had never run. Unassigning and pushing:
```
removed ca-trust.trust (mesh-ca-trust.service)
removed ca-trust.unit (/etc/systemd/system/mesh-ca-trust.service)
removed ca-trust.anchor (/var/lib/ca-trust/anchor)
removed ca-trust.state (/var/lib/ca-trust)
```
Then: the anchor file gone, `trust list` naming no authority of the mesh's, and the plain client back to
`unable to get local issuer certificate (20)`. A machine that leaves the mesh stops trusting it, as the
record claims.
**The order is what makes it work, and is worth saying.** The service is removed *first*, so systemd
runs the unit's `ExecStop` — which is what deletes the certificate and refreshes the bundles — while the
script it calls still exists. Had the script or the state directory gone first, stopping the unit would
have had nothing to run, and the anchor would have been left behind with nothing declaring it. Nothing
in the module says this; it is the host's removal order that makes the module's symmetry real.
## What this leaves
- **Three machines of four** *(extended the same day, after the first was proven)*. `ca-trust` is now
assigned to every converged machine, and each verifies with a plain client:
```
novox mesh CA in trust store: 1 https://git.<node>.internal/ -> 200
g14 mesh CA in trust store: 1 https://git.<node>.internal/ -> 200
shanks mesh CA in trust store: 1 https://git.<node>.internal/ -> 200
```
Before, each of the two that had not been assigned it answered
`curl: (60) … unable to get local issuer certificate (20)` and held no entry for the mesh.
**`ace` is deliberately not among them.** It is the adopted machine, still carrying the
predecessor's resolver and filter, and it is not converged until the network and module-assignment
work is settled. Assigning a module to it would hold rather than run
([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)), which is
correct and is not the same as trusting anything.
- **It arrives per assignment, which is a shape worth questioning.** The report's reasoning — that
being on the private network is what makes a machine one that speaks to the mesh's names — argues
for a fact carried to every machine on the network, the way the roster and the registry trust are.
ADR 0147 chose a module assigned per machine, and this record does not reopen it; the cost is that
a machine joining the mesh trusts nothing until somebody remembers a second command.
- **`service-manager` reports `degraded` on this machine** and the module ran anyway. Worth knowing that
the capability gate passes on a degraded service manager, since a module whose whole delivery is a
unit is the kind that would be worst served by one.
- **The predecessor's authority is still in the trust store**, beside the mesh's now rather than instead
of it. The report notes that it cannot be retired while anything on the machine speaks TLS to a mesh
name; that is no longer true here, and retiring it is its own piece of work.
@@ -1,9 +1,9 @@
---
status: open
status: resolved
opened: 2026-09-28
located-in: [mesh-catalog, mesh-controller internal/catalogue]
fixed-by:
amended-design:
fixed-by: mesh-controller PR 169 (the check, module check, the catalogue-wide test); mesh-catalog PR 188 (the catalogue that passes it); ADR 0155
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
---
# 134 — A definition may still name the mesh, and the check that would say so does not exist
@@ -64,3 +64,17 @@ that.
hostnames above are the first real cases.
- Should a build context name a repository on the git seat rather than by URL, and if so, what does
that mean for a context in *another* mesh's forge?
## Resolved, 2026-09-30
The check exists: `InstallationProblems`, run by `module check` and by a catalogue-wide test. Run over
the 77 definitions it found 42 values, not 15 — the by-hand count had missed a second name one
character after the first on the same line, which is the kind of thing a check is for. The three open
questions: **a domain in a `why` string does not break the rule**, prose is not judged, and the eight
were rewritten anyway because this catalogue is public; **a service's public name is the name the mesh
composes for its route**, read through the route's binding, and an operator's own value is a setting;
**a build context names a repository on the git seat**, `seat: git` with the path, and a context in
another mesh's forge stays a URL, which the check reports and `names-on-purpose` would declare. The
seven values that remain are declared with their reason — four applications built outside the mesh —
and are the list that shrinks ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)).
Registration does not refuse yet; it will when the list has been empty for a release. *2026-09-30, later the same day:* it refuses — the list was empty the day the check landed, and the operator asked for it (mesh-controller PR 175); `module add` and a build's result are refused in the check's words, with the way out, and the build stays recorded.
@@ -68,3 +68,31 @@ container runtime's shape, not a choice; the answer is to recreate, which is wha
that would rather re-read a roster from a file can already ask for one as a fact
([ADR 0120](../../02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md)) and restart on
it.
## The container was umami, and it had its own record (2026-09-30)
The container in the table above is umami, and its symptom had already been filed three days earlier as
[issue 118](../118-umamis-store-answers-the-dial-and-times-out-the-query/00-report.md) — the store
answering the dial and timing out the query, a restart loop, and a public name answering `502`. Neither
record noticed the other: 118 was reasoning about path-MTU and conntrack on the network between the
container and the store, which is what a stale name looks like from inside the container.
118 is resolved by this record's fix and says so. Noted here because a symptom filed twice, diagnosed
once, and closed in one place is how a repository comes to disagree with itself.
## What replaced this fix (2026-09-30)
The fix here — putting the mesh's names into the digest the host compares, so a container whose names
moved is recreated like one whose image moved — worked, and cost more than it was worth. It made the
roster part of every container's identity, so one name moving replaced every container in the mesh: a
module assigned on one machine restarted the store, the registry, the edge and mail on another
([issue 151](../151-a-new-name-recreates-every-container-in-the-mesh/00-report.md)).
[ADR 0148](../../02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md) goes at
the cause this record only described: **a copy taken at creation is stale the moment the roster moves,
and detecting that is not as good as not copying.** A container resolves the mesh's names through its
machine's resolver, at the moment it asks, so the fault this record reports cannot occur rather than
being noticed a restart later.
Recorded here because this is where somebody arrives to find out why the digest carries names, and the
answer is that it did, for two days short of a month, and stopped.
@@ -1,9 +1,9 @@
---
status: open
status: resolved
opened: 2026-09-28
located-in: [mesh-controller internal/catalogue, mesh-catalog]
fixed-by:
amended-design:
located-in: [mesh-host internal/profile/detectors.go (no detector for the network manager), mesh-host cmd/mesh-host (the profile is detected at enrolment only), mesh-controller internal/link (a report carries no profile), mesh-catalog modules/networkmanager, systemd-networkd, dhcpcd (declare no capability of their own)]
fixed-by: mesh-controller PR 192 (a report carries the profile), mesh-host PR 61 (uplink-<manager> detected and reported with every apply), mesh-catalog PR 206 (each holder declares its own)
amended-design: [03-DESIGN/01-to-be/26-the-seats.md]
---
# 138 — Two modules claim one seat and are not interchangeable, and nothing says so
@@ -54,3 +54,25 @@ able to switch the manager, which ADR 0117 refuses for a reason that has not cha
alternative is one module that speaks whichever dialect the machine needs, chosen from the report.
- What should happen on a machine that switches manager afterwards? The seat would then be held by the
wrong module, and the machine is the only place that knows.
## Decided, 2026-10-01
[ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md), rule 3: the host's profile gains one
capability per network manager found active, each holder declares its own, and the existing
capability refusal does the rest, naming it. The profile is detected again by every apply and
travels in the report, so a machine that switches managers is refused at its next push. The uplink
stays one seat; the capability picks the dialect. Order of building: controller (a report may carry
a profile), host, then the three definitions.
## Resolved, 2026-10-01
Every machine now reports which network manager it runs — the control node `uplink-systemd-networkd`,
the laptop and the workstation `uplink-networkmanager`, the home server both `uplink-dhcpcd` and
`uplink-networkmanager`, which is its truth — renewed with every report, and each holder declares the
capability it needs, so the wrong holder is refused on assignment with the capability named. The uplink
stays one seat; the capability picks the dialect. A machine that switches managers is a machine whose
holder lacks a capability at its next push.
*How it is checked:* the host's detector test per manager; the controller's test that a report's
profile replaces the enrolled one; the capability refusal's existing tests; live, `node show <machine>`
lists `uplink-<manager>` for each.
@@ -1,9 +1,9 @@
---
status: open
status: resolved
opened: 2026-09-28
located-in: [mesh-controller internal/catalogue]
fixed-by:
amended-design:
located-in: [mesh-controller internal/catalogue/declaration.go (composeName took the consumer's own name as the internal domain)]
fixed-by: mesh-controller PR 163 — the internal name composes under the node whose proxy serves the route (ADR 0151, 2026-09-30)
amended-design: 03-DESIGN/01-to-be/08-connectivity.md
---
# 139 — An internal route name resolves to the consumer's node, not the one that serves it
@@ -49,3 +49,15 @@ resolve whether or not anything answers.
and if so, is `route` still one mesh-wide provision or a node-scoped seat with a mesh-wide fallback?
- What certifies the name in either case? The certificate is obtained by whoever terminates TLS, and
that is the question above in another form.
## Answered (2026-09-30)
[ADR 0151](../../02-DECISIONS/0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md):
the internal name is composed under the node that serves the route — the machine the request arrives
at — because `<x>.<node>.internal` means *goes to that node* and nothing else. The public name stays
the consumer node's, which is where the operator put it. The first question is answered that way; the
second, a per-node route holder, is a decision about seats and is left where it is; the third is
unchanged, since the proxy that terminates the name is given it and certifies it.
mesh-controller PR 163 carries it. On this mesh every route is served beside its module, so no name
changed; the controller's tests hold the case where it would.
@@ -1,5 +1,5 @@
---
status: located
status: resolved
opened: 2026-09-28
located-in:
- mesh-controller internal/catalogue/manifest.go
@@ -7,7 +7,7 @@ located-in:
- mesh-controller internal/catalogue/declaration.go
- mesh-controller examples/route-proxy
- mesh-catalog (every routed module manifest)
fixed-by:
fixed-by: mesh-controller bdf965d (a module names its endpoints) and c68d3a7 (an assignment configures an endpoint as one thing) — the filter, the proxy's names and both authorities now read one statement
amended-design: 03-DESIGN/01-to-be/08-connectivity.md
---
@@ -0,0 +1,47 @@
# Resolution
*2026-09-29.*
**Built, and this record did not say so.** The issue was written on 2026-09-28 and answered the same
week by two commits in `mesh-controller`; nothing came back to close it, so the mesh's own account of
itself said for a day that reach was declared nowhere while the code read it in three places.
- `bdf965d` — *a module names its endpoints, and a route names the one it serves*. `listens[].name`
is the endpoint; a route contribution names the endpoint rather than repeating a port.
- `c68d3a7` — *an assignment configures an endpoint as one thing*. The `endpoints` settings key, per
node, by endpoint name: `{"endpoints": {"ssh": {"port": 20134, "reach": "public"}}}` — port, label
and reach in one block, which is what [ADR 0138](../../02-DECISIONS/0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md)
asked for and what [ADR 0046](../../02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md)
said configuration is.
## The three readers, which is what the issue was about
The complaint was that the per-node source override had exactly one caller. It now has three, and
they are the three mechanisms reach was decided to settle at once:
| reader | what it does with it |
|---|---|
| the filter | `Reaches` turns each endpoint's reach into the rule for its machine port |
| the proxy's names | `composeName` composes the public name, the internal name, or both — and a name nobody asked for is not composed |
| the authorities | the proxy certifies only names it was actually given, each from its own authority, through two host policies rather than one |
**A routed endpoint keeps the manifest's port**, which is ADR 0138's own insight and older than it
([ADR 0045](../../02-DECISIONS/0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md)): the
proxy is how it is reached, so `public` there asks for a public *name*, not an open port.
**An endpoint that is not routed is reached and never named.** Git over ssh is that case — the one
the issue said the model could not express — and it is now the ordinary one.
## How it is checked
`internal/catalogue/endpoints_setting_test.go`: a block says port, label and reach; a block may say
only a reach; a name the module does not declare is refused; a reach outside the four values is
refused; and saying the same thing twice — once in the block, once through the older per-port keys —
is refused rather than resolved by whichever is read last. The proxy's half is `policy_test.go` and
`authority_test.go`: a name the mesh did not send is not certified, by either authority.
## What is left, and it is not this
The older keys (`ports`, `expose`, and reach keyed by port) still work beside the block. They are
what the block replaces, and retiring them is its own small change — not a gap in what reach can
say.
@@ -74,3 +74,18 @@ every future change of this shape, and it was paid today.
argues for the former.
- Does the same gap apply to the launcher and the units beside the binary, which are also files no
declaration names?
## What now waits on this (2026-09-30)
[Issue 107](../107-a-declaration-carries-no-order/00-report.md) — a declaration carries no order, so a
host cannot tell an older one from a newer. Its fix adds a field to the declaration, and a host refuses a
declaration carrying a field it does not know, **whole**. So it is a flag day: every host upgraded, then
the controller.
With nothing delivering a host, that means placing a binary by hand on every machine and unparking the
adopted one, and a machine missed in the sequence is a machine the mesh cannot send anything to at all.
107 is held for that reason rather than for anything in its own diagnosis.
**This is what makes a declaration field cost a rollout instead of an expedition**, which is a use for
this record beyond keeping machines current: it is the thing standing between the mesh and its own
protocol evolving.
@@ -0,0 +1,185 @@
# 142 — the mesh can now build and publish its own host
*2026-09-30. Both of ADR 0141's named gaps are closed; the host is not yet placed by the mesh.*
## What ADR 0141's insight named, and what each cost
> The remaining work is a way to build the host and a way to name a version in a path, and until both
> exist nothing delivers a version and every machine takes the fallback.
**1. Nothing could compile it.** The toolchain list was a closed set of typescript and python, and its
own warning — every language is another implementation of the contracts modules share, so adding one
commits to keeping N implementations in step — does not attach to Go. Go is how the host, the control
plane and the builder are written, and none of them is a module in that sense: the host is what
*applies* modules.
Closed by a Go toolchain naming a new base module, `mesh-tools-go`. Named and not pinned, so the mesh
answers with the copy it holds and moving compiler is a build rather than an edit to the control
plane's source ([ADR 0044](../../02-DECISIONS/0044-a-public-name-is-provisioned-like-any-capability.md),
[0142](../../02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md)).
**2. A version could not reach the path.** An archive named a fixed path and nothing interpolated the
build into it, so nothing could ask for `…/versions/<version>/`.
Closed by `${version}` in any value of a resource that uses an archive or a bundle. **The version is
the artifact's digest, short, and not the commit**: two builds of one commit are meant to be the same
bytes — the toolchains are `-trimpath` for that — so a content-addressed version means an unchanged
build resolves to the path it already had, where a commit-named path would move for an identical binary
and recreate everything reading it.
## Three more things were in the way, and none was in the record
Found by doing it, in the order they appeared:
- **`sourcesFor` turned every entrypoint into a `.ts` file.** One language's file extension, written
into the code that serves every language. The extension is the toolchain's now.
- **The output directory was left to the compiler.** `tsc --outDir` makes one; `go build -o` writes into
a directory and does not create it, failing with a message about a path rather than about a build.
Made for every toolchain, because which compilers are forgiving is not something a reader should have
to know.
- **A bundle was refused if it named what it is built from.** The reason — a bundle is the module's own
directory compiled whole — holds for an interpreted language and cannot hold for a compiled one: a Go
repository carries several commands, the host and its bootstrap among them, and "the module's own
directory" is then not a package at all. A compiled bundle may now say which package; the refusal
stands for every interpreted one.
And a fourth, in the base module itself: its first Dockerfile ran `apt-get`, and the golang image the
mesh holds is Alpine. That is
[issue 136](../136-a-module-may-name-a-program-the-machine-does-not-have/00-report.md) in an image
rather than on a machine, and the build refused rather than a module failing later — which is the
behaviour that issue wants.
## Measured: the mesh compiles its own host and publishes it to its own registry
```
$ mesh-controller build <the host's repository>
host-arch bundle artifact-store://mesh-host/host-arch/blobs/sha256:ad62528c…
mesh-host 1, built on novox from b5196e97
```
Fetched from the mesh's registry and opened:
```
mesh-host: ELF 64-bit LSB executable, x86-64, statically linked, stripped
$ ./mesh-host help
mesh-host — the node host
```
Statically linked matters: what a machine holds is a file rather than a container, so a binary needing
a libc it did not bring is a delivery that works until a machine differs.
**The host is a module now**, with one bundle for `arch` built from `cmd/mesh-host`. A system is
required for a compiled artifact because a binary is pinned at link time so a host refuses to touch a
machine it was not built for ([ADR 0005](../../02-DECISIONS/0005-the-node-host.md)); `arch` is what all
four of this mesh's machines report themselves to be, and another system is another artifact and
another build.
## What is left
**The host module declares no resources**, so nothing places the built bundle on a machine yet. That is
the next piece and it is the one with the interesting question in it: the resource is an archive
unpacked to `…/versions/${version}/`, applied by the host that is running, and the bootstrap is not
circular because the two are different versions ([ADR 0141](../../02-DECISIONS/0141-the-host-delivers-its-own-successor.md)).
The host half of that mechanism — versions side by side, the newest runs, the running one stands aside
between reconciles, rollback picks a directory — is built and tested and has never had a version to
work on.
**One thing worth noticing while writing it.** The systems list exists because "the difference between
two of them is a C library, not a kernel" — and a static Go binary has no C library. So one build would
in fact run on all three. The pin is then a policy (a host refuses a machine it was not built for)
rather than a necessity, which is a reasonable thing to keep and is worth knowing is a choice.
**And a sharper one, asked as a question and worth its own record.** The artifact's system is validated
and then read by nothing: it does not reach the compiler, no machine is matched against it, and nothing
chooses between two artifacts by it. The host built here is x86-64 because the build machine is, not
because anything in the declaration said so — correct for this mesh by coincidence. That is
[issue 159](../159-an-artifacts-system-is-checked-and-then-ignored/00-report.md).
## Delivered, started, and one fact short (2026-09-30, later)
The loop closed. The launcher is delivered as a **file** resource rather than inside the archive, and
that difference is the safety: a file is written atomically — temp file, then rename — so the running
launcher keeps the inode it was started from, where an archive writes in place with truncate and would
cut the script a running shell is reading. The manifest carries a second copy of the launcher and a
test refuses any difference from `packaging/nox-mesh-host-launch`.
On the workstation, in order: the version landed, the launcher was replaced, the running host saw a
delivered version and stood aside, and after one restart of the unit the launcher started
`/usr/lib/nox-mesh-host/versions/637f65559d16/nox-mesh-host`. **A host the mesh compiled, published,
delivered and started.**
It would then have refused the first declaration it was asked to apply. The host's Makefile links in
two facts the mesh's toolchain does not, and one of them — the system it was built for — is read before
anything is applied. That is
[issue 161](../161-a-delivered-host-carries-none-of-its-link-time-facts/00-report.md), and the machine
is back on its hand-placed binary until it is answered.
**The fallback is what made that safe**, and it was not luck: the launcher runs the pinned version, or
the newest delivered one, or the one placed by hand — so moving the delivered versions aside restored
the machine in one step.
## Self-update works (2026-09-30, end of the day)
```
running: /usr/lib/nox-mesh-host/versions/093231796eb0/nox-mesh-host
mesh-controller node show shanks
host 093231796eb0
```
One machine runs a host the mesh compiled, published, delivered and started, applying declarations and
reporting the version it was delivered as. The two facts a delivered binary was missing are
[issue 161](../161-a-delivered-host-carries-none-of-its-link-time-facts/01-resolution.md) and resolved:
the system comes from the artifact, the version from where the binary sits.
**Three machines still run a hand-placed host**, and rolling each forward is one assignment and one
push. The control node is worth last.
One thing this found on the way out: an archive cannot be undeclared, and the attempt stops the machine
applying anything at all — [issue 162](../162-an-archive-cannot-be-undeclared/00-report.md). It is how
undoing the first delivery froze the workstation, and it is not specific to the host.
## Every machine self-updates (2026-09-30, evening)
```
shanks 76f4566bef3d/nox-mesh-host active
g14 76f4566bef3d/nox-mesh-host active
novox 76f4566bef3d/nox-mesh-host active
ace 76f4566bef3d/nox-mesh-host active
mesh-controller status: (no host split)
```
The last delivery was unattended on all four: the fixed host was built, pushed, each machine stood
aside exactly once for the genuinely newer version, and the delivered launcher started it — no
restart by hand. A following push that delivered nothing new was applied and reported by every
machine and stood nobody aside, which is the check
[issue 163](../163-a-delivered-host-stood-aside-on-every-push-and-reported-nothing/00-report.md) asks
for.
**Two more faults on the way, both mine, both found by reading the machine rather than the success
line.** A delivered host compared the newest delivered version against its link-time stamp rather
than the version it was running, so it stood aside on every push and — because standing aside cancels
the report — never reported again (163). And the adopted machine kept its found launcher as the
adoption rule says, so the delivery there needed a `take` before the launcher moved.
**The crossover needs one restart of the unit per machine, once.** The launcher process that was
running on each machine was the old script, executing from its own inode; a new file beside it
changes nothing until the unit restarts. Every subsequent delivery is unattended.
**Timing, measured:** on a machine, hearing a declaration to reporting it applied is about three
seconds. A push as the operator sees it takes 17–20 seconds, and the difference is the control plane
composing the declaration before it sends. A `--wait` shorter than that reads as "did not report" for
a machine that did; the three-minute default read as slowness for a machine that never would. Neither
number is a defect being chased here, and both are worth knowing before reading a push's answer.
## What this leaves
- [Issue 162](../162-an-archive-cannot-be-undeclared/00-report.md): an archive cannot be undeclared, so
the host module — and any module with an archive — cannot be unassigned, and trying stops the machine
applying anything.
- [Issue 107](../107-a-declaration-carries-no-order/00-report.md) is unblocked: a declaration field is
now a build and a push rather than an expedition.
- Three stale version directories on the workstation from the first attempts, moved aside under
`/var/lib/mesh-host/versions-held-back/`, and a backup of the adopted machine's hand-placed binary
beside its state. Both are safe to delete and are not the mesh's to delete.
@@ -4,7 +4,7 @@ opened: 2026-09-29
located-in:
- mesh-controller internal/catalogue/filtering.go (fixed for this instance)
- mesh-controller (what status reports, and what it does not ask)
fixed-by:
fixed-by: partly — mesh-controller 1da96e8 makes the report state its own scope; nothing dials a provision yet, which is ADR 0146 and is not built
amended-design: 03-DESIGN/01-to-be/10-delivery.md
---
@@ -0,0 +1,64 @@
# 145 — partly resolved: the report says what it is not a claim about
*2026-09-30.*
## What was done
The sentence that was true for eleven hours now states its own scope, immediately below itself:
```
4 machine(s), all doing what they were told, all heard from, running what the mesh would send
them, and every module current with its source
That is the mesh and the machines agreeing. Nothing here dials a provision:
no grant the mesh composed has been tested, so a module unable to reach what it
requires would not appear above (04-ISSUES/145)
```
That is the whole of what this change does, and it is deliberately small. It does not check anything.
It closes the distance between *the machines are as the mesh described them* and *it works* by naming
it, and that distance is where the eleven hours went: the report was read as the second and only ever
meant the first.
Two other reports in this group now carry real information they did not
([issue 125](../125-a-hold-is-not-a-line-in-the-apply-report/00-report.md): a hold is a line in the
apply report and breaks the all-well sentence;
[issue 087](../087-the-controller-cannot-tell-a-host-is-too-old/00-report.md): the mesh knows which
host runs a machine). Neither would have caught this fault, and both were the same shape of blindness.
`printStatus` is now separated from the asking, so these words can be read by a test with no store, bus
or machine — they have been acted on and been misleading twice, which makes them worth holding still.
## What is NOT done, and why this record stays open
**Nothing dials a provision.** [ADR 0146](../../02-DECISIONS/0146-connectivity-is-checked-by-name-per-hosting-form.md)
decides how it should be done — a module on every machine serving an endpoint of its own and dialling
every other machine's, one name per hosting form, over TLS with the certificate verified. **Nothing is
built**, the module that existed was deleted, and the work was deferred deliberately by the operator.
It is not this record's to start.
So the measured fault of this issue — that the mesh can only report on itself — is unchanged. What
changed is that the report no longer implies otherwise.
## The open questions, where they stand
- *Should a grant be checked, and from where?* Answered by ADR 0146 and not built: from the position
the callers are in, by a module on every machine, per hosting form.
- *What would it cost to be wrong in the other direction?* Unanswered and important. A check that
reports a provision broken while it works trains a reader to ignore the report, which is the failure
this whole issue is about arriving by the other door.
- *What should `status` say about a machine whose modules cannot reach each other?* Answered in part:
until something checks, it says that it has not checked. What it says when a check exists is ADR
0146's to settle.
- *Is there a cheaper signal than a probe?* Still open. The affected module logged the failure 6,154
times; the mesh reads no module's logs and arguably should not, but something a module could *say*
about its own provisions would have surfaced this in minutes.
- *Does the same blindness apply to a provider that lost a consumer's grant?* Still open, and still
nothing checks it.
## One thing worth carrying forward
**The certificate half of ADR 0146 is now possible where it was not.** Its check requires an internal
name fetched over TLS with the certificate verified, and until 2026-09-30 no machine trusted the mesh's
authority at all ([issue 129](../129-nothing-makes-a-machine-trust-the-meshs-authority/02-resolution.md)).
Three of four do now. Whoever builds 0146 no longer has to solve that first.
@@ -71,3 +71,15 @@ against a suite nobody can execute.
- The lab rewrites a bundle's registry-prefixed third-party references to upstream ones for a
machine with an uplink (`test/integration/harness.ts`); the new bundle's bus reference needed
that rule added, which is done and is not this issue.
## What one of its fixes then did to a running mesh (2026-09-30)
The change that stopped the doubling — putting the stream into a push consumer's delivery subject —
is correct on a foundation being raised and fatal on a mesh that is already running: the server will
not move that subject while a subscriber is bound, and a node is bound to its declaration consumer
the whole time it is up. The control plane crash-looped on the first build that carried it.
Recorded and fixed as [issue 156](../156-moving-a-consumers-delivery-subject-stops-the-control-plane/00-report.md).
Noted here because this record is where somebody will arrive when reading why the subject carries the
stream at all, and the answer is incomplete without it: **the raise path was the only one exercised,
and it is the one path on which nothing is bound.**
@@ -1,7 +1,9 @@
---
status: located
status: resolved
opened: 2026-09-29
located-in: [nothing in the mesh — the surface in use is the predecessor's, installed on the workstation]
located-in: [mesh-catalog modules/mesh-console, mesh-tools src/mesh.ts, mesh-controller internal/broker]
fixed-by: ADR 0152; mesh-controller PR 164 (invokes); mesh-tools PR 20 (mesh serve, the tools verb); mesh-catalog PR 181 (mesh-console)
amended-design: 03-DESIGN/01-to-be/34-the-console.md
---
# 147 — the operator's tools still dial the bus that was removed
@@ -73,3 +75,28 @@ outside the mesh.
- It fails identically for the local machine, which rules out reachability and points at the
transport alone.
- The mesh's own traffic over the new bus is unaffected: nodes report, declarations apply.
## Answered, 2026-09-30
The work order's question — does the mesh grow its own operator surface, or is the surface an
ordinary module — is answered by [ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md):
**the console is a module.** `mesh-console` is assigned to the machine a person sits at, holds a
credential the mesh minted, calls tools under a grant its manifest declares (`invokes`), and serves the
mesh's tools on that machine's loopback to an agent over MCP and to a person through the same endpoint.
Designed in [34 — The console](../../03-DESIGN/01-to-be/34-the-console.md).
What that leaves, said plainly so nobody reads this record as closed on the whole of its first
paragraph: the console reaches every tool a *module* serves. The mesh's own questions — what a node
runs, what is assigned — are the `mesh-controller` seat's tools under ADR 0132 and are not on the bus
yet; for those a shell is still the way, and design 33 is where that closes.
The predecessor's program on the workstation is not replaced by the mesh; it is left where it is and
the assistant is pointed at the console beside it. The `hal` entry in the assistant's configuration
still names things that are not the mesh's.
**Verified live, 2026-09-30 evening.** The four pull requests merged; the console was registered
(checked first with `module check`), built, assigned to a workstation, issued a bus account, and pushed.
On that machine `tools/list` answered on loopback with 62 tools and named 36 modules as not answering,
and a call to the forge's `gitea_list_repos` returned repositories. The assistant on that machine now
lists the console as a connected MCP server beside the predecessor's program, which was left where it
is. As-is: [`13-the-console.md`](../../03-DESIGN/00-as-is/13-the-console.md).

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