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.
This commit is contained in:
2026-09-30 17:47:49 +02:00
parent 16855ade02
commit 37b46d5349
12 changed files with 437 additions and 70 deletions
@@ -72,6 +72,12 @@ answers from it. Nothing flows back: this repository is public, the mesh is not,
would be how installation-specific detail arrives into documents that must not carry it
([`README.md`](../README.md)).
> **The mechanism changed — 2026-09-30, by ADR 0153.** What stands: read where it is written, no copy,
> one-way. What moved: the reader is a module the mesh assigns (`records`, a checkout at a commit every
> answer names) rather than the agent session of design 15, which is not built; and "the search consults
> the agent" has no store to consult since the cut-over — the console's tool list is where the record
> appears beside everything else. [ADR 0153](0153-the-record-is-read-by-a-module-and-the-console-lists-it.md).
## Consequences
**This repository stops being a fourth knowledge system, properly.** The original objection was
@@ -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
+2
View File
@@ -168,6 +168,7 @@ python3 00-META/checks/index.py fail if stale
- **0132** — [A seat carries the tools its holder must serve](0132-a-seat-carries-the-tools-its-holder-must-serve.md)
- **0134** — [The mesh says what it applied](0134-the-mesh-says-what-it-applied.md)
- **0142** — [The mesh delivers its own components as binaries, not as container images](0142-the-mesh-delivers-its-own-components-as-binaries.md)
- **0154** — [The mesh's own verbs are the mesh-controller seat's tools, and which verbs those are](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)
### Its tiers, from the bottom up
@@ -298,5 +299,6 @@ python3 00-META/checks/index.py fail if stale
- **0034** — [The local account owns the mesh, and a web application's login is not that](0034-the-local-account-owns-the-mesh.md)
- **0080** — [The development cycle is checked, not trusted](0080-the-development-cycle-is-checked.md)
- **0081** — [A decision nothing cites is not yet in the chain](0081-a-decision-nothing-cites-is-not-yet-in-the-chain.md)
- **0153** — [The record is read by a module the mesh assigns, and the console lists it](0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)
<!-- index:end -->