Compare commits

...
11 Commits
Author SHA1 Message Date
jschoubben 860331dc37 Merge pull request 'ADR 0153 and ADR 0154: the record is read by a module, and the mesh's verbs are its seat's tools' (#224) from feat/the-mesh-answers-for-itself into main
Reviewed-on: #224
2026-09-30 15:55:18 +00:00
jschoubben 37b46d5349 ADR 0153 and ADR 0154: the record is read by a module, and the mesh's verbs are its seat's tools
Design 33 in progress against ADR 0154 (the twelve verbs, the prerequisites built); design 35 for
the records module under ADR 0153, extending 0025; as-is 07 rewritten to a mesh that keeps no store;
as-is 12 and 13 updated; issue 006 built and waiting on its live check.
2026-09-30 17:47:49 +02:00
jschoubben 16855ade02 The console shipped: design 34 implemented, as-is 13, issue 147 verified live 2026-09-30 17:13:09 +02:00
jschoubben 8dd566c0aa Merge pull request 'ADR 0152: the operator's surface is a module, the console (group 3)' (#222) from feat/the-console into main
Reviewed-on: #222
2026-09-30 14:47:36 +00:00
jschoubben a98ee0f529 Issues 147 and 148 name what fixed them 2026-09-30 16:21:33 +02:00
jschoubben ad4a5ea004 ADR 0152: the operator's surface is a module, the console
The work order's group-3 question answered: an ordinary module the mesh assigns to the machine a
person sits at, holding a minted credential, calling tools under a manifest grant (invokes), serving
MCP on loopback. Design 34; pointers in 33, 25 and 0095; module check designed into 12 (issue 148);
README stops claiming an indexing nothing provides (issue 006).
2026-09-30 16:08:52 +02:00
jschoubben 0cf1ad5dad Merge pull request 'Issue 172: the ssh client block matches one spelling of a machine's name' (#221) from issue/172-the-ssh-client-block-matches-one-spelling-of-a-machine into main 2026-09-30 13:25:34 +00:00
jschoubben 69a002fce3 Issue 172: the ssh client block matches one spelling of a machine's name
Reported by the operator: ssh by the bare name logs in, by the mesh name
is refused. The predecessor's generator writes the bare name only; the
mesh's ssh-client roster already matches both and is not yet shipped.
2026-09-30 15:25:30 +02:00
jschoubben 16a1a52cd8 Merge pull request 'Issue 171: a module that names its own resolver knows no mesh name' (#220) from issue/171-a-modules-own-resolver-knows-no-mesh-name into main 2026-09-30 13:20:30 +00:00
jschoubben af170e3a67 Issue 171: a module that names its own resolver knows no mesh name
Found and fixed the afternoon ADR 0148 landed: mailu-admin lost its
database behind Mailu's own resolver. Two catalogue PRs; an insight on
0148 that a container's dns is a decision, not a preference.
2026-09-30 15:20:26 +02:00
jschoubben 6c2d5f5913 Merge pull request 'Group 2 is resolved: containers resolve, nothing is copied, a route's name says where it arrives' (#219) from issue/110-resolved into main 2026-09-30 13:07:14 +00:00
24 changed files with 1056 additions and 76 deletions
+12
View File
@@ -75,6 +75,18 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
is a role you occupy, and the two meet where a seat delivers a provision: occupying the seat is is a role you occupy, and the two meet where a seat delivers a provision: occupying the seat is
what makes a module *the* provider of it. what makes a module *the* provider of it.
## The surfaces
- **console** — the module (`mesh-console`) that puts the mesh's tools in front of whoever is on a
machine: an MCP endpoint on the machine's loopback for an agent, the same endpoint for a person. It
is assigned like any module, holds a credential the mesh minted, and calls tools under a grant its
manifest declares (`invokes`). Loopback is the authority boundary: whoever is on the machine owns the
mesh there ([ADR 0152](../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)).
Not "the tool bridge", "the brain" or "the MCP server" — those name the predecessor's program or a
protocol, and the console is a module.
- **invokes** — the manifest word for the tools a module calls, `<module>.<tool>` each or `*` for
every one. A grant on the publish side and nothing else; a module that declares none calls nothing.
## How this page is kept ## How this page is kept
A new name for an existing thing lands here first, in the same change that introduces it in code. A A new name for an existing thing lands here first, in the same change that introduces it in code. A
@@ -72,6 +72,12 @@ answers from it. Nothing flows back: this repository is public, the mesh is not,
would be how installation-specific detail arrives into documents that must not carry it would be how installation-specific detail arrives into documents that must not carry it
([`README.md`](../README.md)). ([`README.md`](../README.md)).
> **The mechanism changed — 2026-09-30, by ADR 0153.** What stands: read where it is written, no copy,
> one-way. What moved: the reader is a module the mesh assigns (`records`, a checkout at a commit every
> answer names) rather than the agent session of design 15, which is not built; and "the search consults
> the agent" has no store to consult since the cut-over — the console's tool list is where the record
> appears beside everything else. [ADR 0153](0153-the-record-is-read-by-a-module-and-the-console-lists-it.md).
## Consequences ## Consequences
**This repository stops being a fourth knowledge system, properly.** The original objection was **This repository stops being a fourth knowledge system, properly.** The original objection was
@@ -47,6 +47,14 @@ Anything with the control plane in reach can ask any module anything it serves.
harder: nothing outside the control plane can, and the control plane's connection is one more harder: nothing outside the control plane can, and the control plane's connection is one more
thing on the path of every question — a cost accepted for the audit it buys. thing on the path of every question — a cost accepted for the audit it buys.
> **The mechanism changed — 2026-09-30, by ADR 0152.** What stands: `ask` on the control plane, and
> that every call passes an account whose permission list says what it may ask. What moved: "nothing
> outside the control plane can" stopped being true when a person's account gained a publish grant per
> tool (design 25 §7, 2026-09-28), and [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)
> takes the first option above for a module as well — a manifest declares `invokes`, and the bus grants
> exactly that publish side. The audit the second option bought is the bus's permission list, which
> derives both.
## How it is checked ## How it is checked
A tools-only bed asks a served tool through the control plane and asserts an answer arrived — A tools-only bed asks a served tool through the control plane and asserts an answer arrived —
@@ -150,6 +150,16 @@ closed by this record, only answered by it.
resolvable inside the mesh — holds unchanged and by the same means the machine already uses. resolvable inside the mesh — holds unchanged and by the same means the machine already uses.
- **Issue 110 stops being a container-DNS inconvenience and becomes a prerequisite** for the mesh not - **Issue 110 stops being a container-DNS inconvenience and becomes a prerequisite** for the mesh not
restarting itself whenever it learns a name. restarting itself whenever it learns a name.
- **A container that names a resolver of its own has opted out of the machine's**, and the copy this
record removes was the only reason such a container could reach anything by a mesh name.
> **Progressive insight — 2026-09-30, the afternoon this landed. Found the hard way.** The mail
> system's admin, behind Mailu's own resolver, lost its database the moment the copy went
> ([issue 171](../04-ISSUES/171-a-modules-own-resolver-knows-no-mesh-name/00-report.md)). A `dns` on
> a container is a decision about whether mesh names exist inside it, not a preference; the module
> was corrected, and whether the controller should refuse the contradiction is that issue's open
> question.
- **A container started by hand gets the mesh's names too**, where before only declared containers did. - **A container started by hand gets the mesh's names too**, where before only declared containers did.
Design 08 drew that boundary deliberately, on the grounds that reaching into every container is what Design 08 drew that boundary deliberately, on the grounds that reaching into every container is what
a nameserver would be for. This record accepts that consequence rather than working around it: a a nameserver would be for. This record accepts that consequence rather than working around it: a
@@ -0,0 +1,160 @@
---
topic: what runs on it
status: accepted
date: 2026-09-30
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
---
# 152. The operator's surface is a module the mesh assigns: the console
## Context
**Since the bus moved, nobody can ask the mesh anything without opening a shell on a machine.** Every
tool call an operator's assistant makes fails, on every machine including the one the operator sits
at, with *AMQP not connected*
([issue 147](../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md)).
The program answering is the predecessor's tool server, started on the workstation by hand, with the
predecessor's broker address written into the assistant's own configuration. It has no manifest, no
assignment, no account on the bus, and the mesh has never known it exists. Nothing regressed: the
mesh removed a transport that a program outside the mesh still dials.
**The mesh has a tool model, and it works.** A module serves each tool on its own subject and its
account may serve nothing else ([ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md));
a person is issued an account whose only permission is to publish the tool subjects named at issue
([25 — The bus on NATS](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §7); a client on the
runtime repository's main branch speaks that account as a command line and as an MCP server. Measured
on the live mesh on 2026-09-28: a call to the forge's `gitea_list_repos` answered with real
repositories; the tool list came back empty, because it asks the catalogue for a tool nothing serves
([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)).
**Two records have already said where the surface belongs.** ADR 0132's consequences: *the MCP
surface belongs inside the mesh — a module the mesh assigns to the machine where the agent sits, with
a credential the mesh minted and authority derived from what it may call, not a program started by
hand with a credential printed to a terminal.* Design [33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md)
§6 says the same. Neither is a decision about the surface: 0132 decided where a seat's tools live,
and named the surface in passing.
**And one record says the opposite, in the letter.** [ADR 0095](0095-the-control-plane-is-the-way-to-ask-a-module.md)
made the control plane *the* way to ask a module, deferred "calling is a grant" until something asked
for it, and recorded that *nothing outside the control plane can*. A person's account (design 25 §7,
built 2026-09-28) is exactly that grant, minted for a person. So 0095's exclusivity has already been
widened once without a record saying so; a module that calls tools widens it a second time, and this
record is where that is said.
**What a module's account may do today, counted from the composition** (`internal/broker`): publish
its own events, publish the accept subjects of seats it uses, subscribe its own tools and what it
consumes. No module principal may publish another module's tool subject. Of 72 modules in the
catalogue, 45 serve tools and 0 may call one.
The question the work order asks before any code: **does the mesh grow its own operator surface, or is
the surface an ordinary module that happens to serve tools?**
## Considered Options
**1. The control plane serves the agent protocol itself** — a listener on the controller, or a verb
its binary runs. Rejected. It puts a surface for tools the control plane does not implement on the one
component that must stay answerable while it is itself being replaced, which is the reason 0132
rejected the control plane as the answer to discovery. A person on a workstation would reach it over
the network, and the networked surface [ADR 0035](0035-one-implementation-several-surfaces.md)
reserves for that authenticates through an OAuth2 provider that is not configured — so the controller's
`api` verb correctly serves nothing today, and this option would either wait for it or bypass it.
**2. A program a person installs and starts by hand with a printed credential** — what exists on the
runtime repository's main. Rejected as the end state. It is outside the mesh in every way issue 147
names: no assignment, no declaration, no seat, no check that it reaches anything, revoked only by a
person remembering to. It is the predecessor's arrangement one bus later, and it fails the same way
the next time an address moves. It stays as the recovery path, the way the command line does
(ADR 0035): a credential from `operator issue` and the `mesh` client work with no console assigned.
**3. An ordinary module, assigned to the machine the person sits at, holding a credential the mesh
minted, serving the mesh's tools on that machine's loopback.** Chosen.
## Decision
**The console is a module.** `mesh-console` is built by the mesh, registered like any module,
assigned to a machine, and given a bus credential sealed to that machine. It serves the mesh's tools to
whoever is on that machine: to an agent over MCP, and to a person through the same endpoint. Assigning
it to a machine is what makes the mesh reachable from there; unassigning it revokes that, at the next
composition, with nothing on the machine to remember to remove.
**A grant to call is a manifest word: `invokes`.** A module declares the tools it calls, each as
`<module>.<tool>`, or the single entry `*` for every tool on the mesh. The bus grants exactly that
publish side and nothing beside it — no event, no subscription, no seat. This is ADR 0095's deferred
first option, taken now that a consumer asks for it; a person's account already has this shape, and
the same composition derives both. The control plane's `ask` stands, and 0095's audit point with it:
every call still passes one account whose permission list says what it may ask.
**Authority is the machine's login.** The console listens on the machine's loopback only, declared
`from: machine`, so whoever can open a socket on the machine is whoever owns the machine, and *the
account that installed the host owns the mesh on that node*
([ADR 0034](0034-the-local-account-owns-the-mesh.md)). Anything on a machine may call anything on it,
and that is the whole of local ([ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md)).
The mesh knows no person: what the audit sees is which console asked, under the account
`<node>.mesh-console`. How a person's identity reaches a session is the question design 15 leaves
open, and this record does not close it.
**What the console lists is asked of the modules.** Design 33 §5: a module's own tools are answered by
the module, from the code that defines them. The tool runtime therefore answers one reserved verb for
every module it serves — `tools`, the module's tool names, descriptions and argument schemas — and the
console assembles its list by asking the catalogue which modules the mesh holds and each module what
it answers. A module that is not running is absent from the list and says so; a tool an agent already
knows the name of can be called whether or not it was listed. A module may not name a tool of its own
`tools`; the runtime refuses the collision at load rather than letting one shadow the other. A
role's tools, and the mesh's own verbs, join the list when the `mesh-controller` seat serves them
(design 33 §1, third family) — the console reads whatever the mesh can say about itself, and grows as
that does.
**The console holds one credential and one grant: `*`.** It is the operator's surface on a machine the
operator owns; narrowing what it may call is a setting on its assignment, which
[ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md) already provides for
and nothing here builds.
## Consequences
- **The way a person drives the mesh is inside the mesh.** It is declared, delivered, replaced and
revoked by the same machinery as everything else, and `status` says whether the machine carrying it
has applied. Issue 147's shape — a surface kept alive by an address in a file — cannot recur, because
there is no file: the console's credential names the bus the mesh is on, and moves when it does.
- **A module may now call tools, which ADR 0095 had reserved to the control plane.** The grant is
explicit, per tool or `*`, and derived by the same composition that grants everything else. A module
that declares no `invokes` gains nothing. What got harder: a manifest reviewer has one more field to
read for authority, and `*` in it deserves the reader's attention every time.
- **A new manifest word ships one release before any manifest uses it**, and must reach both parsers:
the build machine's and the running controller's. The console's manifest cannot be registered until
the controller and the builder that packages it have been rebuilt with the word.
- **Discovery costs a fan-out per list.** One request per module the mesh holds, answered at once by
the bus for every module nothing serves, so the cost is bounded by the modules that are up. The list
is cached briefly in the console; a module assigned a moment ago appears at the next refresh.
- **Every tool runtime must be rebuilt once** to answer `tools`. Until a module is, it is callable and
not listed, and the console says which modules did not answer.
- **The mesh's own verbs are not in the console yet.** `status`, `push`, `assign` are the
`mesh-controller` seat's tools under 0132, and the three prerequisites 0132 names are still not in
place. A person asking what a node runs still opens a shell for that question, and that gap is design
33's to close, not this record's — recorded here so nobody reads the console as the whole of 147.
- **The person's credential is not retired.** `operator issue` and the `mesh` client remain the path
when no console is assigned, and the path an operator uses to bring a mesh up far enough to assign
one.
## How this is checked
| Rule | Checked by |
|---|---|
| A module's `invokes` becomes exactly that publish grant, and nothing else | the bus user composition test: a module invoking `shop.price` may publish that subject and no other tool's; `*` may publish every tool subject; neither may publish an event or subscribe anything it did not consume |
| A malformed `invokes` entry is refused at registration | a parser test: an entry naming no tool is a problem named in the manifest's words |
| The runtime answers `tools` for every module it serves | the runtime's test: a module registering two tools answers three names, and a module naming one of its own `tools` is refused at load |
| The console's list is what the modules answer | the client's test against a real bus: two modules up, a third the catalogue holds and nothing serves, and the list carries the two and names the third as not answering |
| A call from the console reaches a module over the bus | the same test, and the live mesh: the console assigned to a workstation answers `tools/list` on its loopback and a call to the forge returns repositories |
| The console listens on loopback and nowhere else | its manifest declares `from: machine`, and the filter composed for the machine opens nothing for it |
## References
- [issue 147](../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md) — the surface outside the mesh
- [ADR 0095](0095-the-control-plane-is-the-way-to-ask-a-module.md) — extended: a grant to call, for a module as for a person
- [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) — where a seat's tools live, and the sentence that named the surface
- [ADR 0035](0035-one-implementation-several-surfaces.md) — three surfaces over one implementation
- [ADR 0034](0034-the-local-account-owns-the-mesh.md), [ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md) — why loopback is the authority boundary
- [33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §5, §6 — discovery, and what serves it to an agent
- [34 — The console](../03-DESIGN/01-to-be/34-the-console.md) — the design this record authorises
- mesh-tools `src/client.ts`, `src/mcp.ts`, `src/mesh.ts` — the client this makes a module of
@@ -0,0 +1,113 @@
---
topic: how we work
status: accepted
date: 2026-09-30
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
---
# 153. The record is read by a module the mesh assigns, and the console lists it
## Context
[ADR 0025](0025-the-design-record-is-read-not-copied.md) decided that this repository is **read where
it is written, never copied to be found**: an agent reads it directly, and a search of the mesh's
memory consults that agent so its answers appear beside ordinary results. It named the check that
closes [issue 006](../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md): search
for a phrase that appears only in a design document here, and get it back. It gated the build on an
agent that did not exist — the mesh session of
[15 — The agent session](../03-DESIGN/01-to-be/15-the-agent-session.md) — and on a search that
does not exist either, now: the knowledge base 0025 meant was the predecessor's, and since the
cut-over nothing reaches it ([issue 147](../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md)).
**So the two halves of 0025 have no home.** There is no store to be "beside", and no session to be
the reader. What the mesh has instead, since today: a tool model in which every module answers what
it serves, and a console on the machine a person sits at that lists every tool the running modules
answer ([ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)). An agent holding the
console does not search a store; it reads a tool list and calls what fits the question.
**What 0025 could not tolerate was a derived copy** — the enforced copy winning while the reasoned one
quietly stops being true. It rejected a sync for that reason and for no other. A git checkout is not a
derived copy: it is the same bytes at a commit the answer names, and the only way it can differ from
the source is by lagging behind it, which is measurable and stated. 0025's own words allow it —
*retrieval is an agent reading this repository, not a copy living in a second store* — and the
transformation that makes a copy dangerous is exactly what a checkout does not do.
## Considered Options
**1. Wait for the mesh session.** Rejected. Design 15 is `designed` with nothing built, its model
access is a provisions question with no consumer identity yet, and 006 has waited since 2026-08-23.
A record whose check cannot run is a rule enforced by nothing.
**2. The console reads the repository itself.** Rejected. The console holds nothing and decides
nothing (ADR 0152, ADR 0035); a reader inside it would be a second implementation of a thing that
should be one module, unavailable to a person's client and to any other module.
**3. A module that keeps a checkout of the repository and answers questions about it, listed by the
console like any tool.** Chosen.
## Decision
**The reader is a module: `records`.** It requires the `git` provision — the forge — clones the
repository its settings name, keeps the checkout current on every merge the forge announces and on a
timer, and answers over the bus: where a phrase appears as written (document, line, nearest heading),
one document whole, what a folder holds, and where the checkout stands — always with the commit it
read. Nothing is indexed, ranked or summarised: a design record is found by its own words, and a
reader deciding which words matter would be a second opinion about somebody else's document.
**The repository is a setting, not a manifest field.** The module names no mesh
([ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md)): which repository it reads is
the assignment's business, and an installation that keeps its record elsewhere sets that. Until a
repository is set it serves no tools and says why. Public repositories only; it asks for no
credential, because a secret it did not need would be one more thing to seal.
**"Beside everything else" is the console's tool list.** 0025's second half — the search consults the
agent — has no store to consult and needs none: the console lists `records_search` beside the forge's
tools and the mesh's own verbs, with a description that says when to call it, and an agent choosing
tools for a symptom is the search. That is surfacing, not merely reaching: nobody has to know this
repository exists to be offered it.
**Reading stays one-way.** The module reads the forge and answers; nothing flows back into the
repository. It holds no credential that could write.
**The mesh session, when it exists, is a caller of this module, not a replacement for it.** Design 15's
*it holds the design record by reading it* is satisfied by asking `records`; the session brings
judgement, this brings the text.
## Consequences
- **Issue 006 closes on 0025's own check**, run through the console: `records_search` for a phrase
that appears in one design document here returns that document. The module's test does the same
against a repository it makes.
- **The as-is knowledge document is rewritten.** [`07-knowledge.md`](../03-DESIGN/00-as-is/07-knowledge.md)
described the predecessor's two stores; neither is reachable from the mesh, and what the mesh knows
is now what its modules answer. Saying otherwise is the failure this repository exists to name.
- **A checkout lags.** Between a merge and the next sync — seconds when the forge announces it,
minutes when it does not — an answer is the previous commit's, and says which. That is the cost of
no copy, and it is a number rather than a silence.
- **The reader depends on the forge module's event, by name.** `consumes: gitea.pull.merged` names a
module rather than the `git` seat, because the seat declares no events. A forge that is not gitea
leaves the timer as the only refresh, which still works.
- **What got harder:** the record is now reachable from every machine holding a console, which is what
was wanted, and a reader must remember that this repository is public and the mesh is not — the
module reads the public repository and nothing about the installation.
## How this is checked
| Rule | Checked by |
|---|---|
| A phrase in one document comes back from where it is written, with the commit | the module's test against a repository it makes; and live, through the console |
| A merge on the origin is pulled and the next answer names the new commit | the same test |
| A path outside the checkout is refused, not resolved | a test per shape |
| A failed sync leaves the checkout standing and is said | a test against an unreachable origin |
| Without a repository set, no tools are served and the log says why | the module's own start |
| The console lists `records_search` beside every other tool | the console's listing, live |
## References
- [ADR 0025](0025-the-design-record-is-read-not-copied.md) — extended: the reader is a module, the search is the console's list
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — what lists it
- [issue 006](../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md) — what closes
- [35 — Reading the record](../03-DESIGN/01-to-be/35-reading-the-record.md) — the design
- mesh-catalog `modules/records` — the module (PR 183)
@@ -0,0 +1,133 @@
---
topic: the mesh
status: accepted
date: 2026-09-30
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
---
# 154. The mesh's own verbs are the mesh-controller seat's tools, and which verbs those are
## Context
[ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) decided that a seat's protocol
carries its tools in full, that holding a seat means serving them, and that the mesh's own verbs are
the `mesh-controller` seat's. It named three prerequisites, none in place: the protocol in the store
rather than in compiled defaults; a protocol richer than a list of verbs; a node-scoped seat's subject
carrying the node. And it left one thing to a decision per seat: **which verbs each seat serves**,
because a seat's tools bind every future holder.
The console shipped the same day ([ADR 0152](0152-the-operators-surface-is-a-module-the-console.md))
and made the gap visible from the operator's chair: a person on a workstation could call every tool a
*module* serves and none of the mesh's own. What a node runs, what is assigned, whether a push
applied — the questions issue 147 opened with — still meant a shell on the control node. The console's
own handshake said so.
The control plane already answers every one of those questions, as commands: `status --json`,
`node show`, `plan --json`, `assign`, `push`. [ADR 0035](0035-one-implementation-several-surfaces.md)
says a surface is an adapter over those with no decisions in it, and the `api` verb proves the shape:
every route calls the function the command line calls.
## Considered Options
**1. Leave the mesh's verbs to the shell until an identity provider authenticates the HTTP API.**
Rejected. The authenticated network surface is for a browser on another machine; the console is
already behind the machine's login (0152), and the bus already carries every other tool call under an
account whose permission list says what it may ask. Waiting would keep the one surface the mesh has
from answering the mesh's own questions, for a reason that does not apply to it.
**2. Serve the verbs as the mesh-controller *module's* tools, `mesh.mod.mesh-controller.tool.<verb>`.**
Rejected; 0132 rejected it already. The controller holds a seat, and the verbs must keep their address
while the control plane is being replaced, which is the moment they are most needed. A module's name
would change with the implementation; the seat's does not.
**3. Call each command's function inside the serving process.** Rejected on two facts: the commands
print, to the process's standard output, and two calls answered at once would read each other's
words; and each command opens and closes its own stores, which the serving process holds open. Making
every command return a value is the larger refactor, and it would give the tools a second code path to
keep in step with the command line — the thing ADR 0035 forbids.
**4. The holder of the seat runs the command it names, in its own binary, and answers what it
printed.** Chosen.
## Decision
**The `mesh-controller` seat serves twelve verbs**, and these are its interface, additive within a
version ([33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §7):
| verb | answers with | takes |
|---|---|---|
| `tools` | every seat's tools, from the mesh's records | nothing |
| `status` | what is wrong, quiet, behind, waiting — `status --json` | nothing |
| `nodes` | every machine and its mode | nothing |
| `node` | what one machine reported, what it is assigned, why | `node` |
| `modules` | every module, its version, commit and machines | nothing |
| `seats` | every seat, what it delivers, who holds it — `seats --json` | nothing |
| `builds` | what was built lately and what came of it | `module` (optional) |
| `plan` | the declaration a machine would be sent — `plan --json` | `node` |
| `assign`, `unassign` | the mesh's own words, refusal included | `node`, `module` |
| `push` | that it was sent; `status` says what the machine did | `node` (optional: every machine behind) |
| `build` | that the build machine was asked; `builds` says what came of it | `repository`, `path`, `ref` |
**Each verb runs the command it names, in the controller's own binary, and answers what the command
printed** — the output, whether it succeeded, and, where the command speaks JSON, the same as data. A
refusal is the command's refusal in the command's words, because it is the same output. A verb takes
only the arguments its schema names; nothing reaches a flag the schema did not declare. `push` and
`build` are sent and not waited for: a call that blocked for a whole apply would time out on every
machine that takes a minute and say nothing about the others.
**The three prerequisites are built.** A seat's protocol is three columns on its row, seeded from the
compiled defaults where a row had none and additively thereafter, so a verb a release adds joins the
row and nothing an operator wrote is taken away. A served verb is its name, what it does, and the
schema of its arguments and answer; a manifest may still write a bare name. A node-scoped seat's tool
carries the node it is asked of, as the last token of its subject; a mesh-scoped seat's stays flat.
**Holding a mesh seat requires serving its verbs**, judged where the store's set is loaded, and the
refusal names the missing verbs. The controller's own manifest lists the twelve under `tools`.
**Discovery reads the records, through the seat.** `tools` is one of the twelve because the console
cannot read the store and should not: the mesh answers for its own records through the role that owns
them, and the answer is true while any *other* holder restarts. It is not true while the control plane
itself restarts, and the console says so rather than hiding the modules' tools with it.
**A grant of `*` reaches a role's tools; `seat:<seat>.<verb>` grants one.** The console's `*` needed no
change to reach the mesh's verbs, which is what a grant meaning *every tool* should mean.
## Consequences
- **The console answers the mesh's own questions.** Issue 147's first paragraph closes: what a node
runs, what is assigned, whether a push applied, from the machine the person sits at, over the bus,
under an account whose permission list says so.
- **Whoever may call `mesh-controller.push` may change the mesh.** That is the console's `*` on a
machine whose login owns the mesh (0152), and a person's account only if `operator issue` says so.
A grant reviewer reads `*` and `seat:mesh-controller.` with the same care.
- **A verb here binds every future controller.** Twelve is deliberate: what an operator asks weekly,
and nothing that is still finding its shape (`take`, `converge`, `settings`, `secret` stay commands).
- **A command's text is the answer**, and text changes. The three verbs that speak JSON carry it as
data; the rest are read by a person or an agent, not parsed. Anything that needs a shape asks for
`--json` to be added to the command first, which is the right order.
- **What got harder:** the mesh-controller seat's row now carries a protocol an operator could edit, and
a verb removed from the row is a verb the controller stops serving without a build. That is
ADR 0122's arrangement applied to tools, and `seats` shows the row.
## How this is checked
| Rule | Checked by |
|---|---|
| Every declared verb is one the binary can run, with the arguments its schema names | a test walks the table and derives a command line for each |
| A verb missing a required argument is refused in its own words, before anything runs | a test per shape |
| A holder that does not serve a mesh seat's verbs cannot hold it, and the refusal names them | a catalogue test against a seat with two verbs and a holder with one |
| A node-scoped seat's tool carries the node; a mesh seat's does not | the bus composition test: two nodes derive two addresses |
| The controller subscribes its seat's tools and may answer | the composition test, and the golden user list |
| `*` reaches a role's tools; `seat:` grants one and refuses a name with no verb | the composition test |
| The protocol is seeded into the row and widened additively | the store-backed seat test |
| Live: the console lists `mesh-controller.status` and a call answers what `status --json` prints | the rollout of this record |
## References
- [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) — extended: the prerequisites built, the verbs decided
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — the surface that lists them
- [ADR 0035](0035-one-implementation-several-surfaces.md) — a surface is an adapter with no decisions in it
- [ADR 0122](0122-a-seat-is-data-a-rename-is-a-database-update.md) — the row is the mesh's, and now carries the protocol
- [33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) — the design this completes
+3
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) - **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)
### Its tiers, from the bottom up ### Its tiers, from the bottom up
@@ -256,6 +257,7 @@ python3 00-META/checks/index.py fail if stale
- **0146** — [Connectivity is checked by name, per hosting form, with a valid certificate](0146-connectivity-is-checked-by-name-per-hosting-form.md) - **0146** — [Connectivity is checked by name, per hosting form, with a valid certificate](0146-connectivity-is-checked-by-name-per-hosting-form.md)
- **0147** — [A module anchors the mesh's authority on a machine, and takes it away again](0147-a-module-anchors-the-meshs-authority.md) - **0147** — [A module anchors the mesh's authority on a machine, and takes it away again](0147-a-module-anchors-the-meshs-authority.md)
- **0150** — [A module's own code runs as supervised processes under the module's one account](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md) - **0150** — [A module's own code runs as supervised processes under the module's one account](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md)
- **0152** — [The operator's surface is a module the mesh assigns: the console](0152-the-operators-surface-is-a-module-the-console.md)
### How it is built ### How it is built
@@ -297,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) - **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
+63
View File
@@ -0,0 +1,63 @@
---
layer: as-is
status: implemented
code: [mesh-catalog modules/mesh-console, mesh-tools src/mesh.ts, mesh-tools src/http.ts, mesh-tools src/runtime.ts, mesh-controller internal/broker, mesh-controller cmd/mesh-controller/check.go, mesh-controller cmd/mesh-controller/seatverbs.go]
updated: 2026-09-30
decisions:
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
- 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
- 02-DECISIONS/0037-where-a-module-lives.md
- 02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md
---
# The console, as it runs
**The mesh's tools reach a person through a module the mesh assigned to their machine.** Since
2026-09-30 a workstation that is a node can be assigned `mesh-console`; the mesh mints a bus account
`<node>.mesh-console`, seals its credential to the machine, and the container binds
`127.0.0.1:<port>` with the port the mesh assigned for the manifest's declared one. An agent on the
machine is pointed at `http://127.0.0.1:<port>/mcp` and sees the mesh's tools; a person uses the same
endpoint. Nothing on the machine holds a credential a person had to carry.
## What it answers
`initialize`, `tools/list`, `tools/call`, over HTTP, one JSON body per request, no session and no event
stream. `tools/list` is what the running modules answered: every tool runtime built on or after that day
serves a `tools` verb for its module, and the console asks the catalogue for the roster and each module
for its tools. A module that did not answer is named in the list's `_meta.notAnswering`. On the day it
shipped that was 36 of 51 modules — those that serve no tools at all, and those whose rebuilt runtime the
mesh records rather than rolls out — and 62 tools from the rest.
`tools/call` reaches any tool by `<module>.<tool>`, listed or not. The console's grant is `*`, so what it
may call is every tool on the mesh; its account may publish nothing else and subscribes nothing.
## The mesh's own verbs
*Since 2026-09-30 evening ([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)).*
The console asks the `mesh-controller` seat's `tools` verb beside the modules and lists every role's
tools as `<seat>.<verb>` — `mesh-controller.status`, `mesh-controller.push` and the other ten. A call
to `<prefix>.<name>` reaches the seat when the prefix is a seat declaring that verb, and the module
otherwise; `seat:<seat>.<verb>` says so outright. When the control plane does not answer, the list
names `mesh-controller (seat)` as not answering and carries the modules' tools regardless.
## Around it
- **`invokes`** in a manifest is the grant. It is composed into the bus's user list exactly as a
person's account is; the console is the only module that declares it.
- **`module check <file|dir>…`** on the controller's binary judges a manifest with no mesh: the strict
parse, every per-manifest problem, and the rules between the manifests given. It prints what it cannot
judge without a store rather than refusing. The console's own manifest was the first thing checked
with it, and the whole catalogue passes.
- **The person's client remains.** `operator issue` and `mesh tools|call|mcp` with a credential file
still work, for a machine that is not a node and for a mesh not yet able to assign anything.
`mesh tools --console <url>` goes through a running console with no credential; it is covered by the
runtime repository's tests and was not exercised on the live mesh.
## What shipped bent
- A module registered by hand from the catalogue with `--source <url> --path modules/<m>` records a URL,
not a place on the git seat: `--self` takes the forge path form (`<owner>/<repository>`), which the
operator did not pass. The rebuild-on-merge matched the URL anyway.
- Modules whose upgrade policy is *record* — the forge among them — answered `tools` only once
something pushed their rebuilt runtime; until then they are listed as not answering while still
callable. That is the policy doing what it says, not a fault of the console.
+2 -1
View File
@@ -15,12 +15,13 @@ Where the two disagree, the implementation wins and the disagreement is stated.
| [`04-delivery.md`](04-delivery.md) | Push to running: the three silos, levels, and what a green pipeline proves | | [`04-delivery.md`](04-delivery.md) | Push to running: the three silos, levels, and what a green pipeline proves |
| [`05-runtime-and-installation.md`](05-runtime-and-installation.md) | The node runtime, its modes, and how a node comes into being | | [`05-runtime-and-installation.md`](05-runtime-and-installation.md) | The node runtime, its modes, and how a node comes into being |
| [`06-configuration-and-secrets.md`](06-configuration-and-secrets.md) | Managed files, value resolution, and where secrets live | | [`06-configuration-and-secrets.md`](06-configuration-and-secrets.md) | Managed files, value resolution, and where secrets live |
| [`07-knowledge.md`](07-knowledge.md) | The two knowledge stores, and what each is for | | [`07-knowledge.md`](07-knowledge.md) | The mesh keeps no store: what it knows is what modules answer, and the record is read by one |
| [`08-agents-and-work.md`](08-agents-and-work.md) | Agents as employees, tasks, workflows, and the meeting model | | [`08-agents-and-work.md`](08-agents-and-work.md) | Agents as employees, tasks, workflows, and the meeting model |
| [`09-interfaces-and-observability.md`](09-interfaces-and-observability.md) | How the mesh is reached and watched — tools, board, proxy, health, thoughts | | [`09-interfaces-and-observability.md`](09-interfaces-and-observability.md) | How the mesh is reached and watched — tools, board, proxy, health, thoughts |
| [`10-module-catalogue.md`](10-module-catalogue.md) | The catalogue's shape, and what its shape says | | [`10-module-catalogue.md`](10-module-catalogue.md) | The catalogue's shape, and what its shape says |
| [`11-the-lab.md`](11-the-lab.md) | The lab — the first piece of the new shape that exists, and what it does not yet do | | [`11-the-lab.md`](11-the-lab.md) | The lab — the first piece of the new shape that exists, and what it does not yet do |
| [`12-the-seats.md`](12-the-seats.md) | The seats the mesh defines, who holds one, and where a seat changes resolution | | [`12-the-seats.md`](12-the-seats.md) | The seats the mesh defines, who holds one, and where a seat changes resolution |
| [`13-the-console.md`](13-the-console.md) | The mesh's tools on the machine a person sits at, served by a module the mesh assigned there |
## What these documents are not ## What these documents are not
+28 -1
View File
@@ -7,9 +7,10 @@ code:
- mesh-controller internal/catalogue/build.go - mesh-controller internal/catalogue/build.go
- mesh-controller internal/inventory/secrets.go - mesh-controller internal/inventory/secrets.go
- mesh-controller cmd/mesh-builder - mesh-controller cmd/mesh-builder
updated: 2026-09-12 updated: 2026-09-30
decisions: decisions:
- 02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md - 02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md
- 02-DECISIONS/0037-where-a-module-lives.md
- 02-DECISIONS/0009-modules-and-the-graph.md - 02-DECISIONS/0009-modules-and-the-graph.md
- 02-DECISIONS/0010-delivery.md - 02-DECISIONS/0010-delivery.md
- 02-DECISIONS/0005-the-node-host.md - 02-DECISIONS/0005-the-node-host.md
@@ -60,6 +61,32 @@ module from a repository and a path, and the root-only reading left every existi
unbuildable — pointed at the catalogue the builder finds no manifest, pointed at a module's source unbuildable — pointed at the catalogue the builder finds no manifest, pointed at a module's source
it finds no manifest either.* it finds no manifest either.*
## A manifest is checked where it is written
*Added 2026-09-30, from [issue 148](../../04-ISSUES/148-a-manifest-outside-this-catalogue-has-no-check/00-report.md).*
The check the mesh applies at registration — the manifest parses strictly, every name in it is a
usable one, its routes and events and seats are well formed, and no two manifests given together
declare one seat — is a verb on the controller's binary, `module check <manifest>…`, and it needs no
mesh. It reads the files it is given, runs the same functions registration runs, prints every problem
in the manifest's own words, and exits non-zero if there was one. Somebody describing their own
application in their own repository — the case [ADR 0037](../../02-DECISIONS/0037-where-a-module-lives.md)
calls the one that matters most — runs it before pushing, and finds out there rather than when a
running mesh refuses the registration, or later, when a machine applies something that resolved and
should not have.
**What it cannot know, it says.** A seat another module declares elsewhere is unknown to a check that
was not handed that module's manifest, and the output says so rather than refusing: pass the other
manifest too. The mesh's own seats it knows from the binary, which is the one place that set may be
read without a store ([ADR 0122](../../02-DECISIONS/0122-a-seat-is-data-a-rename-is-a-database-update.md)
keeps the store authoritative, so a claim on a mesh seat is judged fully only at registration, and the
check says that too).
*How it is checked:* the controller's test runs the check over the catalogue checkout beside it and
over a manifest with a known fault, and asserts the first passes and the second names the fault; the
test that used to be the only check, `TestEveryCatalogueManifestParses`, now stands beside a command
anybody can run.
## The manifest in the repository is not the manifest the mesh holds ## The manifest in the repository is not the manifest the mesh holds
A resource names an artifact: A resource names an artifact:
@@ -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
+7
View File
@@ -361,6 +361,13 @@ bridged. It is three things:
Nothing is built of this before §10's bed passes; the MCP surface is a thin adapter over (2). Nothing is built of this before §10's bed passes; the MCP surface is a thin adapter over (2).
*Built, and then made a module — 2026-09-30.* (1) and (2) exist: `operator issue` and the `mesh`
client. What (2) describes as a program on the workstation is now the recovery path; the surface an
operator uses is a module the mesh assigns to the machine, holding a credential the mesh minted —
[ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md),
[34 — The console](34-the-console.md). The tool list it asks for is no longer
`catalog_tools`, which nothing served: each runtime answers `tools` for its own module.
## 8. What a module sees, and what the wire does ## 8. What a module sees, and what the wire does
**The contract a module is written against does not change.** `publish` on an envelope becomes a **The contract a module is written against does not change.** `publish` on an envelope becomes a
@@ -1,9 +1,10 @@
--- ---
layer: to-be layer: to-be
status: designed status: in-progress
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
@@ -102,6 +103,12 @@ being something a person carries and becomes something the mesh runs, on a node,
An agent's authority can then be role-shaped: *the forge's tools*, rather than a list of An agent's authority can then be role-shaped: *the forge's tools*, rather than a list of
module-specific names that changes the day the forge is replaced. module-specific names that changes the day the forge is replaced.
*Decided and designed on 2026-09-30:* the module is the console —
[ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md),
[34 — The console](34-the-console.md). It builds the second half of §5 now (a module's tools are
asked of the module, through a `tools` verb every runtime answers) and lists a role's tools when the
records carry them.
## 7. Versioning ## 7. Versioning
A seat's tools are an interface and change like one. Additive within a version. A change that would A seat's tools are an interface and change like one. Additive within a version. A change that would
@@ -120,6 +127,18 @@ 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 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
+133
View File
@@ -0,0 +1,133 @@
---
layer: to-be
status: implemented
code: [mesh-catalog, mesh-tools, mesh-controller]
updated: 2026-09-30
decisions:
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
- 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
- 02-DECISIONS/0035-one-implementation-several-surfaces.md
- 02-DECISIONS/0034-the-local-account-owns-the-mesh.md
---
# 34 — The console
**The mesh's tools, on the machine a person sits at, served by a module the mesh assigned there.**
An agent reaches them over MCP on the machine's loopback; a person reaches the same endpoint. Nothing is
installed by hand, nothing is configured with an address, and the mesh knows the surface exists because
it put it there ([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)).
## 1. What it is
A module, `mesh-console`, in the catalogue. Its image is the tool runtime's own — the client that
already speaks the bus as a command line and as an MCP server — started in a mode that reads the
module's credential and listens on loopback. It has no state, no provision, no seat. What it needs is
the bus, which it gets the way every module does: a credential the mesh minted for `<node>.mesh-console`,
sealed to the machine, delivered as the module's own secret.
Its manifest says three things nothing else in the catalogue says together:
- `invokes: ["*"]` — it calls every tool on the mesh, and the bus grants exactly that publish side;
- `listens` on a port `from: machine` — the filter opens nothing for it, because loopback is not outside;
- no `emits`, no `consumes`, no `tools` — it answers nothing on the bus and nobody can address it there.
## 2. What it serves, and to whom
**One endpoint, `POST /mcp` on the machine's loopback**, speaking MCP over HTTP: `initialize`,
`tools/list`, `tools/call`. An agent on the machine is pointed at it once — the address is the machine's
own and never changes — and sees every tool the mesh can say it has. A person at a terminal uses the
same endpoint through the `mesh` client, or through anything that can make an HTTP request; the client
needs no credential, because the console holds it.
**The endpoint is the machine's login.** It binds `127.0.0.1` and nothing else. Whoever can connect is
on the machine, and whoever is on the machine is the account that owns the mesh there
([ADR 0034](../../02-DECISIONS/0034-the-local-account-owns-the-mesh.md),
[ADR 0144](../../02-DECISIONS/0144-anything-on-a-machine-may-call-anything-on-it.md)). There is no
token, no login page and no second identity, on purpose: a credential a person had to carry to reach
their own machine's console would be the arrangement this replaces, moved one hop.
## 3. How it knows what the mesh can do
Design [33](33-the-tools-the-mesh-answers.md) §5 splits discovery in two: a role's tools are read from
the mesh's records, a module's own are asked of the module. The console builds the second half now and
reads the first when it exists.
**Every tool runtime answers `tools`.** The runtime that serves a module's tools also serves one verb of
its own under that module's name, `mesh.mod.<module>.tool.tools`, answering the module's tool names,
descriptions and argument schemas — the definitions from the code that answers them, and from nowhere
else. A module may not name a tool of its own `tools`; the runtime refuses the collision at load.
**The console asks the catalogue which modules the mesh holds, then asks each.** `catalog_modules`
answers the roster; one `tools` request per module, in parallel, answers the list. The bus refuses at
once a request nothing serves, so a module that is not running costs nothing and is named in the answer
as not answering, rather than silently absent — *silence and success must never look alike*. The list is
kept for a short while and refreshed, so an agent asking on every turn does not fan out on every turn.
**A tool that was not listed can still be called.** Listing is discovery; calling is the grant. An agent
that knows a tool's name asks for it by `<module>.<tool>` and the module answers or the bus says why not.
**What is missing from the list, and until when.** A role's tools and the mesh's own verbs — `status`,
`push`, `assign` — are the `mesh-controller` seat's under ADR 0132 and are not served yet; their three
prerequisites are listed in that record. When the seat serves them, the console lists them beside the
modules' own, and the person stops opening a shell for the mesh's own questions. Until then the console
says so in its handshake.
## 4. Where it runs
On whichever machines an operator sits at, by assignment. It is not on the control node by default and
does not need to be: it reaches the bus like any module, from anywhere in the mesh. A machine that is
not a node cannot have it, which is the right refusal — the mesh reaches what it declares, and a
workstation that wants the console joins first.
The person's credential and the `mesh` client (design [25](25-the-bus-on-nats.md) §7) remain the path
for a machine that is not a node, and the path to a mesh not yet far enough along to assign anything.
## 5. Removing it
Unassigning the console from a machine revokes its bus account at the next composition and stops the
container; nothing is left on the machine that could still connect. An agent pointed at the loopback
address gets a refused connection, which is the truthful answer.
## How it is checked
| Check | Defends |
|---|---|
| a module invoking one tool may publish that subject and no other tool's; `*` may publish every one; neither may publish an event or subscribe what it did not consume | ADR 0152, the grant |
| a module registering two tools answers three names to `tools`, with schemas; a module naming its own `tools` is refused at load | ADR 0152, discovery |
| against a real bus: two modules up, a third held and not running — the console lists the two and names the third as not answering | ADR 0152, silence is not success |
| a call through the console's endpoint reaches a module over the bus and the answer is the module's own, unshaped | ADR 0035, a surface decides nothing |
| on the live mesh: the console assigned to a workstation answers `tools/list` on loopback and a call to the forge returns repositories | the exit of work-order step 3 |
| the composed filter for a machine carrying the console opens no port for it | ADR 0144 |
## What shipped, 2026-09-30
Everything above, the same day: mesh-controller PR 164 (`invokes`, `module check`), mesh-tools PR 20
(`mesh serve`, the `tools` verb), mesh-catalog PR 181 (`mesh-console`). Verified on the live mesh: the
console assigned to a workstation answered `tools/list` on its loopback with 62 tools from the modules
whose runtimes had been rebuilt to answer `tools`, named 36 modules as not answering (modules that serve
no tools, and modules whose new runtime the mesh records rather than rolls out), and a `tools/call` of
the forge's `gitea_list_repos` returned repositories. An agent on that machine reaches it as an HTTP
MCP server and reports it connected. The mesh assigned the declared port unchanged, which is what a
machine with nothing else on it does; the console binds whatever it is given.
Two things shipped bent, both stated in [`00-as-is/13-the-console.md`](../00-as-is/13-the-console.md):
the person's client through the console (`--console`) exists and was exercised in the test suite, not
on the live mesh; and a module registered from the catalogue by hand recorded its source as a URL rather
than as a path on the git seat, because `--self` takes the forge path form — the rebuild-on-merge still
matched it by URL.
## What this does not settle
- Narrowing a console's grant per assignment. ADR 0046 makes it a setting; nothing reads one yet.
- The mesh's own verbs on the bus. Design 33's third family; this document only says where they appear
once they exist.
- A person's identity behind the console. The mesh sees the console's account; design 15 keeps the
question open.
## References
- [ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md) — the decision
- [33 — The tools the mesh answers](33-the-tools-the-mesh-answers.md) — what the console lists
- [25 — The bus on NATS](25-the-bus-on-nats.md) §7 — the person's client this makes a module of
- [issue 147](../../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md) — the symptom
@@ -0,0 +1,85 @@
---
layer: to-be
status: in-progress
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 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,7 +1,7 @@
--- ---
status: located status: located
opened: 2026-08-23 opened: 2026-08-23
located-in: [hal, hq] located-in: [mesh-catalog modules/records, mesh-catalog modules/mesh-console]
fixed-by: fixed-by:
amended-design: 02-DECISIONS/0025-the-design-record-is-read-not-copied.md amended-design: 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
--- ---
@@ -169,3 +169,27 @@ them into. The record stays open, and its answer is no longer "index this reposi
is whatever the mesh grows as its own knowledge surface, if it grows one. Until then the honest fix is whatever the mesh grows as its own knowledge surface, if it grows one. Until then the honest fix
is the README, which should stop claiming a property nothing provides. is the README, which should stop claiming a property nothing provides.
## Where this stands, 2026-09-30
The README no longer claims a property nothing provides: it says the indexing never existed, that the
store it named is unreachable since the cut-over, and that ADR 0025's answer — read, not copied, by an
agent that consults this repository — is decided and not built. That was the honest fix the previous
note asked for, and it is done.
The record stays open on ADR 0025's build, and on nothing else. The mesh's own operator surface is now
a module ([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)); the
reader ADR 0025 describes is the mesh session of design 15, which would answer through that surface
like any tool. What closes this is still the check 0025 names: search for a phrase that appears only in
a design document here, and get it back.
## Built, 2026-09-30
[ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md): the
reader is a module, `records` (mesh-catalog PR 183), keeping a checkout of this repository from the
forge and answering `records_search`, `records_read`, `records_list`, `records_status` and
`records_sync` at the commit it read; the console lists them beside every other tool, which is where
"beside everything else" lives in a mesh with no store. Design
[35 — Reading the record](../../03-DESIGN/01-to-be/35-reading-the-record.md). The module's test runs
0025's check against a repository it makes; this record closes when the same check passes through the
console on the live mesh, and says so below.
@@ -1,7 +1,9 @@
--- ---
status: located status: resolved
opened: 2026-09-29 opened: 2026-09-29
located-in: [nothing in the mesh — the surface in use is the predecessor's, installed on the workstation] located-in: [mesh-catalog modules/mesh-console, mesh-tools src/mesh.ts, mesh-controller internal/broker]
fixed-by: ADR 0152; mesh-controller PR 164 (invokes); mesh-tools PR 20 (mesh serve, the tools verb); mesh-catalog PR 181 (mesh-console)
amended-design: 03-DESIGN/01-to-be/34-the-console.md
--- ---
# 147 — the operator's tools still dial the bus that was removed # 147 — the operator's tools still dial the bus that was removed
@@ -73,3 +75,28 @@ outside the mesh.
- It fails identically for the local machine, which rules out reachability and points at the - It fails identically for the local machine, which rules out reachability and points at the
transport alone. transport alone.
- The mesh's own traffic over the new bus is unaffected: nodes report, declarations apply. - The mesh's own traffic over the new bus is unaffected: nodes report, declarations apply.
## Answered, 2026-09-30
The work order's question — does the mesh grow its own operator surface, or is the surface an
ordinary module — is answered by [ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md):
**the console is a module.** `mesh-console` is assigned to the machine a person sits at, holds a
credential the mesh minted, calls tools under a grant its manifest declares (`invokes`), and serves the
mesh's tools on that machine's loopback to an agent over MCP and to a person through the same endpoint.
Designed in [34 — The console](../../03-DESIGN/01-to-be/34-the-console.md).
What that leaves, said plainly so nobody reads this record as closed on the whole of its first
paragraph: the console reaches every tool a *module* serves. The mesh's own questions — what a node
runs, what is assigned — are the `mesh-controller` seat's tools under ADR 0132 and are not on the bus
yet; for those a shell is still the way, and design 33 is where that closes.
The predecessor's program on the workstation is not replaced by the mesh; it is left where it is and
the assistant is pointed at the console beside it. The `hal` entry in the assistant's configuration
still names things that are not the mesh's.
**Verified live, 2026-09-30 evening.** The four pull requests merged; the console was registered
(checked first with `module check`), built, assigned to a workstation, issued a bus account, and pushed.
On that machine `tools/list` answered on loopback with 62 tools and named 36 modules as not answering,
and a call to the forge's `gitea_list_repos` returned repositories. The assistant on that machine now
lists the console as a connected MCP server beside the predecessor's program, which was left where it
is. As-is: [`13-the-console.md`](../../03-DESIGN/00-as-is/13-the-console.md).
@@ -1,7 +1,9 @@
--- ---
status: open status: resolved
opened: 2026-09-29 opened: 2026-09-29
located-in: [mesh-controller cmd/mesh-controller] located-in: [mesh-controller cmd/mesh-controller, mesh-controller internal/catalogue]
fixed-by: mesh-controller PR 164 (module check)
amended-design: 03-DESIGN/01-to-be/12-a-module-repository.md
--- ---
# 148 — a manifest outside this catalogue has no check # 148 — a manifest outside this catalogue has no check
@@ -35,3 +37,14 @@ Nothing prevents this; it was noticed and left. ADR 0037 named it on 2026-09-01
take a path from `MESH_CATALOG`, so the mechanism is already path-driven and not repository-bound. take a path from `MESH_CATALOG`, so the mechanism is already path-driven and not repository-bound.
- `mesh-controller module add` refuses a bad manifest at registration, which is the same check far - `mesh-controller module add` refuses a bad manifest at registration, which is the same check far
too late: by then it is in a running mesh's records. too late: by then it is in a running mesh's records.
## Built, 2026-09-30
`mesh-controller module check <manifest>…` runs what registration runs — the strict parse, the
per-manifest problems, and the cross-manifest rules over every manifest given — with no store and no
mesh, prints every problem in the manifest's words, and exits non-zero on any. What it cannot judge
without a store it says: a claim on one of the mesh's own seats is judged fully only at registration
([ADR 0122](../../02-DECISIONS/0122-a-seat-is-data-a-rename-is-a-database-update.md)), and a seat
declared by a module whose manifest was not passed reads as unknown. Written into
[12 — A module repository](../../03-DESIGN/01-to-be/12-a-module-repository.md) as the section *a
manifest is checked where it is written*. The console's own manifest was the first checked with it.
@@ -0,0 +1,78 @@
---
status: resolved
opened: 2026-09-30
located-in:
- mesh-catalog modules/mailu (eight containers name Mailu's own resolver, and one of them binds a mesh name)
- mesh-catalog modules/dnsmasq (dropped the DNSSEC bit its upstreams set)
fixed-by: mesh-catalog PR 178 (mailu-admin uses the machine's resolver) and PR 179 (the machine's resolver passes the DNSSEC bit down) — 2026-09-30, the same afternoon
amended-design:
---
# 171 — A module that names its own resolver knows no mesh name
## What was observed
The afternoon [ADR 0148](../../02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md)
landed — no container is given the mesh's names any more; it asks the machine's resolver — the mail
system's admin container began logging, 523 times in three minutes:
```
psycopg2.OperationalError: could not translate host name "novox.internal" to address: Name does not resolve
```
Mail was accepted on every port and the web front answered; the admin and the spam filter beside it
were unhealthy, and anything that needed the database — a mailbox change through the API, the spam
filter's domain list — failed. Found by the operator asking whether mail was back, forty minutes in.
Mailu ships its own resolver, an unbound in a container, and every other Mailu container is told to
use it — the module carries `dns: [192.168.203.254]` on eight containers. That resolver recurses from the
root and knows nothing under `.internal`. Until that afternoon the admin container had the database's
name anyway, because the mesh wrote every name into every container at creation; the copy was the only
reason a container behind its own resolver could reach anything by a mesh name, and nobody knew it was
load-bearing.
**Removing the override was not enough.** Given the machine's resolver instead, the admin refused to
start: `Your DNS resolver at 127.0.0.11 isn't doing DNSSEC validation`. Mailu checks, at start, that
its resolver returns the Authenticated Data bit for a signed name. The mesh's resolver forwards to two
upstreams that validate and set the bit, and dropped it on the way down — dnsmasq does unless told
otherwise. Mailu's own unbound has no hook to forward a zone elsewhere, so it could not be taught the
mesh's names either.
## Why it matters beyond this instance
**A container with a resolver of its own has opted out of the machine's, and nothing says so.** 0148
made the machine's resolver load-bearing for every container; a `dns` on a container is a quiet
exception to that, and the exception used to be papered over by the copy the record removed. The
manifest field reads like a preference and is a decision about whether mesh names exist inside the
container.
**A resolver that forwards to validating upstreams and hides the fact is less useful than it could
be**, and the first program to check found out.
**The mesh reported nothing.** Every container ran; the failing one accepted connections; the report
was about bytes. It is [issue 145](../145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md)
again, and the check that would have caught it is the same unbuilt one.
## What was done
- The one Mailu container that binds a mesh name — the admin, through the database it is granted —
no longer names Mailu's resolver and uses the machine's, like every container without a `dns` of
its own (mesh-catalog PR 178). The other seven keep unbound: the spam filter needs a validating
resolver for its blocklist lookups, and none of them asks for a mesh name.
- The machine's resolver passes the DNSSEC bit down from its upstreams, `proxy-dnssec` (PR 179). It
does not validate itself; the trust is the upstream's and the path to it, as a forwarding resolver's
always was, and the configuration says so.
## What checks it
The admin container's own start-up check, which is what failed, and the mesh's status once it reads
healthy. A container-level check that a mesh name resolves from inside every declared container is
the one 110 and 145 both ask for and is not built.
## Open questions
- Should a container's `dns` be refused, or made to say what it gives up? A module that names its
own resolver and binds a mesh name is a contradiction the controller can see at composition — the
grant hands it a name its resolver will not answer.
- Should the machine's resolver validate rather than proxy? It would cost a trust anchor on every
machine and make the resolver slower to start; proxying was enough for the one program that asked.
@@ -0,0 +1,55 @@
---
status: located
opened: 2026-09-30
located-in:
- the predecessor's terminal module (still generating the operator's ssh client blocks on every workstation)
- mesh-controller internal/catalogue (the ssh-client roster, tested and not yet a catalogue module)
fixed-by:
amended-design:
---
# 172 — The ssh client block for a machine matches one spelling of its name, and the other gets the wrong user
## What was observed
On a workstation, 2026-09-30, reported by the operator. `ssh home-server` logs in; `ssh
home-server.internal` is refused with `Permission denied (publickey)`. The operator expected the
opposite, if either: the full mesh name is the one the resolver serves.
The name is not the fault. Both spellings resolve to the machine's private address — the mesh's roster
region in the hosts file carries `<node>.internal <node>` on one line, and the resolver answers
anything under the node's name. What differs is the login: the generated client configuration has a
`Host home-server` block naming the account to log in as, and `home-server.internal` matches no block,
so ssh falls back to the operator's local username, which has no account on that machine. Spelled
`account@home-server.internal` it works.
The file is the predecessor's. `~/.ssh/config.d/mesh` says in its own header that it is generated by
the predecessor's terminal module, which only ever wrote the bare name. The mesh's own ssh-client
roster — every other machine's Host block, written as a marked region of the operator's `~/.ssh/config`
with the account the mesh knows for that machine (to-be 29) — already matches both spellings, and a
controller test holds `Host marge marge.internal`. It is composed and tested in the controller and is
not a module in the catalogue, so no machine receives it; every workstation still runs the
predecessor's generator.
## Why it matters beyond this instance
**A name the mesh serves and a name a person can use are not the same set**, and the difference is
silent. The resolver, the hosts file and the certificate authority all treat `<node>.internal` as the
machine's name; the one file that decides who you log in as does not know it. A person who learns the
mesh's name from `status` or from a certificate and types it is refused with an error that says
nothing about a missing Host block.
**It is the migration story for the operator's own tooling, arriving as a symptom.** The mesh has the
right file and does not ship it. Until the ssh-client roster is a module and is assigned to the
workstations, the predecessor's generator keeps writing a file the mesh has already superseded, and
every such file is one the mesh cannot correct.
## Open questions
- Should the ssh-client roster become a catalogue module now, assigned to every workstation, and take
the predecessor's `config.d/mesh` out of the operator's `Include`? Its content is settled; what is
not is the takeover of a file in a person's home that another generator still writes.
- Should the block match a third spelling — the machine's public name, where it has one — or is that
a different key and a different account?
- What checks it? A controller test holds the two spellings; nothing checks that the file a workstation
actually has is the mesh's rather than the predecessor's.
+9 -6
View File
@@ -87,12 +87,15 @@ rather than location** — that these documents would be indexed into the knowle
symptom search returns them beside everything else. One source, many surfaces. Where the source symptom search returns them beside everything else. One source, many surfaces. Where the source
is authored is then a separate question. is authored is then a separate question.
**That indexing does not exist.** It was checked on 2026-08-23 and returns nothing; it appears **That indexing never existed, and the store it would have indexed into is gone.** It was checked
never to have existed. Until it does, the objection stands unanswered and this repository is on 2026-08-23 and returned nothing; the knowledge base it named was the predecessor's, and since the
the fourth knowledge system it was argued not to be. Recorded as mesh moved to its own bus on 2026-09-28 nothing can reach it at all. The claim is recorded as
[`04-ISSUES/006`](04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md), and [`04-ISSUES/006`](04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md) and answered
left standing here rather than quietly reworded, because a claim that held up a decision and by [ADR 0025](02-DECISIONS/0025-the-design-record-is-read-not-copied.md): these documents are
was never checked is precisely the failure this repository exists to name. **read, not copied** — an agent reads this repository and a search consults it — and that agent is
not built. So today this repository is reachable by whoever knows to open it and surfaces to nobody
else. Said here rather than quietly reworded, because a claim that held up a decision and was never
checked is precisely the failure this repository exists to name.
Answered separately, a repository of its own is the better home: Answered separately, a repository of its own is the better home: