Compare commits

...
Author SHA1 Message Date
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 d0044cf555 Issue 170: assigning a module claims every seat it could hold
Assigning postgres on ace claimed mesh-store and made novox's own store
assignment unresolvable. The resolver reads a manifest's claims as 'does
hold'; ADR 0110 says the assignment holds, by a deliberate act the controller
already has (seat …, HoldSeat) but resolution does not consult.
2026-09-30 14:29:11 +02:00
jschoubben 105ae9a56a Issue 169: network-share is the module responsible for a node's network shares — a node role 2026-09-30 13:56:24 +02:00
jschoubben ee801a6441 Issue 169: the consumer half is a module (network-share), not a host resource kind; the access-on-a-mountpoint check is the data-loss case 2026-09-30 13:56:07 +02:00
jschoubben 90b89aa1c9 Issue 169: the consumer's half — the host mounts the share, a mount is a resource of the consuming module, not a client module 2026-09-30 13:53:32 +02:00
jschoubben e33191161d Issue 169: two module-defined seats, and the gap — a seat definition has no neutral home
nfs-share and smb-share share an intent, not a contract; one seat would be
a union with every field optional. What the exemplar exposes is that a
seat declared inside one module cannot be implemented by another without
depending on it.
2026-09-30 13:52:20 +02:00
jschoubben a0a930b1cd Issue 169: file sharing is a core seat, one per protocol
The operator's call: a node-scoped system seat family (node-nfs-share,
node-smb-share, …) defined by the control plane, one per protocol as
package registries are (ADR 0109), so several modules occupy it and a
machine may hold both.
2026-09-30 13:50:31 +02:00
jschoubben 8d9c9ab6b5 Issue 169: a machine shares its files, and the mesh does not know
ace exports the operator's media library over NFS and Samba with host
services no module declares: they close at converge, nothing owns their
configuration, and no module elsewhere can require the share. Proposes a
file-sharing module that accesses the paths, holds a node-scoped seat it
defines, and provides file-share to consumers.
2026-09-30 13:49:25 +02:00
jschoubben 49b1136ded Issues 164-168: found provisioning every dependency on ace
164 a credential that must be accepted is minted anyway
165 one accepted value must be accepted once per consumer
166 a requirement cannot be optional
167 code several modules share has no home
168 a setting reaches every file and every contribution
2026-09-30 13:28:53 +02:00
jschoubben 743051efe7 Issue 163 (was 161): another record took 161 on main first 2026-09-30 13:28:22 +02:00
jschoubben e8470057aa Merge remote-tracking branch 'origin/main' into issue/161-an-assignment-does-not-record-its-provider 2026-09-30 13:28:22 +02:00
jschoubben 39340fcd76 Issue 161: an assignment does not record which provider answers it
ADR 0110 decided each assignment records where its requirements are answered
from; the control plane keeps only a per-machine pin (none recorded) and
resolves every requirement implicitly. Harmless with one provider; a second
one silently moves consumers' data.
2026-09-30 11:54:31 +02:00
84 changed files with 3838 additions and 148 deletions
+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 - **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. **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 - **artifact** — anything a build produces and the mesh delivers to a machine by **digest**: an
**digest**. Served by the **artifact-store** (distribution). Every node pulls from it. `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). - 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 ## 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 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. 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 ## 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 A new name for an existing thing lands here first, in the same change that introduces it in code. A
@@ -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 would be how installation-specific detail arrives into documents that must not carry it
([`README.md`](../README.md)). ([`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 ## Consequences
**This repository stops being a fourth knowledge system, properly.** The original objection was **This repository stops being a fourth knowledge system, properly.** The original objection was
@@ -106,3 +106,14 @@ is refused with the candidates named, never resolved by picking.
gap, and the day-one evidence. 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) - [`03-DESIGN/01-to-be/23-choosing-a-provider.md`](../03-DESIGN/01-to-be/23-choosing-a-provider.md)
— the design. — 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 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. 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 ## How it is checked
A tools-only bed asks a served tool through the control plane and asserts an answer arrived — A tools-only bed asks a served tool through the control plane and asserts an answer arrived —
@@ -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 **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. 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 ### Until an adapter can
**An adapter that cannot yet ensure a second credential says so.** The two-party credentials it applies **An adapter that cannot yet ensure a second credential says so.** The two-party credentials it applies
@@ -81,7 +81,9 @@ closed set stays what its name says it is: the *system's* roles, not everyone's.
- **`the-uplink` → `node-uplink`** ([ADR 0125](0117-a-machines-uplink-is-a-seat.md)). Unheld, so it - **`the-uplink` → `node-uplink`** ([ADR 0125](0117-a-machines-uplink-is-a-seat.md)). Unheld, so it
renames with no migration. renames with no migration.
- **The registry seats — `the-artifact-store`, `npm-package-registry` (→ `mesh-artifact-store`, - **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 *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 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. the same pass as the node-* renames, so they keep their names until done deliberately.
@@ -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
+12
View File
@@ -168,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) - **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) - **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) - **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 ### Its tiers, from the bottom up
@@ -256,6 +265,8 @@ python3 00-META/checks/index.py fail if stale
- **0146** — [Connectivity is checked by name, per hosting form, with a valid certificate](0146-connectivity-is-checked-by-name-per-hosting-form.md) - **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) - **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) - **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 ### How it is built
@@ -297,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) - **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) - **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) - **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 --> <!-- index:end -->
+40 -60
View File
@@ -1,78 +1,58 @@
--- ---
layer: as-is layer: as-is
status: implemented status: implemented
code: [hal] code: [mesh-catalog modules/records, mesh-catalog modules/mesh-console]
updated: 2026-08-23 updated: 2026-09-30
decisions: [] 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 # Knowledge
The mesh keeps two knowledge stores. They are not redundant, and knowing which is which is the **The mesh keeps no knowledge store.** What it knows is what its modules answer, and the way a person
difference between finding an answer in one search and rediscovering it over several hours. 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 Until the cut-over of 2026-09-28 the predecessor ran two stores: an operational memory of notes
note is a slug and a body: how something works, what went wrong, what the fix was, what indexed on symptoms, and a structured archive of governed documents with a librarian approving
assumption turned out to be false. 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 ## The record
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.
Its content is overwhelmingly the record of previous debugging: a large body of **The design record is read where it is written.** Since 2026-09-30 a module, `records`, keeps a
troubleshooting entries, module conventions, and standing notes about work that is open. It is checkout of this repository from the forge — cloned from the `git` seat, reset to the origin on every
the mesh's institutional memory of *what has already gone wrong*. 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 It is listed by the console beside every other tool, with a description that says to search the
from scratch, over hours, in sessions where the search was skipped because the trail felt literal words of a symptom before forming a hypothesis. That is what
confident. It fires hardest on familiar ground, not unfamiliar ground. [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 ## The constitution
search. Where the operational memory is a note, this is a document with an owner and a
lifecycle.
Content is promoted through tiers — private, then team, then platform — with a librarian agent [`00-META/how-we-build.md`](../../00-META/how-we-build.md) is the source of the mesh constitution
owning approval and promotion at the boundary. Proposals to edit are reviewed rather than ([ADR 0021](../../02-DECISIONS/0021-hq-is-the-source-of-the-constitution.md)). The page it used to be
applied. 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.
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.
## Where this repository sits ## Where this repository sits
This repository is a third thing, and the objection was raised when it was created: a fourth A third thing beside two that are gone, which makes it the first: the one governed record the mesh
knowledge system repeats the mistake the split was made to fix. has, public, read by a module the mesh assigns, and edited nowhere else.
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.
+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 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. 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 ## Where this differs from the design
**Capacity is not implemented.** The design's vocabulary has a seat with a capacity, and a **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 | | [`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 | | [`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 | | [`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 | | [`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 | | [`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 | | [`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 | | [`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 | | [`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 ## What these documents are not
+11 -1
View File
@@ -2,8 +2,9 @@
layer: to-be layer: to-be
status: in-progress status: in-progress
code: [mesh-host] code: [mesh-host]
updated: 2026-09-29 updated: 2026-10-01
decisions: decisions:
- 02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md
- 02-DECISIONS/0141-the-host-delivers-its-own-successor.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/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 - 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 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. 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** **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 ([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 keeps its mode and owner, a unit present with no record keeps its state and boot setting, a
+13 -1
View File
@@ -8,8 +8,9 @@ code:
- mesh-host packaging/nox-mesh-host-network.sh - mesh-host packaging/nox-mesh-host-network.sh
- mesh-controller internal/token - mesh-controller internal/token
- mesh-controller internal/inventory/nodes.go - mesh-controller internal/inventory/nodes.go
updated: 2026-09-23 updated: 2026-10-01
decisions: 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/0100-a-node-in-use-is-adopted-before-it-is-converged.md
- 02-DECISIONS/0004-a-node-and-how-it-joins.md - 02-DECISIONS/0004-a-node-and-how-it-joins.md
- 02-DECISIONS/0005-the-node-host.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 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. 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, 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) 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 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/catalogue/build.go
- mesh-controller internal/inventory/secrets.go - mesh-controller internal/inventory/secrets.go
- mesh-controller cmd/mesh-builder - mesh-controller cmd/mesh-builder
updated: 2026-09-12 updated: 2026-09-30
decisions: decisions:
- 02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md - 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/0009-modules-and-the-graph.md
- 02-DECISIONS/0010-delivery.md - 02-DECISIONS/0010-delivery.md
- 02-DECISIONS/0005-the-node-host.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 unbuildable — pointed at the catalogue the builder finds no manifest, pointed at a module's source
it finds no manifest either.* 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 ## The manifest in the repository is not the manifest the mesh holds
A resource names an artifact: A resource names an artifact:
@@ -5,7 +5,7 @@ code:
- mesh-controller internal/inventory/secrets.go - mesh-controller internal/inventory/secrets.go
- mesh-controller cmd/mesh-controller/rotate.go - mesh-controller cmd/mesh-controller/rotate.go
- mesh-controller examples/postgres-provisioner - mesh-controller examples/postgres-provisioner
updated: 2026-09-21 updated: 2026-10-01
decisions: decisions:
- 02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md - 02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md
- 02-DECISIONS/0009-modules-and-the-graph.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 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, secret the vault provides. What this page proves for a database password holds, by construction,
for a secret from the vault. 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 ([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. 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 **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 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 silently omits this material looks identical to one where nothing matched — the same rule as the
+32 -5
View File
@@ -5,8 +5,9 @@ code:
- mesh-controller cmd/mesh-builder - mesh-controller cmd/mesh-builder
- mesh-controller internal/builder - mesh-controller internal/builder
- mesh-catalog modules/builder - mesh-catalog modules/builder
updated: 2026-09-30 updated: 2026-10-01
decisions: 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/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/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/0111-a-build-source-is-on-the-git-seat-or-external.md
@@ -186,7 +187,8 @@ disagrees with it.
| `grants` | credentials it must create for its consumers | | `grants` | credentials it must create for its consumers |
| `filtering` | rules beyond its own ports | | `filtering` | rules beyond its own ports |
| `computed` | marks a module the controller generates rather than an author writing | | `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** **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 ([ADR 0091](../../02-DECISIONS/0091-a-mount-is-declared-three-ways.md)). A bind mount the module
@@ -233,10 +235,10 @@ counts as a copy and what as a base.
| resource | is | a module may | | resource | is | a module may |
|---|---|---| |---|---|---|
| `directory` | a directory with a mode and an owner | ✅ | | `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:…}` and `${secret:…}` filled in | ✅ | | `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 | ✅ | | `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 | ✅ | | `archive` | files fetched by digest and unpacked | ✅ |
| `package` | a package that must be present | ✅ | | `package` | a package that must be present | ✅ |
| `network` | a named container network | ✅ | | `network` | a named container network | ✅ |
@@ -266,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 **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. 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 ## The builder compiles the languages the mesh is written in
*2026-09-29 — *2026-09-29 —
+18 -1
View File
@@ -2,8 +2,9 @@
layer: to-be layer: to-be
status: implemented status: implemented
code: [mesh-catalog, mesh-controller, mesh-host] code: [mesh-catalog, mesh-controller, mesh-host]
updated: 2026-09-21 updated: 2026-10-01
decisions: 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/0094-a-module-may-hold-several-secrets-from-one-provider.md
- 02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md - 02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md
- 02-DECISIONS/0085-a-secret-is-a-provision.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 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. 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 ## Beyond generate and hold
Owning a secret means owning more than its creation. The mesh being migrated onto has a working 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-tools src/broker-amqp.ts (to be replaced)
- mesh-catalog modules/nats (to be written) - mesh-catalog modules/nats (to be written)
- mesh-sdk src (the protocol's NATS binding, step 3) - mesh-sdk src (the protocol's NATS binding, step 3)
updated: 2026-09-27 updated: 2026-10-01
decisions: 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/0106-the-bus-is-nats.md
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.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 - 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>.event.<verb> a role's own event (JetStream: EVENTS)
mesh.seat.<seat>.tool.<verb> a role's tool (core request/reply) 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.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)): **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.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 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 | | 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 | | 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 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. 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). 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 ## 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 **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 cmd/mesh-controller/source.go
- mesh-controller internal/inventory/migrations/0032-a-source-may-live-on-a-seat.sql - mesh-controller internal/inventory/migrations/0032-a-source-may-live-on-a-seat.sql
- mesh-catalog modules/gitea/module.json - mesh-catalog modules/gitea/module.json
updated: 2026-09-27 updated: 2026-10-01
decisions: decisions:
- 02-DECISIONS/0161-what-deserves-a-seat.md
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-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/0126-a-module-declares-its-own-seats.md
- 02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.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 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. 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
**The set is derived, and only the mesh's half is written here.** Revision, 2026-09-26 **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-controller` | — | mesh | — | the controller |
| `mesh-store` | — | mesh | — | the store the mesh's own records live in | | `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-broker` | — | mesh | `mesh-bus` | the broker carrying the mesh's own bus |
| `mesh-vault` | — | mesh | `secret`, reserved | the vault | | `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` | mesh | `artifact-store` | the artifact registry | | `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-catalog` | `the-catalogue` | mesh | — | the catalogue |
| `mesh-npm-package-registry` | `npm-package-registry` | mesh | `npm-package-registry` | the forge | | `mesh-npm-package-registry` | `npm-package-registry` | mesh | `npm-package-registry` | the forge |
| `mesh-git` | `git` | mesh | `git` | 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 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`. | | 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. | | 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. | | 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. | | 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 layer: to-be
status: proposed status: in-progress
code: [] code: [mesh-controller internal/catalogue]
updated: 2026-09-26 updated: 2026-09-30
decisions: decisions:
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md - 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
- 02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md
- 02-DECISIONS/0115-one-assignment-of-a-module-per-node.md - 02-DECISIONS/0115-one-assignment-of-a-module-per-node.md
- 02-DECISIONS/0113-the-vault-makes-every-secret.md - 02-DECISIONS/0113-the-vault-makes-every-secret.md
- 02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.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; the assignment says nothing;
- **a placement**, where the assignment puts one directory elsewhere: on a second disk, or where an - **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)). 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)): **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 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 assignment, and the route provider answers. A public name already held by another assignment is
refused, like any other singular thing. 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 ## 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 **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 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 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 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 **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 concrete paths exactly as today — the wire format and the host's apply do not change for
@@ -2,8 +2,10 @@
layer: to-be layer: to-be
status: proposed status: proposed
code: [] code: []
updated: 2026-09-27 updated: 2026-10-01
decisions: 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/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 - 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 (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. 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 now, and why not yet
**Why it matters:** self-update is the difference between a mesh a person maintains by typing **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 - mesh-catalog modules/mesh-catalog
updated: 2026-09-28 updated: 2026-09-28
decisions: 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/0126-a-module-declares-its-own-seats.md
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.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 - 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 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. 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 ## 2. Three namespaces, and nothing else
**Its own** — `mesh.mod.<module>.>`. Its events and its tools. Nothing else may publish into it, **Its own** — `mesh.mod.<module>.>`. Its events and its tools. Nothing else may publish into it,
@@ -1,9 +1,12 @@
--- ---
layer: to-be layer: to-be
status: designed status: implemented
code: [] code: [mesh-controller, mesh-tools]
updated: 2026-09-28 updated: 2026-10-01
decisions: 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/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/0129-a-seat-carries-the-protocol-of-its-role.md
- 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.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 node-scoped seat's tool therefore carries the node it is asked of. Nothing about a mesh-scoped seat
changes. 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 ## 5. Discovery
**What a role answers is a read.** The seats and their protocols are records, so the list is a query **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 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. 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 ## 7. Versioning
A seat's tools are an interface and change like one. Additive within a version. A change that would 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 - **Two nodes holding one node-scoped seat derive two addresses.** Checked by the same test as the rest
of the subject table. 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 ## 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 - 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,9 +1,9 @@
--- ---
status: located status: resolved
opened: 2026-08-23 opened: 2026-08-23
located-in: [hal, hq] located-in: [mesh-catalog modules/records, mesh-catalog modules/mesh-console]
fixed-by: fixed-by: ADR 0153; mesh-catalog PR 183 (records), PR 185 (a phrase that wraps)
amended-design: 02-DECISIONS/0025-the-design-record-is-read-not-copied.md 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 # 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 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. 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 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: fixed-by:
amended-design: 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? 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 - 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? 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,7 +1,7 @@
--- ---
status: open status: located
opened: 2026-09-22 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: fixed-by:
amended-design: 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? 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? - 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)? - 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 opened: 2026-09-22
located-in: [mesh-catalog, mesh-controller internal/catalogue] 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: 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 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 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. 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 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: fixed-by:
amended-design: amended-design:
--- ---
@@ -58,3 +58,8 @@ knowing the code.
an operator to undo it without reading the source? 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 - Is there anything a node must never be pushed without, such that sending a partial declaration is
worse than sending none? 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 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: fixed-by:
amended-design: amended-design:
--- ---
@@ -75,3 +75,8 @@ found, and so would be kept for ever on purpose.
module unassigned between the two declarations? module unassigned between the two declarations?
- What reports this? Nothing on the machine currently answers "what is running here that the mesh - 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. 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 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: fixed-by:
amended-design: amended-design:
--- ---
@@ -63,3 +63,8 @@ written.
substitutes settings into content today. substitutes settings into content today.
- Is the kept original enough of an answer, given nothing restores it and nothing points at it - Is the kept original enough of an answer, given nothing restores it and nothing points at it
when the service starts behaving differently? 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 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: fixed-by:
amended-design: amended-design:
--- ---
@@ -60,3 +60,8 @@ expected rate.
nothing answers the first. nothing answers the first.
- Is a digest pin the right thing for a module that takes over an existing service at all, or - 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? 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 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: fixed-by:
amended-design: 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? 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 - 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. 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 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: fixed-by:
amended-design: amended-design:
--- ---
@@ -61,3 +61,8 @@ exercise.
learn to take a group atomically? Nothing takes more than one module at a time today. 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 - 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`? 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 opened: 2026-09-23
located-in: [] located-in: [mesh-controller cmd/mesh-controller/network.go (the placing command), internal/inventory/migrations/0004-the-overlay.sql (one hub)]
fixed-by: 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: amended-design: [03-DESIGN/01-to-be/26-the-seats.md]
--- ---
# 105 — The hub of the private network is a placement, not a seat # 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 - Does the per-node seat still say anything once the hub is a seat, or is it the interface's
presence restated? presence restated?
- What else in the mesh is "exactly one" and recorded as a placement rather than a seat? - 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 opened: 2026-09-23
located-in: [] located-in: [mesh-controller internal/catalogue/seats.go (the seed lacks mesh-vault), mesh-catalog modules/mesh-vault/module.json (claims nothing)]
fixed-by: 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: amended-design: [03-DESIGN/01-to-be/26-the-seats.md]
--- ---
# 106 — The vault claims no seat, so nothing refuses a second one # 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? - 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 - 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? more than one is allowed, so the omission cannot recur?
## Decided, 2026-10-01
[ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md), rule 1: `mesh-vault` joins the mesh's
own set, mesh-scoped, delivering `secret`, and the vault claims it; a second provider is a second
claimant, refused by name. The record also answers the second question: a provision the mesh's own
code dereferences by name gets a seat, every other mesh-scoped provision may have several providers.
Design 26's *reserved* for `secret` named an effect no rule produced; corrected there.
## Resolved, 2026-10-01
`seats` on the live mesh lists `mesh-vault` at mesh scope, delivering `secret`, held by the vault on
the control node. A second provider of `secret` is now a second claimant and refused by name
(`CanHold`'s test). Found on the way: the vault's definition could not be rebuilt at all — it emitted
`secret.provisioned` and the like, which the builder reads as another module's events — so the events
are now the vault's own, `provisioned`, `rotated`, `deprovisioned`.
@@ -1,9 +1,9 @@
--- ---
status: located status: resolved
opened: 2026-09-25 opened: 2026-09-25
located-in: [mesh-catalog modules, mesh-controller internal/catalogue] located-in: [mesh-catalog modules, mesh-controller internal/catalogue]
fixed-by: 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: 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 # 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 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 - What would the contributions file carry instead of host paths, so a provider needs no
identical-path mount? 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 opened: 2026-09-26
located-in: [mesh-controller internal/catalogue/declaration.go, mesh-catalog modules/keycloak, mesh-catalog modules/minio, mesh-catalog modules/nextcloud] located-in: [mesh-controller internal/catalogue/declaration.go, mesh-catalog modules/keycloak, mesh-catalog modules/minio, mesh-catalog modules/nextcloud]
fixed-by: 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: 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 # 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? 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 - 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. 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 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] 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: fixed-by: ADR 0156; mesh-controller migration 0048 (feat/the-artifact-store-seat-is-named-for-its-scope); mesh-catalog modules/distribution
amended-design: 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 # 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? exercise that leaves the code disagreeing?
- What check would keep the glossary honest — a definition tested against the kinds a definition may - What check would keep the glossary honest — a definition tested against the kinds a definition may
actually declare, rather than restated by hand? 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,5 +1,5 @@
--- ---
status: open status: located
opened: 2026-09-26 opened: 2026-09-26
located-in: [mesh-host internal/apply] 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, 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; old-spec forge started, artifacts rebuilt, data renamed forward, push. Nothing lost;
the install-page junk was discarded twice. 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,9 +1,9 @@
--- ---
status: open status: resolved
opened: 2026-09-28 opened: 2026-09-28
located-in: [mesh-catalog, mesh-controller internal/catalogue] located-in: [mesh-catalog, mesh-controller internal/catalogue]
fixed-by: 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: 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 # 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. 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 - 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? 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.
@@ -1,9 +1,9 @@
--- ---
status: open status: resolved
opened: 2026-09-28 opened: 2026-09-28
located-in: [mesh-controller internal/catalogue, mesh-catalog] 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: 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: 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 # 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. 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 - 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. 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,7 +1,9 @@
--- ---
status: located status: resolved
opened: 2026-09-29 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 # 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 - It fails identically for the local machine, which rules out reachability and points at the
transport alone. transport alone.
- The mesh's own traffic over the new bus is unaffected: nodes report, declarations apply. - 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).
@@ -1,7 +1,9 @@
--- ---
status: open status: resolved
opened: 2026-09-29 opened: 2026-09-29
located-in: [mesh-controller cmd/mesh-controller] located-in: [mesh-controller cmd/mesh-controller, mesh-controller internal/catalogue]
fixed-by: mesh-controller PR 164 (module check)
amended-design: 03-DESIGN/01-to-be/12-a-module-repository.md
--- ---
# 148 — a manifest outside this catalogue has no check # 148 — a manifest outside this catalogue has no check
@@ -35,3 +37,14 @@ Nothing prevents this; it was noticed and left. ADR 0037 named it on 2026-09-01
take a path from `MESH_CATALOG`, so the mechanism is already path-driven and not repository-bound. take a path from `MESH_CATALOG`, so the mechanism is already path-driven and not repository-bound.
- `mesh-controller module add` refuses a bad manifest at registration, which is the same check far - `mesh-controller module add` refuses a bad manifest at registration, which is the same check far
too late: by then it is in a running mesh's records. too late: by then it is in a running mesh's records.
## Built, 2026-09-30
`mesh-controller module check <manifest>…` runs what registration runs — the strict parse, the
per-manifest problems, and the cross-manifest rules over every manifest given — with no store and no
mesh, prints every problem in the manifest's words, and exits non-zero on any. What it cannot judge
without a store it says: a claim on one of the mesh's own seats is judged fully only at registration
([ADR 0122](../../02-DECISIONS/0122-a-seat-is-data-a-rename-is-a-database-update.md)), and a seat
declared by a module whose manifest was not passed reads as unknown. Written into
[12 — A module repository](../../03-DESIGN/01-to-be/12-a-module-repository.md) as the section *a
manifest is checked where it is written*. The console's own manifest was the first checked with it.
@@ -1,11 +1,11 @@
--- ---
status: open status: resolved
opened: 2026-09-29 opened: 2026-09-29
located-in: located-in:
- mesh-controller internal/catalogue/dir_into.go (dirsFor: a stated path or <data root>/<module>/<id>, nothing else) - mesh-controller internal/catalogue/dir_into.go (dirsFor: a stated path or <data root>/<module>/<id>, nothing else)
- mesh-controller (accesses: the path is the manifest's literal) - mesh-controller (accesses: the path is the manifest's literal)
fixed-by: fixed-by: mesh-controller PR 176 (`places` and `accesses` on an assignment, `${access:<id>}`, an owner the data already has); mesh-catalog PR 198 (ten definitions name their accesses by id)
amended-design: amended-design: [03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md, 03-DESIGN/01-to-be/18-building-a-module.md]
--- ---
# 153 — An adopted machine's data cannot be placed where it is # 153 — An adopted machine's data cannot be placed where it is
@@ -55,3 +55,25 @@ The two assignment halves 0112 decided: a setting that places a declared directo
path on this node, and a setting that says where an access's data is — both validated like path on this node, and a setting that says where an access's data is — both validated like
`endpoints` (unknown ids refused), and an access placed by the assignment still never created, `endpoints` (unknown ids refused), and an access placed by the assignment still never created,
chowned or removed. chowned or removed.
## Resolved, 2026-10-01
The two assignment halves ADR 0112 decided exist. On an assignment's settings, `places` puts a
declared directory (by id) at a path on this node, with an owner where the data already has one —
`{"config": "/where/it/is", "data": {"path": "…", "owner": "1001:2000"}}` — and `accesses` says where
the operator's data is, by the access's id. Both are validated the way `endpoints` is: an id the
definition does not declare is refused, naming what it does declare; a relative path and a
non-numeric owner are refused; an access nothing places and whose definition carries no path is
refused with the setting to write, rather than mounted as nothing. A placed directory is still the
mesh's — created, owned as said, removed when empty and undeclared. A placed access is still the
operator's — mounted, never created, owned or removed.
An access now has an **id**, and the definition's mounts name it as `${access:<id>}`, so a placement
moves the mount with it. Ten catalogue definitions were given ids; each keeps its path as the default
an assignment may replace, so the machine that said nothing received exactly the paths it had before
(the path-preservation proof, extended to accesses). That default is still a host path in a
definition, tolerated as the transition: the media modules on the control node hold it until their
assignments say where the data is, and then the defaults go.
What the home server's assignments say next is the operator's: per media module, `places` for the
configuration on the second disk and `accesses` for the pool, with the owner the predecessor ran as.
@@ -0,0 +1,29 @@
---
status: open
opened: 2026-09-30
located-in:
- mesh-controller internal/inventory/secrets.go (SecretFor mints a pair credential nobody accepted)
fixed-by:
amended-design:
---
# 164 — A credential that must be accepted is minted anyway
## What was observed
Provisioning ace's modules. Several providers hold exactly one credential they did not get from the
mesh and cannot take one from it: a Servarr app's API key (sonarr, radarr, lidarr), jackett's API key,
plex's X-Plex-Token, nzbget's ControlPassword, qBittorrent's WebUI password. Their consumers' pair
credential must be **accepted** by the operator (ADR 0092). Until it is, `SecretFor` mints a random
value, seals it to both ends, and reports nothing: the value can never work.
Every consumer therefore had to learn to detect it — try the credential against the provider first,
refuse a value the provider rejects, print the `secret accept` command — six write-in steps, one probe
each (ombi, home-assistant, and the four download-stack consumers). qBittorrent bans an address after
five failed logins, so a consumer retrying a minted value locks itself out.
## What would be right
A provision (or a provider's `serves`) can declare its pair credential **accepted-only**. The plan then
refuses the pair — naming the accept command — instead of minting, and a consumer is never handed a
value the mesh knows cannot work.
@@ -0,0 +1,24 @@
---
status: open
opened: 2026-09-30
located-in:
- mesh-controller internal/inventory/secrets.go (AcceptSecretForPair is per consumer)
fixed-by:
amended-design:
---
# 165 — One accepted value must be accepted once per consumer
## What was observed
On ace, jackett's API key is the pair credential for sonarr, radarr, lidarr and bookshelf; sonarr's is
the credential for ombi, bazarr and home-assistant. It is **one value**, owned by the provider — yet
`secret accept` is per pair, so ace's download stack alone needs 12 accepts of 3 values, and rotating
a provider's key means finding and re-accepting every pair. Missing one leaves that consumer on a
stale (or minted, 164) value.
## What would be right
A provider-level accept: "this provider's credential for `<provision>` is X" — delivered to every
consumer pair, current and future, and rotated in one place. Pairs whose credential is genuinely per
consumer (postgres, keycloak, mosquitto, influxdb — minted and created by a provisioner) are unaffected.
@@ -0,0 +1,24 @@
---
status: open
opened: 2026-09-30
located-in:
- mesh-controller internal/catalogue (requires is a list of hard requirements)
fixed-by:
amended-design:
---
# 166 — A requirement cannot be optional
## What was observed
Making every dependency on ace a provision turned soft dependencies into hard ones. grafana now
requires `influxdb-api` (a data source), ombi requires `sonarr-api`, `radarr-api` and `lidarr-api`,
home-assistant requires the Servarr APIs and `mqtt-topic`. Each is optional to the software — grafana
runs without a data source, ombi without lidarr — but a mesh without influxdb cannot assign grafana at
all, and a mesh without lidarr cannot run ombi.
## What would be right
A requirement a module can run without: resolved and bound when a provider exists, absent (with its
`${bound:…}` placeholders refused or defaulted explicitly, never rendered empty) when none does — so
the module description stays true on every mesh.
@@ -0,0 +1,26 @@
---
status: open
opened: 2026-09-30
located-in:
- mesh-catalog (each module builds from its own directory, ADR 0069)
- mesh-sdk
fixed-by:
amended-design:
---
# 167 — Code several modules share has no home
## What was observed
The download-stack write-in step (register download clients and torznab indexers through the Servarr
API) is identical for sonarr, radarr, lidarr and bookshelf. Because a module builds from its own
directory, it now exists as four byte-identical copies under `modules/<m>/downloads/`, kept honest by a
test that fails when one differs. The same shape repeats: an MQTT probe copied into two modules, and a
"write the provider into the app through its API, idempotently, refuse a minted value" step in ombi,
home-assistant, nodered, tautulli and the four downloaders.
## What would be right
A home for shared module code the builder can use — an sdk helper (a write-in step harness: read
bindings and pair credentials, probe the provider, diff, write, report) or a shared package the
catalogue builds once — so a fix lands in one place.
@@ -0,0 +1,32 @@
---
status: open
opened: 2026-09-30
located-in:
- mesh-controller internal/catalogue/settings.go (settle: every key but `ports` merges into every mergeable file and every contribution)
- mesh-controller internal/catalogue/declaration.go (a provider's settings are laid over what it serves)
fixed-by:
amended-design:
---
# 168 — A setting reaches every file and every contribution
## What was observed
Settings merge key by key into **every** `"merge": "json"` file of a module **and** every contribution
it makes; a provider's settings are also laid over what it serves. Seen on ace:
- searxng's `endpoints` and a route `label` land in searxng's own `settings.yml`; nodered's
`timeZone` and `mqtt` keys land in mosquitto's grants file; keycloak's `issuer` lands in its
`postgres-database` and `route` contributions.
- every consumer's `plex-api` binding carries plex's `endpoints` and `expose` settings — and a provider
setting named `port` would silently redirect every consumer.
- a module cannot have two configurable files: searxng's sidecar config had to stop being mergeable
so searxng's keys would not reach it.
Harmless today only because every receiver happens to ignore unknown keys.
## What would be right
A setting is aimed: at a file (by resource id), at a contribution (by requirement), or at what the
module serves — declared settable by the module (ADR 0046 already says settings drive "the fields the
manifest marks") — and an unaimed key is refused like any unknown setting.
@@ -0,0 +1,128 @@
---
status: open
opened: 2026-09-30
located-in:
- mesh-catalog (no module shares a path over the network)
- hq 02-DECISIONS (a file-share seat, per ADR 0126, is a module's own to define)
fixed-by:
amended-design:
---
# 169 — A machine shares its files, and the mesh does not know
## What was observed
ace serves the operator's media library to the home network with two host services no module
declares and HAL never managed either:
```
/etc/exports: /storage/media 192.168.1.0/24(rw,sync,root_squash,…) nfs-server active, :2049
/etc/samba/smb.conf: [media] path = /storage/media/ valid users = media smb active, :139/:445
```
Two LAN clients were connected at survey (2026-09-30). The library itself is operator data
(ADR 0051: ~40 TB on ZFS, the mesh owns nothing about it — [issue 153](../153-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md)
is about modules reaching it in place).
Under the mesh as it stands, this arrangement has no expression and one failure mode:
- **Nothing declares the listens.** At `converge ace` the filter is the sum of what modules listen
on (ADR 0045); 2049 and 445 are nobody's, so the shares close — silently, for the two clients
that mount them.
- **Nothing owns the configuration.** `/etc/exports` and `smb.conf` are hand-written files on one
machine; a second machine sharing a directory would be written by hand again.
- **Nothing can consume it.** A module on another node that wanted the library (a player, an
indexer, a backup) has no `requires` to state and no binding to read; it would mount by a
hand-typed host and path.
- The clients are LAN devices, so this also meets [issue 154](../154-a-machines-own-network-is-not-a-reach/00-report.md)
(no reach for the machine's own network).
## The proposal (the operator's, 2026-09-30, settled after two rounds)
**Two module-defined seats, one per protocol, because NFS and SMB share an intent and not a
contract.** A seat in the mesh's sense is a contract — what it accepts, emits and serves, and the
tools its holder must answer (ADR 0126, 0132) — and lined up, the two share almost none of it:
| | `nfs-share` | `smb-share` |
|---|---|---|
| serves | export path(s); the client ranges allowed (`sec=sys` authorises by address) | share name(s), path |
| pair credential | none | a user and password per consumer |
| consumer's mount | `at:/path` | `//at/share` with credentials |
| holder's tools | export / unexport a path for a range | add / remove a share, create a user |
One `file-share` seat would be the union with every field optional — a consumer could bind it and
still not know how to mount what it got (the emptiness ADR 0129 warns against). "Export a path to
the network" is a category, and the mesh needs no seat category: a consumer requires the one it
can mount. If "give me the library, however" is ever needed, it is a provision an umbrella module
serves, not a seat.
Both are node-scoped, one holder per node (ADR 0110), so ace holds both. `nfs` and `samba` are the
first implementations; a second (Ganesha for `nfs-share`, ksmbd for `smb-share`) is what proves
0126's promise that "replacing the implementation changes nothing for any caller".
The holder module:
- declares the exported paths as `accesses` (ADR 0051: it owns nothing about them — never creates,
chowns or removes), and *which* paths as the assignment's settings (ADR 0046/0112);
- writes the share configuration (`/etc/exports`, `smb.conf`) as mesh-managed files and drives the
units, like `dnsmasq`/`sshd` do for theirs;
- declares its endpoints (`nfs` 2049/tcp; `smb` 445/tcp, …) so the reach — internal, or the LAN
once 154 has an answer — is the assignment's, and converge keeps them open;
- **provides** the seat's provision, so a consumer on another node `requires nfs-share` (or
`smb-share`) and reads `${bound:nfs-share:at}` and the path from its binding instead of a
hand-typed mount.
## The design gap this exposes
**A seat definition has no home outside the module that first declared it.** Today a seat is
declared inside a manifest (`showcase` declares `the-showcase`, `ca-trust` its own). If `nfs`
declared `nfs-share`, Ganesha could hold it only by depending on nfs's manifest — the coupling
0126 removed for callers, reintroduced for implementations. The protocol needs a neutral place in
the catalogue beside the modules (a seat definition registered like a manifest), with a module
saying which seats it implements. This is the first role with an obvious second implementation,
which is what makes it the exemplar for that mechanism.
## The consumer's half: a module mounts it (2026-09-30, third and fourth round)
A binding tells a consumer *where* the share is; it does not put the files on its machine. Mounting
is something done on a machine, and something done on a machine is a module's work — not the host's
(the vocabulary stays closed; no `mount` resource kind).
**A consumer-side module, `network-share` — the module responsible for setting up the network
shares a node uses** (the operator's framing). A node role, like `node-uplink` or
`node-dns-resolver`: each machine has it at most once, which is a reason for it to hold a
node-scoped seat, so two modules can never both be writing mount units on one machine. Assigned on
the node that wants the files:
- `requires nfs-share` (or `smb-share`); several shares on one node are several local names of
the requirement (ADR 0094);
- its manifest is a `package` (nfs-utils), a `file` writing a systemd `.mount` unit filled from
the binding — `What=${bound:nfs-share:at}:${bound:nfs-share:path}` — and a `service` enabling it
after the overlay is up: the same shape as `resolv-conf` or `sshd`, files and a unit;
- *where* it mounts is the assignment's setting (`/srv/media` on one machine, elsewhere on
another); which machine mounts what is an operator decision made at assignment, exactly as which
paths a machine shares is.
**The modules that use the files never learn about NFS.** A player, an indexer, a backup declares
the mounted path as an `access` — an operator-chosen, pre-existing path the mesh never owns
(ADR 0051), exactly as `/storage/media` is on ace. The same app manifest then runs on ace against
the local library and on another node against the mounted one, with only its assignment differing.
**The one check to add, because it is the data-loss case.** An `access` is confirmed today by the
path being present. For a mountpoint that is not enough: a writer whose container starts before the
mount is up writes into the empty directory underneath it, and the files vanish when the mount
lands. The access check must confirm the path is *a mountpoint* when the module says so (or the
module's unit is ordered before the consumer's container — which crosses modules and is exactly
what the mesh does not order). Which of the two is the decision's.
**Identity crosses the wire.** `sec=sys` NFS trusts the client's uid, so a consumer must run as the
library's owner on the server (ace: `media`, 1001:2000) — hq 153's `${access:<id>:uid}`, read from
the mounted tree, answers it on the consumer's side too.
## Open questions for the decision
- Whether an NFS export over the overlay is an `internal` reach of the same endpoint or a second
export line — NFS authorises by client address, so the mesh range and the LAN range are two
entries in one file.
- How a consumer's binding expresses a *path* to mount (today bindings carry `at`, `port`, `as` and
whatever the provider `serves`), and whether one share can serve several paths.
@@ -0,0 +1,63 @@
---
status: open
opened: 2026-09-30
located-in:
- mesh-controller internal/catalogue/resolve.go (holdings are derived from every resolved assignment's manifest `claims`)
- mesh-controller cmd/mesh-controller/seats.go (the deliberate act exists — HoldSeat, "recording … as its standing holder" — beside it)
fixed-by:
amended-design:
---
# 170 — Assigning a module claims every seat it could hold
## What was observed
ace's migration needs a postgres of its own: the operator's decision is that a `postgres` module
assigned on ace provides `postgres-database` to ace's modules and has **nothing to do with the
`mesh-store` seat**, which novox's assignment holds by a deliberate act already taken ("make
novox's postgres the mesh-store").
`assign ace postgres` (2026-09-30):
```
ace is assigned postgres
AND 1 other machine(s) cannot be worked out as things stand, so nothing will be sent to them:
novox
- postgres on novox claims "mesh-store", which postgres on ace already holds — one per mesh
mesh-controller: these assignments cannot be applied:
- postgres on ace claims "mesh-store", which postgres on novox already holds — one per mesh
```
The second assignment did not merely fail: it made **the control plane's own store's
assignment unresolvable** until unassigned. Nothing was pushed; the state is restored.
## Why
`resolve.go` derives what a node holds from the manifest's `claims` of every module resolved on
it, so a claim in a definition is a claim by every assignment of that module. The deliberate
act ADR 0110 describes exists beside it — `seat …` records "X on Y as its standing holder"
(`HoldSeat`) — but resolution does not consult that record; it consults the manifests.
## What was decided
[ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md):
> **A definition says which seats a module *can* hold. An assignment says which it *does*
> hold.** The store module can hold `mesh-store`, and it may be assigned to every node. Exactly
> one of those assignments holds the seat, because that assignment said so.
The manifest's `claims` is being read as *does hold*.
## What would be right
Resolution takes the holder of a seat from the recorded holding (the seat's standing holder),
not from the manifests: a module whose definition can hold a seat is assignable anywhere, and only
the assignment recorded as holder claims it — with the refusal reserved for a second *recorded*
holder at the seat's scope. Assigning postgres to ace is then exactly what the operator said it
is: a database provider on ace, and no more.
## Until then
`postgres` cannot be assigned on any second node; ace's database windows (baserow, letta, n8n,
car-hunter, txt-game) wait on this.
@@ -0,0 +1,72 @@
---
status: resolved
opened: 2026-09-30
located-in: [mesh-controller internal/catalogue/settings.go (settle), mesh-controller internal/catalogue/declaration.go (composed, ownNames)]
fixed-by: mesh-controller PR 175 (a setting overrides a declared key and adds none; `${setting:…}` in a served or contributed value); mesh-catalog PR 196 (mail declares its domain, the identity provider its issuer)
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
---
# 173 — A module's settings reach every fact it contributes, not only the file that asked
## What was observed
Setting the mail module's operator values — `domain`, `sitename`, `website`, `proxy-address` — so that
its environment file could read them as `${setting:…}`
([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)),
and then planning the control node, showed the four keys in places nothing asked for them:
- in every **route** the module contributes to the proxy, beside `label`, `endpoint` and `port`;
- in the **database** it contributes to the store's provider, beside the database's `name`;
- in the `smtp` facts every **consumer** of its mail provision is bound to.
Nothing broke: a provider ignores a key it does not read. But a proxy now receives a mail server's
`proxy-address` and `website` as if they were route facts, a consumer of mail is told the site's name,
and a reader of `plan` cannot tell which of a contribution's keys the module meant and which leaked in.
## Why this is here
Settings are one flat map per module, laid over every mergeable file, every contribution and every
served fact alike (`settle`). That was the right generality when a setting *was* a contribution's
override — a route's label is the example the code gives. It stops being right the day a setting is
an operator's value for one file, which ADR 0155 made ordinary. The design permits a value to travel
where nobody sent it, silently, and every consumer of a provision reads a map that grows with the
provider's unrelated settings.
[ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) and design 27
already say where this ends: a requirement has a contract, and a value goes to the requirement that
asked for it. Until that form exists, this is the cost of the placeholder being the first case of it.
## Open questions
- Should a key a file asks for with `${setting:<key>}` be withheld from contributions and served
facts, or should a contribution's overrides live under their own key (`contributes`, `serves`)?
The second is the shape design 27 draws; the first is the smaller change and keeps the leak from
widening while it is drawn.
- What does a consumer do with a served key it did not expect? Today: nothing, silently. A served
map is not checked against what the provision's contract says it carries, because there is no such
contract yet.
## Resolved, 2026-09-30
**A setting overrides a key a contribution or a served fact declares, and adds none.** A file keeps
taking any key, because a configuration file is where an operator adds things; a contribution and a
served fact are a contract the other side reads, and a setting made for one of the module's files is
no part of it. A key that lands nowhere — no mergeable file, no `${setting:…}` asking for it, no
contribution or served fact declaring it — is named as stray when the node is planned, rather than
dropped.
The first open question is answered the smaller way, and it turned out to be the right one: the two
keys consumers actually read through the leak — the mail provider's `domain`, the identity provider's
`issuer` — are now **declared** by the provider in what it serves, as the operator's value
(`${setting:domain}`, `${setting:issuer}`), filled from the same setting that used to leak and refused
by name when nothing sets it. So the contract says what travels, which is the shape design 27 draws,
without a second key for overrides. The second question stands: a consumer still checks nothing
against a contract, because there is none yet; what it is told is now only what the provider
declared.
*How it was checked:* the plans of all four nodes, under the running controller and the one with the
rule, compared resource by resource — every key that disappears from a contribution is a leaked file
setting or one of the mesh's own words (`expose`, `endpoints`), and nothing a provider reads goes
away; the two served keys were declared before the controller rolled. Unit tests: a setting a route
never declared does not reach the proxy; a served value nothing sets is refused by name; the setting
a served fact asks for is not stray.
@@ -0,0 +1,66 @@
---
status: resolved
opened: 2026-09-30
located-in: [mesh-controller internal/catalogue/dir_into.go, mesh-controller internal/catalogue/declaration.go, mesh-catalog modules]
fixed-by: mesh-controller PR 175 (`place: "mesh"`, a directory beneath a placed one, the proof test resolving both sides); mesh-catalog PR 197 (48 definitions converted)
amended-design: [03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md, 03-DESIGN/01-to-be/18-building-a-module.md]
---
# 174 — The mesh's own files for a module are placed by the definition, not by the mesh
## What was observed
After every module's *own* data directory was placed by the mesh
([issue 119](../119-a-module-definition-decides-where-its-files-live/00-report.md)), the catalogue
still carries **232 host paths in 50 definitions**, all of one kind: where the mesh writes what it
makes *for* the module — its sealed bus credential (`own-secrets.broker`), its merged config file, its
bindings — under `/var/lib/mesh/<module>/…`, and the directory resource that creates that subtree.
Not one of those files is the module's. The mesh mints the credential, composes the binding, merges
the config; the definition only says where to put them, and says it the same way seventy times.
## Why this is here
Design 27's answer to *what sits beneath a node's root* (2026-09-26) is that the mesh's writes need no
module-visible reservation: **what the mesh writes for a module is the mesh's plumbing, placed where
the mesh chooses and mounted in, never part of the module's contract.** The definition today names
that place, so a definition is not yet free of host paths — and a node whose root is elsewhere would
place the module's data there and the mesh's files still under `/var/lib/mesh`.
The path-preserving test that let issue 119 close does not cover this: it proves a *placed* directory
resolves to what was named, and these are not placed.
## What it would take
A word for "the mesh's file for this module", or none: `own-secrets` values, a merged config file and
a binding could be named by key alone, with the mesh choosing `<root>/mesh/<module>/<key>` and
mounting it where the container says. The container side of the mount already exists in every
definition (`/run/secrets/broker`, `/run/config/config.json`); only the host side would go. The
change is in the controller, once, and then a mechanical edit of fifty definitions, which the same
test that proved 119 can prove again with the rule extended.
## Open questions
- Does `own-secrets` keep its map shape with the value becoming the *container* path rather than the
host path, or does the mount stay where it is and the host side become a placeholder the mesh
fills, `${mesh:<key>}`?
- A binding file today lands wherever `binds` says; a module's code reads it from an environment
variable naming the container path. If the host side is the mesh's, is the container side still the
definition's to choose? It should be: it is the software's contract.
## Resolved, 2026-09-30
The word is the one issue 119 introduced, with a second place: a directory saying `place: "mesh"` is
the mesh's directory for the module, `<root>/mesh/<module>`, beside the assignment's own root and
under the same node setting. The mesh's files keep their map shape and the container side of every
mount stays the definition's — only the host side changed, to `${dir:mesh-state}/…`. A directory that
sat beneath the mesh's (a forge's runtime state, a manager's output) states its path as
`${dir:mesh-state}/<rest>` and moves with it; one that sits elsewhere by adoption (the registry's data)
keeps its literal path as the exception it is.
Forty-eight catalogue definitions and the controller's own manifest were converted mechanically. The
proof is the same test that let issue 119 close, now resolving *both* checkouts before comparing,
because the earlier manifest already placed its own directories: resolved on the default root, every
converted definition names exactly the paths it named before. Nothing moved.
Both open questions are answered by keeping what exists: the map stays, the host side is placed; the
container side is the software's contract and stays where the definition says.
@@ -0,0 +1,62 @@
---
status: resolved
opened: 2026-09-30
located-in: [mesh-controller internal/broker/streams.go (the controller's EVENTS consumer), mesh-controller internal/link/receive_nats.go]
fixed-by: mesh-controller PR 173 (MaxAckPending 1 on the controller's events consumer); the packaging module's rebuild record and the status count remain open in the text below
amended-design:
---
# 175 — An announcement queued behind a long build comes back, and the build runs again
## What was observed
On the evening of 2026-09-30 five merges landed within minutes. The controller's log then showed the
same four announcements — two into this repository, one into the catalogue, one into the controller's
own — arriving again every couple of minutes, and each arrival of the controller's rebuilt the two
modules that package its source. The builder built `builder` and `route-proxy` five times over for one
merge, the control node was pushed after each, and every other message the controller handles waited
behind the builds. It looked like a slow mesh; it was a loop.
Earlier the same evening, at a lower rate, the log already carried duplicated lines — a merge seen
twice, a module "moved" twice — that nobody read as a symptom.
## Why
The controller acts on what it consumes in **one loop, one message at a time**, and a merge's handler
builds every module the merge changed before it returns — minutes of work. The bus's acknowledgement
window is thirty seconds. That contradiction was met once already: the message being worked on is kept
alive by a heartbeat while its handler runs
([issue 127](../127-a-module-event-derives-a-subject-nothing-publishes/00-report.md)'s stretch,
controller PR 122). **The heartbeat covers one message.** The consumer is a push consumer with no
bound on what it may have outstanding, so the client is handed everything that is waiting at once; the
messages queued behind the one being built time out unacknowledged, come back after thirty seconds,
and are handled again when the loop gets to them — including the merge whose builds are already done,
which builds them again. A module that only *packages* another repository's source has no record of
which commit it was last rebuilt for, so nothing says "already done".
## Why it matters beyond this instance
The design permits work to be done twice, silently, and the doubling scales with how busy the mesh
is: the busier the builder, the longer the queue, the more that comes back. A push to a machine is
idempotent and a rebuild produces the same digest, so nothing broke — but every merge cost several
builds, the control node was pushed after each, and a person watching saw a mesh that would not
settle. It was the redelivery storm of 2026-09-28 in a narrower form, one layer out.
## What would have prevented it
- **A consumer that is handled one at a time is delivered one at a time.** `MaxAckPending: 1` on the
controller's events consumer: the server holds the rest, nothing times out behind a build, and the
heartbeat that keeps one message alive is then keeping *the* message alive.
- **A packaging module records the commit it was last rebuilt for**, so a replayed announcement is
"already built from it", the answer the source-built modules already give.
- **A log line that appears twice with the same commit is a symptom**, and the check is cheap: the
same announcement acted on twice within its window is a count worth exposing in `status`.
## Resolved on the first remedy, 2026-09-30
`MaxAckPending: 1` on the controller's events consumer (mesh-controller PR 173): the server hands the
controller one announcement at a time and holds the rest, so nothing times out behind a build. The
existing consumer is brought to that configuration by the assertion the controller makes at start.
The second and third remedies — a packaging module recording the commit it was last rebuilt for, and
a doubled announcement counted in `status` — are not built; they would make the same fault visible
and cheaper should the first ever be undone, and they are left here as what to reach for then.
@@ -0,0 +1,74 @@
---
status: resolved
opened: 2026-10-01
located-in: [mesh-controller cmd/mesh-controller/seatverbs.go (argvFor, "build": `--wait 0`, no `--self`), mesh-controller cmd/mesh-controller/main.go (builds.Built records a build and registers nothing)]
fixed-by: mesh-controller PR 179 (one take-in for a build's outcome, called by the waiting command and by the daemon; `--wait 0` asks and returns the id; the seat verb says `--self` for a forge path); ADR 0157 (mesh-controller PR 178) gave the tool something to hear
amended-design: 03-DESIGN/01-to-be/18-building-a-module.md
---
# 176 — The console's `build` tool neither waits nor registers, and does not take a forge path
## What was observed
Eight media modules held by the mesh had not been rebuilt after their manifests changed, because a
merge rebuilds only the modules whose recorded source is the merged repository and theirs was another
one. Asked through the console — the controller's `build` tool, served on its seat
([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)) —
with the repository given as its path on the forge, the way the tool's own description invites:
- every call answered at once with *no build machine answered within 0s … the work is queued*;
- the builder, asked in the same breath, failed each one with *cannot clone novox/mesh-catalog*: the
path was handed to `git clone` as written, because the tool never says the repository is a path on
the forge holding the git seat (`--self`), which the command line requires for that form;
- given the repository's URL instead, the call still answered within zero seconds, the build ran on
the builder, its result was heard and recorded — and the module was **not registered**: what hears a
finished build records the build and stops; only the caller that waited would have parsed the
manifest and registered it, and the caller had gone.
So the console can start a build and never learn its outcome, and a build it starts cannot change
what the mesh holds. The tool's description — *have the build machine build a repository and record
what came out* — is true of the build record and false of the module.
## Why this is here
The seat verb was written as fire-and-forget, deliberately (`--wait 0`), so that a tool call over the
bus does not sit for the minutes a build takes. That reasoning moved the wait but not the work that
followed it: registration lives in the waiting caller, not in the path that hears the result. The two
halves of "build" — asking, and taking in what came back — are split across the command and the
event handler, and the tool reaches only the first.
The forge-path form is a second, smaller gap: the seat verb maps three arguments and forgets the flag
the same command needs to read one of them.
## What would be right
Registration belongs where the result is heard, once, so a build's outcome reaches the mesh whoever
asked and whether or not they waited — the same rule as an announcement's builds. The tool then
answers with what it can say at once (asked, queued, or refused) and `builds` says the rest. A
repository given without a scheme is a path on the git seat, and the verb says so.
## Open questions
- Should a tool call be able to wait at all? A builder answers in minutes; the console's transport
holds a call for a bounded time. If not, the tool needs a way to follow one build — which is the
builder's missing progress (no tools, no events) named in the console's review of 2026-10-01.
## Resolved, 2026-10-01
Registration moved to where the outcome is heard. One function takes a build's outcome in — records
the build, parses the manifest, refuses a definition that names an installation, registers the module
with its source as the seat and path the request carried and the outcome echoes — and both the
command that waited and the daemon that follows the role's `built` event call it. So a build asked
for by anything that could not wait reaches the catalogue the same as one asked for by hand, and the
same outcome heard twice writes one row twice with the same values.
The tool keeps not waiting, and says so: `build --wait 0` publishes the work and answers with the
build's id, and `builds --log <id>` follows the build line by line ([ADR 0157](../../02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md)),
which is what a tool call over the bus can do in the seconds it has. A repository given without a
scheme is said to be a path on the git seat, so the forge-path form the tool's description invites
now works.
The open question is answered by the shape: a tool call does not wait; it asks, gets the id, and
follows. *How it is checked:* the shared take-in against a raised store — registered with the seat
source, a definition naming an installation recorded and refused, a failure said in the builder's
words — and the tool's mapping of a forge path against a URL.
@@ -0,0 +1,51 @@
---
status: resolved
opened: 2026-10-01
located-in: [mesh-controller cmd/mesh-controller/sendable_test.go (the converged-declaration guard), mesh-controller cmd/mesh-controller/adopting_test.go (the adopted-anchor fixture), the build of the controller (runs no check)]
fixed-by: mesh-controller PR 180 (the two tests, for what they missed); the process half is open below
amended-design:
---
# 177 — The controller's check is run by nobody, and two of its tests failed for days unseen
## What was observed
`make check` on the controller's main failed two store-backed tests on 2026-10-01, both for
reasons older than that day:
- the guard that holds a converged declaration byte for byte to what an older host was sent still
expected a `hosts` list on every container, after the change of 2026-09-30 that took it off — a
machine's own resolver knows the mesh's names now ([issue 171](../171-a-modules-own-resolver-knows-no-mesh-name/00-report.md));
- the adopted machine in the converge test reported no outward link, after
[ADR 0140](../../02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md) (2026-09-28)
made a filter depend on one.
Neither commit touched the test it broke, and neither merge failed: the mesh builds the controller
from its repository and runs none of its tests. The tests that need a store — the ones that say what
a machine is actually sent — are exactly the ones a quick `go test ./...` skips, so a person running
the fast check sees green too. Every merge tonight, this one included, was checked that way.
## Why this is here
A guard that is not run is a comment. The byte-for-byte guard exists because an older host parses a
declaration strictly and a field it does not know is a machine that applies nothing
([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)); it went
red on a change that happened to be safe — a field removed — and would have gone red the same way on
one that was not. The mesh has a rule that a test defends a decision
([ADR 0017](../../02-DECISIONS/0017-a-test-defends-a-decision.md)) and no rule that says when the
test is run.
## Resolved, 2026-10-01 — the tests
The guard is re-captured with the change named in its own comment: a field an older host never sees
is the one change the guard permits, a field it would refuse is the one it exists to catch. The
fixture reports an outward link as a real host does. `make check` is fully green on main again
(mesh-controller PR 180). No code changed.
## Open — the process
The build should run the check, or something should, before a merge lands. What that is — the
builder raising the store the tests need, a check the forge runs on a pull request, or the controller
refusing to record a build whose repository's own check fails — is a decision not taken here. Until
it is, `make check` before a controller merge is the operator's habit, written into the work order,
and this issue stays the record of why.
@@ -0,0 +1,59 @@
---
status: resolved
opened: 2026-10-01
located-in: [mesh-controller cmd/mesh-controller/plan.go (routeNamesInTheMesh), mesh-controller internal/catalogue (NamesServed)]
fixed-by: mesh-controller PR 181 (every node's resolution read first, names attributed across them to the terminus, in order; the many shape of contributions counted)
amended-design:
---
# 178 — A routed name resolves to a provider that was merely told it, and flips between plans
## What was observed
From the home server, the dashboard's public name resolved inside the mesh to the control node, where
no proxy serves it, and TLS failed; from outside it resolved to the home server and worked. Filed
first in the forge's tracker on the hq repository (its issue 227), on 2026-09-30. The same night,
two plans of the same machine taken a minute apart differed in exactly one resource — the mesh's
names region — with the dashboard's name on one node's address and then on the other's.
A second thing hid behind it: no module that is routed under several names — the photo service's
six, the invoicing service's two, the mail server's five — had any of them in the names region at
all.
## Why this is here
The names region is composed from every contribution the mesh gave a name to. A consumer that is
routed contributes its label to its route, and the proxy serves the composed name. A consumer that
also uses an identity provider contributes the same label there, because the provider must know the
consumer's public name to compose a redirect — and by the rule that two readers must agree
([issue 122](../122-a-module-cannot-ask-for-its-own-public-name/00-report.md)) it is
given the same composed name. So one name reaches two providers, and the code attributed it to the
node of whichever contribution a map yielded last. Map order is not stable between runs; the region
was not either.
The second fault is older: the single-value reading of a module's contributions is deliberately
empty when the module contributes several times to one requirement
([ADR 0094](../../02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md)'s
sibling), and the names region used that reading, so a module with several routes named none.
## Resolved, 2026-10-01
Every node's resolution is read first, and the names are attributed across them at once. **The
terminus serves the name**: among the providers a name reaches, the one that is not itself published
under a labelled name through another provider. An identity provider is routed through the proxy and
so is a consumer of names, not their end; the proxy contributes no label to anyone and is. The rule
knows nothing of what "route" means — it reads the graph the modules declared — and it walks nodes,
requirements and contributions in order, so one mesh yields one region. Every shape of contribution
is counted, so a module routed under several names has every one of them resolved.
*How it is checked:* the two-node mesh of the report, resolved and attributed twenty-five times — the
dashboard's name on the home server, the identity provider's own name on the control node, no name
leaked to the identity provider; a module with two routes yields two names; and, live, two plans of
the home server after the roll-out identical in the names region, with the dashboard's name at the
home server's address and the several-routed modules' names present.
## Note on where this was filed
The symptom was filed in the forge's issue tracker, which is not where hq's issues live: a tracker
issue carries no status the mesh's records read, and its numbers collide with hq's pull request
numbers in conversation. It is closed there pointing here.
@@ -0,0 +1,53 @@
---
status: resolved
opened: 2026-10-01
located-in: [the identity provider's assignment on the control node (an adopted database whose admin predates the mesh), mesh-catalog modules/keycloak (the minted `admin` own-secret, applied by the server only when it creates its master realm)]
fixed-by: done by hand on 2026-10-01 through the server's own bootstrap command — the admin's password set to the value the mesh minted; no code changed
amended-design:
---
# 179 — An adopted identity provider's admin never took the secret the mesh minted
## What was observed
Filed first in the forge's tracker on this repository (its issue 228, 2026-09-30). The identity
provider's sidecar failed every call with *401 invalid_grant, Invalid user credentials*, and the
server logged a login error for `admin-cli` every five seconds. The provisioner that creates a client
for every consumer of `oidc-client` could never create one, so the dashboard's and the car service's
single sign-on on the home server failed at the identity provider. Not new that day: present since
the module moved to the mesh.
## Why this is here
The manifest mints an `admin` own-secret and renders it into the server's environment and the
sidecar's file. The server reads that variable only when it creates its master realm. This instance
was adopted with its database, whose `admin` user dates from 2022; the real password predates the
mesh, and the minted one was inert from the first start. An own-secret the mesh mints is a statement
the module's software is expected to honour, and adopted software that already holds its own
credential does not — the same shape as the download client's web password on the home server
([ADR 0092](../../02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md)'s
operator-delivered secret), met from the other side.
## Resolved, 2026-10-01
Reality was made to match the mesh rather than the mesh told about reality: the server's own
bootstrap command created a temporary admin, that admin set `admin`'s password to the value the
mesh minted — read from the sidecar's mounted secret on the machine, never printed — and the
temporary admin was removed. Within a minute the sidecar listed realms through the console, and the
two consumers' clients, `mesh_ace_grafana` and `mesh_ace_carhunt`, existed in the realm.
The alternative, `secret accept` with the real password, was not available: the predecessor's
configuration directory is gone and the password with it. For the next adopted module that holds a
credential the mesh mints, the choice is the same, and the mesh's word for the second path is still
only the download client's: a credential the mesh cannot make is the operator's to deliver.
*How it was checked:* `keycloak_list_realms` through the console answers; `keycloak_list_clients`
on the realm lists both mesh-named clients; the server's log stops the five-second login error.
## Open
The manifest still says the admin password is the mesh's to mint, which is true of a fresh install
and false of an adopted one, and nothing in a definition can say which it will be. Whether an
own-secret should be acceptable the way a pair secret is — `secret accept <node> <module> <name>`
already exists and takes an own-secret — is a sentence for design 27's operator provider, not taken
here.
@@ -0,0 +1,76 @@
---
status: resolved
opened: 2026-10-01
located-in: [mesh-controller cmd/mesh-controller/secret.go (accept and now rotate), mesh-controller internal/inventory/secrets.go (a module's own secret), mesh-controller internal/catalogue/manifest.go (how an own secret is taken), the controller's seat (rotate was not a verb)]
fixed-by: mesh-controller PR 183 (`secret rotate`, the `taken` word on an own secret, `rotate` on the controller's seat with two shapes); the staged form for an applied secret stays open below
amended-design: [03-DESIGN/01-to-be/13-credentials-and-their-rotation.md, 02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md]
---
# 180 — A module's own secret cannot be rotated, and nothing rotates from the console
## What was observed
Filed first in the forge's tracker on this repository (its issue 231, 2026-09-30), after a module's
API token was printed by accident on the home server and the only way to change it was to generate
a value by hand and `secret accept` it. The report said there was no rotation at all. That was half
right: `rotate <provision>` has existed for a pair credential since design 13, pushing both ends
together; what did not exist was any rotation of a **module's own secret** — a token, an application
secret, an administrator — and any way to ask for either through the console
([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)).
## Why this is here
[ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md) decided how a
single party's credential rotates: in place, in one of two forms. **Read at start** — the vault
delivers the new value as current and the party is started again. **Applied** — the party's own
code applies the value to a backend that takes it once, so the new value must be staged beside the
current one until the party confirms it. Neither form was built, and nothing said which form a given
secret needed. That last gap is the dangerous one: [issue 179](../179-an-adopted-identity-providers-admin-never-took-the-minted-secret/00-report.md)
is what a value looks like when the mesh believes it was taken and the software never read it. A
rotation that made that happen on purpose would be worse than no rotation.
## Resolved, 2026-10-01 — the read-at-start form, and the verb
**An own secret says how it is taken.** In a definition, `"own-secrets": {"api-token": {"path": …,
"taken": "at-start"}}` says the module reads the file when it starts; `"taken": "applied"` says its
own code applies the value to a backend; a path alone says neither. A definition that says neither
is not rotated by the mesh, and the refusal names the word to write.
**`secret rotate <node> <module> <name>`** makes the secret anew the way the first mint did, seals it
to the machine and to the operator, and sends the machine, so the module starts again on the new
value, under the same `restart-on` that any changed file triggers. Said in the log with who asked and
when, never the value. An applied secret is refused by name, until the staged form exists. A value
given to the mesh rather than made by it is refused as [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)
says, with the way out: change it where it lives, then accept the new value.
**`rotate` is a verb on the controller's seat** ([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)),
with the two shapes the mesh has: a pair credential by provision and consuming machine, or an own
secret by machine, module and name. The console can ask for either.
Nothing in the catalogue says `taken` yet: the word ships one release ahead of its first use, and the
first definitions to say it follow once this controller runs.
*How it is checked:* the manifest form and its refusals; the rotation against a raised store —
rotates a secret taken at start, refuses an applied one, an undeclared one, an accepted one and an
unknown name, each in its own words; the verb's two shapes; and, live, a secret rotated through the
console on a module that reads it at start, the module restarted, and the module working.
*Done live, 2026-10-01 10:13 UTC:* the search module on the home server, the first definition to say
`taken: at-start` whose value the mesh had made. Asked through the console; the controller said it was
rotated and sent the machine; twenty-seven seconds later the secret file carried a new write time, the
server container had restarted on it, and the module answered. The two secrets filed in the forge's
report, the automation module's token and admin password, were refused: both had been accepted by hand
during adoption, and that refusal is the one ADR 0113 asks for — something outside the mesh may hold
an accepted value, so replacing it is a person's act. Modules whose source is pinned to a commit are
not rebuilt by a merge; their new definition was registered by asking for a build of main through the
console, which since [issue 176](../176-the-consoles-build-tool-neither-waits-nor-registers/00-report.md)
registers what it hears.
## Open — the applied form
The staged rotation ADR 0114 decided for an applied secret is not built: the vault delivering the
new value beside the current one, the module's own code switching the backend and confirming, and
only then the new value current. It needs a word in the module's protocol for *confirm*, and it is
the form the identity provider's administrator and every database's superuser need. The request's
second half — re-issuing a pair credential through the provider's own code rather than by re-minting
and pushing — is the same shape from the provider's side, and sits with it.
@@ -0,0 +1,63 @@
---
status: open
opened: 2026-09-30
located-in:
- mesh-controller cmd/mesh-controller/modules.go (assign takes no provider; pin is a separate, per-machine command)
- mesh-controller internal/inventory (provision_pin keyed by (node, name))
fixed-by:
amended-design:
---
# 181 — An assignment does not record which provider answers it
## What was observed
Planning ace's modules that need a database (baserow, letta, n8n, and the apps using ace's
predecessor postgres). The operator's model — and ADR 0110's — is that **an assignment states where
each of its requirements is answered from**: gitea's assignment on novox says its `postgres-database`
comes from novox; an app assigned to ace says whether its database comes from ace or from novox.
The mesh holds no such statement for any assignment. Read on novox (2026-09-30):
```
select … from provision_pin; -- 0 rows
```
Every requirement in the mesh resolves implicitly, each time, by ADR 0084's order (a pin, then the
provider on the consumer's own node, then the only provider).
## What was decided, and what exists
[ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md):
> Where several remain and none is local, **a person chooses when the module is assigned**.
> Assignment lists the candidates, with the holder of a seat that delivers the provision suggested
> first, and records the answer on the assignment as its pin. Without an answer the module is not
> assigned.
What the control plane implements:
| decided | implemented |
|---|---|
| the answer is recorded **on the assignment** | `provision_pin` is keyed `(node, name)` — one answer per machine per provision, shared by every module on it |
| chosen **at assignment** | `assign <node> <module>` takes no provider; `pin <node> <provision> <from-node>` is a separate command |
| an assignment may be answered from its own machine (gitea ← novox) | `pin` refuses a machine pinning to itself ("does not need saying") |
| every assignment has an answer | none recorded; resolution guesses the same answer every time |
## Consequence
Nothing is wrong *today* — with one postgres provider, every guess is the intended answer. But the
answer is not a fact anyone stated, so:
- **it changes silently** the day a second provider appears (e.g. a postgres assigned on ace): every
unpinned consumer re-resolves — a consumer on ace moves from novox's database to an empty one on ace
at the next push, which is data a module stops seeing without anything saying so;
- two modules on one machine cannot take one provision from different providers;
- a person reading an assignment cannot see where its data lives.
## What would be right
ADR 0110 as written: `assign` records, per requirement, the node that answers it (its own node
included), offering the candidates and refusing an assignment without an answer where several exist;
the per-machine `provision_pin` becomes a per-assignment record, with existing assignments backfilled
from what they resolve to now so nothing moves.
@@ -0,0 +1,50 @@
---
status: resolved
opened: 2026-10-01
located-in: [mesh-tools src/broker-nats.ts (one subject, one queue group per module), mesh-tools src/mcp.ts (no way to name a machine), mesh-tools src/runtime.ts (a claimed seat's verbs served by nobody), mesh-controller internal/broker/nats.go (the grant for a tool named one subject)]
fixed-by: mesh-tools PR (feat/a-tool-call-names-the-machine) and mesh-controller PR (same branch) — see ADR 0159; the store seat's verbs and postgres's tools follow in the catalogue
amended-design: [03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md, 03-DESIGN/01-to-be/34-the-console.md]
---
# 182 — A tool call reaches whichever instance answers first, and a claimed seat's verbs are served by nobody
## What was observed
Asked how to list the databases of the store on one machine, the mesh had no answer. A module's tools
are served on one subject per module, `mesh.mod.<module>.tool.<name>`, and every instance of the
module joins one queue group on it, so a call to the database engine's tool while it runs on two
machines reaches whichever answered first, and the answer does not say which. There is no way to ask
the instance on one machine. The console lists the tool once and offers no machine.
And the seat half was missing too. Design 33 §3 says holding a seat means serving its tools, and
ADR 0154 built that for the controller's own seat alone. A module that holds a seat — the database
engine on the control node holding `mesh-store` — served none of the seat's verbs, because no seat
but the controller's declares any and no runtime knew which seats its module claimed.
## Why this is here
Both are the same omission: the tool surface was built as if every module ran on one machine and
held no seat. A queue group is the right default for a stateless module answering anywhere, and the
wrong only choice for a module whose instances are different things — two stores with different
databases. The architecture had the distinction: a module is a thing that runs on machines, a seat is
a role one of them holds. The tool surface did not carry it.
## Resolved, 2026-10-01
[ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md).
Every instance serves its module's subject twice: in the queue group as before, and with its own
machine as the subject's last token. `<module>.<tool>@<node>` reaches one machine's instance; the
console lists `node` on every module tool and puts it in the subject, never in the module's
arguments; every answer carries the machine that gave it, and the console appends it as its own line.
The grant for a tool covers both subjects.
A holder's runtime serves its seat's verbs: the credential the mesh writes names the seats the module
claims and the verbs each 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 and with the machine for a node-scoped one, and
the bus admits the subscription only where the module holds the seat. The store seat's first verbs
and the database engine's tools for them are the catalogue's next step, recorded in the decision.
*How it is checked:* against a real bus, a module on two machines answers each by name and says who
answered when unnamed, and a claimant answers a seat's verb on the seat's subject; the console lists
`node` on a module's tool and not on a seat's; the grant for a tool covers both subjects; and, live,
the store's databases listed from one named machine through the console.
@@ -0,0 +1,43 @@
---
status: resolved
opened: 2026-10-01
located-in: [mesh-controller internal/broker/nats.go (the controller's own publish grant)]
fixed-by: mesh-controller PR 189 (fix/the-controller-may-publish-memberships)
amended-design: []
---
# 183 — The controller could not publish the memberships it issued
## What was observed
The controller release that issues a membership per assignment ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md))
rolled onto the control node. After its first push the controller's log said, once:
```
nats: permissions violation: Permissions Violation for Publish to "mesh.assignment.<node>.<module>"
```
No membership reached the assignments stream. Nothing else changed: every runtime kept serving the
shape it derives for itself, which is what the decision says happens while no membership is issued,
and so the mesh looked healthy while the whole new mechanism was inert.
## Why this is here
The controller composes every account's grant, its own included, and its own grant named the
control, node and JetStream subjects and not the assignments it alone issues. A grant composed by its
holder is checked by nothing but the server at publish time, and a refused publish is one log line
that nothing reads. The fallback that makes the roll-out safe is the same thing that makes this
failure silent.
## Resolved, 2026-10-01
The controller's grant names `mesh.assignment.>`; the broker golden changed by that one line. A
grant composed by its holder arrives late — the broker node is pushed after the controller rolls —
so the roll-out is merge, push the broker node, then any push issues memberships.
The refusal had a second consequence: published with the daemon's own context, the refused
membership was waited on for ever and the controller went deaf —
[issue 185](../185-a-refused-membership-publish-stops-the-controller/00-report.md).
*How it is checked:* the broker golden carries the allow line; live, a runtime's log after the next
push says it was issued a membership rather than that none exists.
@@ -0,0 +1,69 @@
---
status: resolved
opened: 2026-10-01
located-in: [mesh-controller cmd/mesh-controller/upgrades.go (the merge handler builds inside the receive loop)]
fixed-by: mesh-controller PR 197 (ADR 0162: the merge handler writes a plan, asks the first tier and returns; outcomes and a ticker advance it) and PR 199
amended-design: []
---
# 184 — A merge announcement blocks the controller's receive loop
## What was observed
The controller restarted at 12:53:32Z on 2026-10-01 and heard, among its first messages, a merge
announcement for the catalogue that touched some forty modules. Until 13:17:16Z — twenty-four
minutes — it took nothing else in: build outcomes that the build machine had announced and that
the console's `builds` log showed as done sat unrecorded, so `builds` listed none of them and the
modules stayed at their old versions; the node heartbeats were dropped by the bus as a slow consumer
on `mesh.control.*.alive`, twice. When the merge handler returned, everything queued arrived at once
and was taken in within a second.
## Why this is here
The receive loop acts on one message at a time, which is the right discipline for a store the
controller must write in order. Acting on a merge means asking builds and waiting for each outcome,
minutes of work, and that wait happens inside the loop that would otherwise be hearing the outcomes
of everything else. The bus keeps the merge message alive while the work runs — the fix for the
earlier repeated-merge fault — so nothing is lost and nothing is redelivered, and nothing is heard
either. The design permits the controller to go deaf for as long as a merge takes to build, and no
status says so: the mesh reads as quiet, builds read as missing, and heartbeats read as a slow
machine.
## Also seen, 2026-10-01 evening
The handler's work is lost when the controller is replaced while it runs. The runtime image's merge
at 14:22Z was taken by a controller that rolled forty seconds later, after asking the image's own
build and before asking the forty-two that stand on it. The announcement was redelivered to the new
controller, which judged it history — the source had been looked at after the merge — and said
"nothing the mesh holds reads it". The dependents were asked by hand. Whatever shape the fix takes,
the work a merge implies has to be recorded as asked, not held in the handler's stack.
## What a fix needs to decide
Whether a merge is work the loop dispatches and returns from — the builds asked, the outcomes taken
in by the same `Built` handler every other outcome uses, since the stream already delivers them —
or whether the loop runs more than one handler at a time with the store's ordering kept for the
kinds that need it. The first is smaller and keeps one ordering. Either way, a controller that is
busy should say so where `status` is read.
*How this would be checked:* a controller test where a merge announcement that asks a slow build
and a build outcome for another module arrive together, and the outcome is recorded before the
build finishes; live, the controller's log during the next catalogue merge shows registrations
interleaved with the merge's own.
## Decided, 2026-10-01
[ADR 0162](../../02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md): a merge
produces a tiered plan the store keeps; the handler asks the first tier and returns; outcomes advance
the plan; a controller replaced mid-plan resumes it. The loop is never held by a build again.
## Resolved, 2026-10-01 evening
Since the controller holding ADR 0162's plan rolled, a merge announcement is handled in
milliseconds: the plan is written, the first tier asked, the loop free. The builds the merge
implies are asked from the store's record, tier by tier, so a controller replaced mid-plan resumes
it rather than losing it. The first live merge under it (a controller change) is the proof the
decision's table asks for; its tiers are read with `plans`.
*How it is checked:* the plan tests in mesh-controller; live, `plans` after a merge and the loop's
log taking reports in while the plan builds.
@@ -0,0 +1,47 @@
---
status: resolved
opened: 2026-10-01
located-in: [mesh-controller internal/link/bus.go (a membership published with the daemon's own context), mesh-controller cmd/mesh-controller/push.go (a push that returns on the first membership it cannot issue)]
fixed-by: mesh-controller PR 190 (fix/a-refused-membership-does-not-stop-the-controller)
amended-design: []
---
# 185 — A refused membership publish stops the controller
## What was observed
At 13:17:22Z on 2026-10-01 the controller, acting on a build it had just taken in, sent the control
node a declaration and then issued that node's memberships. The server refused the first publish
([issue 183](../183-the-controller-could-not-publish-the-memberships-it-issued/00-report.md)). From
that second on the controller heard nothing: the control node applied the declaration at 13:18 and
its report was never taken; two merges announced by the forge were not built; the heartbeats were
dropped by the bus as a slow consumer; the console's `builds` showed nothing new while the build
machine's own log showed builds done. The controller's seat verbs still answered, so `status` read
as quiet. It stayed so until the controller was replaced.
## Why this is here
A stream publish waits for its acknowledgement for as long as its context lives, and a publish the
server refuses is never acknowledged. The membership was published with the daemon's own context,
which lives as long as the daemon, from inside the one loop that hears everything else. Two
mechanisms built the day before met badly: the receive loop that acts on one message at a time
([issue 184](../184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md)) and a
publish that could wait for ever. The design let a refusal that is said in one log line become a
controller that is deaf with no sign of it.
## Resolved, 2026-10-01
Issuing one membership is bounded to ten seconds, and a push counts the memberships it could not
issue, names the first failure, and stands: the declarations were sent and recorded before it, and
every runtime without a membership serves the shape it derives
([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)).
The live controller was replaced by hand: the fix was built from the CLI inside the running
container with a wait; the stuck daemon held the control node's advisory lock in the store, so the
container was restarted to release it; the control node was pushed from the CLI and took the fixed
controller; a second push from the fixed controller carried the broker's grant, and the broker
reloaded. The push command itself issued no memberships — only the roll-out path did — which is
mesh-controller PR 191.
*How it is checked:* a link test publishes a membership to a server that refuses it and returns
within the bound; live, the controller's log after a push names the memberships it issued or could
not, and keeps taking reports either way.
@@ -0,0 +1,74 @@
---
status: located
opened: 2026-10-01
located-in: [mesh-controller internal/broker/derived.go (the holder worker consumer let many asks stand in flight), mesh-controller internal/link/builds_nats.go (a running build said nothing to the bus), mesh-controller cmd/mesh-controller/upgrades.go (a merge rebuilt its own modules and not what stood on them)]
fixed-by:
amended-design: []
---
# 186 — A release across repositories is an order in a person's head, and a build is a line in a queue nobody keeps
## What was observed
On 2026-10-01 one decision (ADR 0161) was built as three pull requests in three repositories that
must land in order: the controller first, so the seat exists and a report's profile is kept; the host
second, so every machine reports the capability; the catalogue last, so a claim names a seat that
exists and a holder is not refused on every machine. That order is written in a work-order file and
in the pull requests' descriptions. The mesh holds none of it. A merge is handled as a merge: build
the modules whose recorded source is that repository, record what came back. Nothing says what the
mesh should end up as, and nothing checks whether it got there.
The same day showed what a build is. The build machine takes asks from an in-memory queue; when it
was itself rebuilt in the middle of a wave of forty-three asks, the machine rolled, the new one
started with an empty queue, and forty asks were gone without a word — the wave read "one of
forty-three" for two hours. A merge announcement that asks forty builds waits for them inside the
controller's one receive loop ([issue 184](../184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md)).
"Did everything I asked for succeed" was answered by counting lines in two containers' logs.
## Why this is here
Every single step is sound: a build is reproducible, a push composes a machine from the store as it
is, memberships are last-per-subject, so the mesh converges to the right state once every build is
recorded and every machine pushed. What is missing is the whole: the mesh has no durable account of
work it has asked for and no account of the state a release is aiming at, so the two faults that
matter most to an operator — work silently lost, and a dependency between repositories merged in
the wrong order — are detected by nobody. The rule that a manifest word ships one release ahead of
its use is a discipline a person keeps, and a person kept it by hand eleven times this week.
## What a decision would settle
- Whether a build ask is a durable message on the bus (a work queue the build machine takes from and
acknowledges, as the build outcome already is an event), so a restarted builder resumes rather
than forgets, and `builds` can list what is asked and not yet built.
- Whether a release across repositories is a thing the mesh records — a set of commits that belong
together with the order they land in — so that a merge out of order is refused or held rather than
built, and `status` can say what a release still waits for.
- What the smallest honest surface is in the meantime: at least `builds` listing the asked and the
running beside the built, so a person polling logs becomes a person reading one table.
## Located, 2026-10-01 evening
Two of the three faults turned out to be mechanism, and are fixed; the third stands as the decision
this report asks for.
- **The queue was durable; the delivery was not.** A build ask is a message in the seat's work-queue
stream and survives a builder restart. What lost forty-three asks twice was the worker consumer:
with the server's default of many deliveries in flight, every ask behind the one being built was
handed over at once, left unacknowledged for the length of the build, redelivered after the ack
wait, and dropped after the fifth time. The builder's log shows the survivors in redelivery order.
Fixed by mesh-controller PR 194: one in flight, and a running build says it is still working.
- **A merge rebuilt what it changed and not what stood on it.** The relation existed in the store
and only the explicit `build --on` read it. Fixed by mesh-controller PR 193: a merge takes every
module standing on what moved, in base order.
- **A release across repositories is still an order in a person's head.** That is the decision.
*How this would be checked:* a builder restarted between an ask and its build still builds it; a
merge of a dependent repository before its prerequisite is held and named; `builds` lists asked,
running and built.
## Decided, 2026-10-01
The third fault is answered by [ADR 0162](../../02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md):
a release across repositories is a plan whose dependency edges cross repositories, sorted into
tiers and deployed tier by tier, read in `status`. The order a person kept is the order the tiers
give.
@@ -0,0 +1,65 @@
---
status: open
opened: 2026-10-01
located-in: []
fixed-by:
amended-design: []
---
# 187 — The mesh tells nobody when it stops working
## What was observed
One day, 2026-10-01, and five faults, each found by a person reading a container's log hours after
it began, and each invisible to every surface the mesh offers:
- The controller's receive loop was blocked for twenty-four minutes by a merge handler, then for
nineteen minutes by a publish the bus had refused ([184](../184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md),
[185](../185-a-refused-membership-publish-stops-the-controller/00-report.md)). Throughout, `status`
answered and read as quiet, `builds` listed what it had, the console answered every tool. Nothing
said *the controller has taken nothing in since 13:17*.
- The build machine dropped twenty-six of forty-three asks ([186](../186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md)).
Nothing counts asks against builds; the queue read as empty; the loss was inferred two hours later
from a wave that would not finish.
- The controller's own grant refused every membership it published ([183](../183-the-controller-could-not-publish-the-memberships-it-issued/00-report.md)),
and then every module on the new runtime was refused its one read of the stream. Both were one
`Publish Violation` line each in the bus's log, which nothing in the mesh reads.
- The bus dropped the machines' heartbeats as a slow consumer, twice, and said so to the controller's
log only.
In every case the designed fallback held — runtimes served the derived shape, a push later carried
what an earlier one had not — which is why the mesh kept working and why nobody was told.
One more the same evening: the laptop applied a declaration and logged *applied, and could not tell
the mesh: reporting: context canceled*. The report was not retried; the mesh went on believing the
machine's previous state until the next push, and nothing on either side counted the loss.
## Why this is here
The repository's own rule is that a rule states how it is checked, and every record here does. But
the checks are tests and `status`, both asked by a person. The mesh has no account of its own
liveness: whether the controller is hearing, whether the queue is moving, whether the bus is
refusing what the mesh composed, how old each machine's last report is. A fault that leaves the
fallbacks standing is a fault nobody learns about until it compounds, and today three of them
compounded into an evening of reading logs. This is the design permitting a failure to be silent,
which is the first line of what belongs here.
## What a decision would settle
- **What the mesh observes about itself.** At least: the receive loop's last message taken and its
age; asks against builds, with the oldest unbuilt ask's age; publishes the bus refused and
subscriptions it dropped, read from the bus rather than from a log; each machine's last report
and heartbeat age; a module whose runtime says it serves the derived shape.
- **Where it says so.** As events on the bus under the controller's seat, so a log viewer and a
notifier are consumers and not special cases; and in `status`, which must go red for any of them
rather than listing only machines that are behind.
- **Who is told.** A channel a person actually reads — the mesh already has modules that send
mail and messages — chosen once, with a rule for what interrupts a person and what waits for
`status`.
- **What is not a monitor.** Nothing here is a dashboard product the mesh adopts; it is the mesh
stating facts about itself, the way [ADR 0134](../../02-DECISIONS/0134-the-mesh-says-what-it-applied.md)
made it state what it did.
*How this would be checked:* a controller test where the loop is held and `status` goes red
naming the age; a test where an ask is unbuilt past a bound and `builds` says so; live, the next
fault of today's kinds reaches a person before a person reaches the log.
@@ -0,0 +1,63 @@
---
status: resolved
opened: 2026-10-01
located-in: [mesh-controller cmd/mesh-controller/plan.go (theRestOfTheMesh resolves every other machine without its pins and skips one that refuses, saying nothing), mesh-controller cmd/mesh-controller/network.go (onTheNetwork, the same)]
fixed-by: mesh-controller PR 199 (each machine resolved with its own pins; a dropped machine said in both passes), rolled 2026-10-01 evening; PR 200 keeps the per-machine view quiet
amended-design: []
---
# 188 — A refusal inside "who is on the network" drops a machine silently, and every symptom points elsewhere
## What was observed
At 15:28Z on 2026-10-01 the controller rolled to a build that refuses a machine with two modules
answering one provision and no pin naming which (mesh-controller 195). The control node had two
issuers of `acme-ca`. From that moment every plan of the control node failed with *step-ca has a
content that says `${machine:at}`, and this machine says mesh-range or name*; `seats` listed every
seat as unheld; the build machine refused the builder's and the proxy's builds with *no clone base
for that seat — nothing holds it*; and the roll-out of the next controller was refused with the
`${machine:at}` words. Not one of those names the cause. It was found by running the previous image
as a one-shot beside the current one and reading the difference, forty minutes later.
## Why this is here
`onTheNetwork` decides which machines have an address by resolving each one, unchecked, and
*skipping* any whose resolution errs. A machine skipped there has no `at`, so its own plan fails on
the first placeholder that needs one, in another module's words; everything held on it reads as
unheld; everything built from it cannot be built. The design lets one refusal become four unrelated
symptoms and no sentence about the refusal itself. It is the same shape as
[issue 187](../187-the-mesh-tells-nobody-when-it-stops-working/00-report.md): a fault that is swallowed
where it happens and discovered where it hurts.
## What a fix needs
- A machine whose resolution refuses is said, by `onTheNetwork`'s caller or in `status`: *the
control node does not resolve: more than one module provides acme-ca; pin one* — the resolver's
own words, which exist and were dropped.
- A refusal that a release introduces for a machine already converged — a new rule the stored
state does not meet — must not be silent at the roll either; the controller's prepare or first
resolution after a roll should name every machine it now refuses.
*How this would be checked:* a controller test where one machine's unchecked resolution refuses:
`status` names the machine and the refusal, and the other machines keep their addresses.
## Resolved in the live mesh, 2026-10-01
Two faults, one on top of the other. The refusal was mesh-controller 195's new rule — two modules
answering one provision on one machine need a pin — which its author hotfixed for the first pass
(196). The second pass of "the rest of the mesh" resolves every machine *without its pins*, so the
control node, pinned or not, was refused there and vanished: every seat it holds read as unheld,
the builder's and the proxy's builds were refused for want of the git seat's clone base, the
roll-out of the next controller was refused, and the first tiered plan failed at its first tier.
Found by a diagnostic build counting what each machine yielded. Fixed by mesh-controller PR
`fix/a-machine-not-on-the-network-is-said`: each machine is resolved with its own pins, and a machine
left out is named with the resolver's words in both places. The pin itself (`step-ca`, the issuer
the proxy already had) was made by hand and stands.
## Resolved, 2026-10-01 evening
The controller holding the fix was rolled onto the control node by the operator and a colleague
(the running one could not roll itself); after it every seat read as held again, a build asked
through the git seat worked, all four machines resolved and pushed. The line naming a dropped
machine spoke once too often — in the per-machine view, where the others are resolved without the
planned machine's offers and may fail by design — and is quiet there since mesh-controller PR 200.
@@ -0,0 +1,43 @@
---
status: located
opened: 2026-10-01
located-in: [mesh-controller internal/inventory/catalogue.go (RegisterModule records the source commit; a moved event follows a commit that changed, not an artifact that did), mesh-controller cmd/mesh-controller/upgrades.go (the roll-out follows the moved event)]
fixed-by:
amended-design: []
---
# 189 — A rebuild from the same commit is not a move, so a packaging module's new image never rolls out
## What was observed
The build machine's definition packages the controller's source. A controller merge rebuilds it, and
the rebuilt image carries the new controller; its own source commit in the catalogue is unchanged.
Registering that build therefore moves nothing the catalogue announces: no *moved* event, no
roll-out, although the module's upgrade policy says roll out and the artifact is new. On 2026-10-01
at 19:45Z the first tiered plan under [ADR 0162](../../02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md)
built the build machine and waited for the machine running it to apply the new build — a wait
that nothing would end, because nothing had sent it. A push by hand opened the gate.
## Why this is here
A version is what a module *runs*, and that is the artifact. The source commit is how the mesh
knows the artifact's provenance, not what makes it new: a build that reads another repository, or
pulls a base image, produces a different artifact from the same commit. The roll-out followed the
commit, so every module that packages another's source, and every dependent rebuilt because its
base moved, is rebuilt and then left behind on every machine until somebody pushes. The plan now
sends what it waits for (mesh-controller PR `fix/a-plan-sends-what-it-waits-for`), which covers the
gate; the general rule — a new artifact for a module with a roll-out policy is sent, moved commit or
not — is the decision this report asks for, and the catalogue's *moved* event should say what moved:
the artifact.
*How this would be checked:* a controller test registering a build of an unchanged commit with a
new artifact digest for a module whose policy rolls out: the machines are sent; `status` shows no
machine behind afterwards.
## Built in part, 2026-10-01
mesh-controller 203 and 204: a plan sends the machines of every module in a built tier whose policy
rolls out, once, moved commit or not, and waits for the ones a later tier is built by. After a merge
nothing is left for a hand to push, except what a *record* policy leaves by design. What remains for
a decision is the catalogue's own word: *moved* should follow the artifact, so a module rebuilt
outside any plan — by `build` by hand — rolls out the same way.
+9 -6
View File
@@ -87,12 +87,15 @@ rather than location** — that these documents would be indexed into the knowle
symptom search returns them beside everything else. One source, many surfaces. Where the source symptom search returns them beside everything else. One source, many surfaces. Where the source
is authored is then a separate question. is authored is then a separate question.
**That indexing does not exist.** It was checked on 2026-08-23 and returns nothing; it appears **That indexing never existed, and the store it would have indexed into is gone.** It was checked
never to have existed. Until it does, the objection stands unanswered and this repository is on 2026-08-23 and returned nothing; the knowledge base it named was the predecessor's, and since the
the fourth knowledge system it was argued not to be. Recorded as mesh moved to its own bus on 2026-09-28 nothing can reach it at all. The claim is recorded as
[`04-ISSUES/006`](04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md), and [`04-ISSUES/006`](04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md) and answered
left standing here rather than quietly reworded, because a claim that held up a decision and by [ADR 0025](02-DECISIONS/0025-the-design-record-is-read-not-copied.md): these documents are
was never checked is precisely the failure this repository exists to name. **read, not copied** — an agent reads this repository and a search consults it — and that agent is
not built. So today this repository is reachable by whoever knows to open it and surfaces to nobody
else. Said here rather than quietly reworded, because a claim that held up a decision and was never
checked is precisely the failure this repository exists to name.
Answered separately, a repository of its own is the better home: Answered separately, a repository of its own is the better home: