Compare commits

...
Author SHA1 Message Date
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
28 changed files with 1122 additions and 100 deletions
+5 -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
@@ -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
@@ -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,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`
+4
View File
@@ -168,6 +168,8 @@ 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)
### Its tiers, from the bottom up ### Its tiers, from the bottom up
@@ -257,6 +259,7 @@ python3 00-META/checks/index.py fail if stale
- **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) - **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
@@ -298,5 +301,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
+10 -5
View File
@@ -1,12 +1,13 @@
--- ---
layer: as-is layer: as-is
status: implemented 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] 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 updated: 2026-09-30
decisions: decisions:
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md - 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/0095-the-control-plane-is-the-way-to-ask-a-module.md
- 02-DECISIONS/0037-where-a-module-lives.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 console, as it runs
@@ -30,11 +31,15 @@ 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 `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. may call is every tool on the mesh; its account may publish nothing else and subscribes nothing.
## What it does not answer ## The mesh's own verbs
The mesh's own verbs. `status`, `push`, `assign` and the rest are not served on the bus — they are the *Since 2026-09-30 evening ([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)).*
`mesh-controller` seat's tools under ADR 0132, whose three prerequisites are not built — so a person The console asks the `mesh-controller` seat's `tools` verb beside the modules and lists every role's
still opens a shell on the control node for them. The console's handshake says so. 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 ## Around it
+1 -1
View File
@@ -15,7 +15,7 @@ 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 |
@@ -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
+5 -4
View File
@@ -186,7 +186,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 +234,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 | ✅ |
+1 -1
View File
@@ -109,7 +109,7 @@ convention, which later seats departed from.
| `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`, reserved | the vault |
| `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 |
@@ -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
@@ -1,9 +1,10 @@
--- ---
layer: to-be layer: to-be
status: designed status: implemented
code: [] code: [mesh-controller, mesh-tools]
updated: 2026-09-28 updated: 2026-09-30
decisions: decisions:
- 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
@@ -126,6 +127,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
@@ -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: [hq README.md, and the agent ADR 0025 names — not built] 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
@@ -182,3 +182,27 @@ a module ([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-t
reader ADR 0025 describes is the mesh session of design 15, which would answer through that surface 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 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. 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,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,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,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,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,54 @@
---
status: located
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:
amended-design:
---
# 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.