Compare commits

...
Author SHA1 Message Date
jschoubben 8dd566c0aa Merge pull request 'ADR 0152: the operator's surface is a module, the console (group 3)' (#222) from feat/the-console into main
Reviewed-on: #222
2026-09-30 14:47:36 +00:00
jschoubben a98ee0f529 Issues 147 and 148 name what fixed them 2026-09-30 16:21:33 +02:00
jschoubben ad4a5ea004 ADR 0152: the operator's surface is a module, the console
The work order's group-3 question answered: an ordinary module the mesh assigns to the machine a
person sits at, holding a minted credential, calling tools under a manifest grant (invokes), serving
MCP on loopback. Design 34; pointers in 33, 25 and 0095; module check designed into 12 (issue 148);
README stops claiming an indexing nothing provides (issue 006).
2026-09-30 16:08:52 +02:00
jschoubben 0cf1ad5dad Merge pull request 'Issue 172: the ssh client block matches one spelling of a machine's name' (#221) from issue/172-the-ssh-client-block-matches-one-spelling-of-a-machine into main 2026-09-30 13:25:34 +00:00
jschoubben 69a002fce3 Issue 172: the ssh client block matches one spelling of a machine's name
Reported by the operator: ssh by the bare name logs in, by the mesh name
is refused. The predecessor's generator writes the bare name only; the
mesh's ssh-client roster already matches both and is not yet shipped.
2026-09-30 15:25:30 +02:00
jschoubben 16a1a52cd8 Merge pull request 'Issue 171: a module that names its own resolver knows no mesh name' (#220) from issue/171-a-modules-own-resolver-knows-no-mesh-name into main 2026-09-30 13:20:30 +00:00
jschoubben af170e3a67 Issue 171: a module that names its own resolver knows no mesh name
Found and fixed the afternoon ADR 0148 landed: mailu-admin lost its
database behind Mailu's own resolver. Two catalogue PRs; an insight on
0148 that a container's dns is a decision, not a preference.
2026-09-30 15:20:26 +02:00
jschoubben 6c2d5f5913 Merge pull request 'Group 2 is resolved: containers resolve, nothing is copied, a route's name says where it arrives' (#219) from issue/110-resolved into main 2026-09-30 13:07:14 +00:00
jschoubben 04c9500b5b Group 2 is resolved: containers resolve, nothing is copied, a route's name says where it arrives
Issue 110's cause was not the filter: the runtime had never been told,
and the resolver dropped a query arriving on a bridge. ADR 0148 step 3
landed once it did (109, 151 resolved). ADR 0151 composes a route's
internal name under the serving node and drops the suffixed alias
(139, 157 resolved). Design 08 amended; a fact in 0148 corrected.
2026-09-30 14:56:43 +02:00
23 changed files with 788 additions and 31 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
what makes a module *the* provider of it.
## The surfaces
- **console** — the module (`mesh-console`) that puts the mesh's tools in front of whoever is on a
machine: an MCP endpoint on the machine's loopback for an agent, the same endpoint for a person. It
is assigned like any module, holds a credential the mesh minted, and calls tools under a grant its
manifest declares (`invokes`). Loopback is the authority boundary: whoever is on the machine owns the
mesh there ([ADR 0152](../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)).
Not "the tool bridge", "the brain" or "the MCP server" — those name the predecessor's program or a
protocol, and the console is a module.
- **invokes** — the manifest word for the tools a module calls, `<module>.<tool>` each or `*` for
every one. A grant on the publish side and nothing else; a module that declares none calls nothing.
## How this page is kept
A new name for an existing thing lands here first, in the same change that introduces it in code. A
@@ -47,6 +47,14 @@ Anything with the control plane in reach can ask any module anything it serves.
harder: nothing outside the control plane can, and the control plane's connection is one more
thing on the path of every question — a cost accepted for the audit it buys.
> **The mechanism changed — 2026-09-30, by ADR 0152.** What stands: `ask` on the control plane, and
> that every call passes an account whose permission list says what it may ask. What moved: "nothing
> outside the control plane can" stopped being true when a person's account gained a publish grant per
> tool (design 25 §7, 2026-09-28), and [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)
> takes the first option above for a module as well — a manifest declares `invokes`, and the bus grants
> exactly that publish side. The audit the second option bought is the bus's permission list, which
> derives both.
## How it is checked
A tools-only bed asks a served tool through the control plane and asserts an answer arrived —
@@ -103,6 +103,13 @@ reintroduces 109 and 135 — silently, and on a live mesh, which is exactly how
([issue 110](../04-ISSUES/110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/00-report.md));
and on two of four machines the resolver binds loopback only, so the runtime hands containers a
public resolver instead. Both are prerequisites, not related work.
> **Progressive insight — 2026-09-30, later the same day. The loopback claim was wrong.** The
> resolver bound the private address on all four machines; on two the runtime had never been told
> to use it, and on all four the resolver discarded a query that arrived on the runtime's bridge.
> The step stands; the facts under it were those. Both fixed the same day
> ([issue 110's resolution](../04-ISSUES/110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/01-resolution.md)),
> and step 3 landed after them.
2. **The runtime is told which resolver to use, per machine, as a file** — not per container as a
creation-time argument, or the resolver's address is back in every container's identity and the
problem has only got smaller.
@@ -143,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.
- **Issue 110 stops being a container-DNS inconvenience and becomes a prerequisite** for the mesh not
restarting itself whenever it learns a name.
- **A container that names a resolver of its own has opted out of the machine's**, and the copy this
record removes was the only reason such a container could reach anything by a mesh name.
> **Progressive insight — 2026-09-30, the afternoon this landed. Found the hard way.** The mail
> system's admin, behind Mailu's own resolver, lost its database the moment the copy went
> ([issue 171](../04-ISSUES/171-a-modules-own-resolver-knows-no-mesh-name/00-report.md)). A `dns` on
> a container is a decision about whether mesh names exist inside it, not a preference; the module
> was corrected, and whether the controller should refuse the contradiction is that issue's open
> question.
- **A container started by hand gets the mesh's names too**, where before only declared containers did.
Design 08 drew that boundary deliberately, on the grounds that reaching into every container is what
a nameserver would be for. This record accepts that consequence rather than working around it: a
@@ -0,0 +1,99 @@
---
topic: the tiers
status: accepted
date: 2026-09-30
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0066-public-routing-is-name-agnostic.md
---
# 151. A route's internal name is composed under the node that serves it
## Context
A module that requires a route is given two names from one label: a public one, `<label>.<public
domain>`, and an internal one, `<label>.<node>.internal`
([ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md)). Both were composed
from the node the module runs on.
The two are answered differently. The public name is published into every machine's roster at the
address of the node whose proxy serves it ([ADR 0066](0066-public-routing-is-name-agnostic.md)), so
it reaches the proxy from anywhere in the mesh. The internal name is answered by every machine's
resolver as *anything under a node's name goes to that node*
([design 08 §2](../03-DESIGN/01-to-be/08-connectivity.md)) — the node it was composed from, which is
the consumer's. Where the proxy runs on another machine, that name sends a client to a machine with
nothing listening, while the public name works
([issue 139](../04-ISSUES/139-an-internal-route-name-resolves-to-the-consumers-node/00-report.md)).
Every route on this mesh today is served beside its module, so it has not been seen; `route` is
provided mesh-wide precisely so that stops being true.
Beside it, the roster gave every routed name a second entry with the mesh's suffix appended —
`<name>.<public domain>.internal` — because it composed a full name for every entry as it does for a
machine. That name resolved on every machine, was served by nothing, and was refused by the proxy at
the handshake; the first three names tried while reproducing an unrelated issue were those, and the
evidence pointed at a regression that had not happened
([issue 157](../04-ISSUES/157-a-routed-names-internal-alias-is-served-by-nothing/00-report.md)).
## Considered Options
**1. Keep the consumer's name and publish it at the serving node's address**, as the public name is.
The name stays `<label>.<consumer>.internal` and an exact roster entry overrides the wildcard.
Rejected: it makes `<x>.<node>.internal` mean *goes to that node* except when it does not, which is
the one rule the resolver design states; it needs an entry per route where the wildcard needed none;
and which of an exact entry and a wildcard a resolver answers first is the resolver's business, which
the mesh deliberately does not know.
**2. A proxy on every machine, so the serving node is always the consumer's.** Rejected for this
question: it is a different decision about what `route` is — a node-scoped seat with a mesh-wide
fallback — and this mesh runs one proxy on the hub today. Whatever is decided there, a route served
from another machine must have a name that reaches it.
**3. Compose the internal name under the node that serves the route.** Chosen.
## Decision
**A route's internal name is `<label>.<serving node>.internal` — composed under the node whose proxy
answers the route, which is the machine the request arrives at.** The public name is unchanged:
`<label>.<public domain>` of the node the module runs on, which is where the operator put it.
Where the proxy runs beside the module — every route on this mesh today — the two nodes are one and
nothing changes. Where it does not, the name says where the request goes, which is what a name under
a node's name has always meant.
**A routed name has no mesh form.** The roster publishes it as itself, once, at the serving node's
address. Only a machine has a bare name beside its full one.
What certifies the internal name is unchanged by this: the proxy that terminates it obtains a
certificate from the mesh's authority for the names it is given, and it is given this one.
Taken on the operator's standing instruction to answer the open design questions in the work order.
## How this is checked
- **Composition.** A controller test contributes a route from a module on one node to a proxy offered
from another, gathered the way the controller gathers a consumer's contribution for a provider on
another machine, and asserts the internal name carries the serving node.
- **Publication.** A controller test renders a roster with a machine and a routed name and asserts
the routed name appears as itself, once, and never with the suffix appended.
- **On the mesh.** After the change no machine's roster carries a `<domain>.internal` entry, and a
route's internal name still answers from a container with a certificate from the mesh's authority.
## Consequences
- **A route served from another machine now has a usable internal name.** The first module assigned
that way will resolve, where before it would have resolved to the wrong machine with no error.
- **The internal name of a route can change when its proxy moves.** A route re-homed from one proxy
to another gets a new internal name, as the design's rule implies; clients that dialled the old one
reach the old machine. The public name does not move with the proxy and is the stable one.
- **The roster is one line shorter per routed name**, and a person reading a hosts file no longer
finds names that resolve to a refusal.
- **Issue 139's second question — a per-node route holder — is left open**, and is a decision about
what a seat is rather than about a name.
## References
- [issue 139](../04-ISSUES/139-an-internal-route-name-resolves-to-the-consumers-node/00-report.md) — the question
- [issue 157](../04-ISSUES/157-a-routed-names-internal-alias-is-served-by-nothing/00-report.md) — the alias
- [ADR 0066](0066-public-routing-is-name-agnostic.md) — routed names propagate mesh-wide; extended here
- [ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md) — how the two names are composed and how far each reaches
- [design 08 §2](../03-DESIGN/01-to-be/08-connectivity.md) — anything under a node's name goes to that node
@@ -0,0 +1,160 @@
---
topic: what runs on it
status: accepted
date: 2026-09-30
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
---
# 152. The operator's surface is a module the mesh assigns: the console
## Context
**Since the bus moved, nobody can ask the mesh anything without opening a shell on a machine.** Every
tool call an operator's assistant makes fails, on every machine including the one the operator sits
at, with *AMQP not connected*
([issue 147](../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md)).
The program answering is the predecessor's tool server, started on the workstation by hand, with the
predecessor's broker address written into the assistant's own configuration. It has no manifest, no
assignment, no account on the bus, and the mesh has never known it exists. Nothing regressed: the
mesh removed a transport that a program outside the mesh still dials.
**The mesh has a tool model, and it works.** A module serves each tool on its own subject and its
account may serve nothing else ([ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md));
a person is issued an account whose only permission is to publish the tool subjects named at issue
([25 — The bus on NATS](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §7); a client on the
runtime repository's main branch speaks that account as a command line and as an MCP server. Measured
on the live mesh on 2026-09-28: a call to the forge's `gitea_list_repos` answered with real
repositories; the tool list came back empty, because it asks the catalogue for a tool nothing serves
([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)).
**Two records have already said where the surface belongs.** ADR 0132's consequences: *the MCP
surface belongs inside the mesh — a module the mesh assigns to the machine where the agent sits, with
a credential the mesh minted and authority derived from what it may call, not a program started by
hand with a credential printed to a terminal.* Design [33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md)
§6 says the same. Neither is a decision about the surface: 0132 decided where a seat's tools live,
and named the surface in passing.
**And one record says the opposite, in the letter.** [ADR 0095](0095-the-control-plane-is-the-way-to-ask-a-module.md)
made the control plane *the* way to ask a module, deferred "calling is a grant" until something asked
for it, and recorded that *nothing outside the control plane can*. A person's account (design 25 §7,
built 2026-09-28) is exactly that grant, minted for a person. So 0095's exclusivity has already been
widened once without a record saying so; a module that calls tools widens it a second time, and this
record is where that is said.
**What a module's account may do today, counted from the composition** (`internal/broker`): publish
its own events, publish the accept subjects of seats it uses, subscribe its own tools and what it
consumes. No module principal may publish another module's tool subject. Of 72 modules in the
catalogue, 45 serve tools and 0 may call one.
The question the work order asks before any code: **does the mesh grow its own operator surface, or is
the surface an ordinary module that happens to serve tools?**
## Considered Options
**1. The control plane serves the agent protocol itself** — a listener on the controller, or a verb
its binary runs. Rejected. It puts a surface for tools the control plane does not implement on the one
component that must stay answerable while it is itself being replaced, which is the reason 0132
rejected the control plane as the answer to discovery. A person on a workstation would reach it over
the network, and the networked surface [ADR 0035](0035-one-implementation-several-surfaces.md)
reserves for that authenticates through an OAuth2 provider that is not configured — so the controller's
`api` verb correctly serves nothing today, and this option would either wait for it or bypass it.
**2. A program a person installs and starts by hand with a printed credential** — what exists on the
runtime repository's main. Rejected as the end state. It is outside the mesh in every way issue 147
names: no assignment, no declaration, no seat, no check that it reaches anything, revoked only by a
person remembering to. It is the predecessor's arrangement one bus later, and it fails the same way
the next time an address moves. It stays as the recovery path, the way the command line does
(ADR 0035): a credential from `operator issue` and the `mesh` client work with no console assigned.
**3. An ordinary module, assigned to the machine the person sits at, holding a credential the mesh
minted, serving the mesh's tools on that machine's loopback.** Chosen.
## Decision
**The console is a module.** `mesh-console` is built by the mesh, registered like any module,
assigned to a machine, and given a bus credential sealed to that machine. It serves the mesh's tools to
whoever is on that machine: to an agent over MCP, and to a person through the same endpoint. Assigning
it to a machine is what makes the mesh reachable from there; unassigning it revokes that, at the next
composition, with nothing on the machine to remember to remove.
**A grant to call is a manifest word: `invokes`.** A module declares the tools it calls, each as
`<module>.<tool>`, or the single entry `*` for every tool on the mesh. The bus grants exactly that
publish side and nothing beside it — no event, no subscription, no seat. This is ADR 0095's deferred
first option, taken now that a consumer asks for it; a person's account already has this shape, and
the same composition derives both. The control plane's `ask` stands, and 0095's audit point with it:
every call still passes one account whose permission list says what it may ask.
**Authority is the machine's login.** The console listens on the machine's loopback only, declared
`from: machine`, so whoever can open a socket on the machine is whoever owns the machine, and *the
account that installed the host owns the mesh on that node*
([ADR 0034](0034-the-local-account-owns-the-mesh.md)). Anything on a machine may call anything on it,
and that is the whole of local ([ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md)).
The mesh knows no person: what the audit sees is which console asked, under the account
`<node>.mesh-console`. How a person's identity reaches a session is the question design 15 leaves
open, and this record does not close it.
**What the console lists is asked of the modules.** Design 33 §5: a module's own tools are answered by
the module, from the code that defines them. The tool runtime therefore answers one reserved verb for
every module it serves — `tools`, the module's tool names, descriptions and argument schemas — and the
console assembles its list by asking the catalogue which modules the mesh holds and each module what
it answers. A module that is not running is absent from the list and says so; a tool an agent already
knows the name of can be called whether or not it was listed. A module may not name a tool of its own
`tools`; the runtime refuses the collision at load rather than letting one shadow the other. A
role's tools, and the mesh's own verbs, join the list when the `mesh-controller` seat serves them
(design 33 §1, third family) — the console reads whatever the mesh can say about itself, and grows as
that does.
**The console holds one credential and one grant: `*`.** It is the operator's surface on a machine the
operator owns; narrowing what it may call is a setting on its assignment, which
[ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md) already provides for
and nothing here builds.
## Consequences
- **The way a person drives the mesh is inside the mesh.** It is declared, delivered, replaced and
revoked by the same machinery as everything else, and `status` says whether the machine carrying it
has applied. Issue 147's shape — a surface kept alive by an address in a file — cannot recur, because
there is no file: the console's credential names the bus the mesh is on, and moves when it does.
- **A module may now call tools, which ADR 0095 had reserved to the control plane.** The grant is
explicit, per tool or `*`, and derived by the same composition that grants everything else. A module
that declares no `invokes` gains nothing. What got harder: a manifest reviewer has one more field to
read for authority, and `*` in it deserves the reader's attention every time.
- **A new manifest word ships one release before any manifest uses it**, and must reach both parsers:
the build machine's and the running controller's. The console's manifest cannot be registered until
the controller and the builder that packages it have been rebuilt with the word.
- **Discovery costs a fan-out per list.** One request per module the mesh holds, answered at once by
the bus for every module nothing serves, so the cost is bounded by the modules that are up. The list
is cached briefly in the console; a module assigned a moment ago appears at the next refresh.
- **Every tool runtime must be rebuilt once** to answer `tools`. Until a module is, it is callable and
not listed, and the console says which modules did not answer.
- **The mesh's own verbs are not in the console yet.** `status`, `push`, `assign` are the
`mesh-controller` seat's tools under 0132, and the three prerequisites 0132 names are still not in
place. A person asking what a node runs still opens a shell for that question, and that gap is design
33's to close, not this record's — recorded here so nobody reads the console as the whole of 147.
- **The person's credential is not retired.** `operator issue` and the `mesh` client remain the path
when no console is assigned, and the path an operator uses to bring a mesh up far enough to assign
one.
## How this is checked
| Rule | Checked by |
|---|---|
| A module's `invokes` becomes exactly that publish grant, and nothing else | the bus user composition test: a module invoking `shop.price` may publish that subject and no other tool's; `*` may publish every tool subject; neither may publish an event or subscribe anything it did not consume |
| A malformed `invokes` entry is refused at registration | a parser test: an entry naming no tool is a problem named in the manifest's words |
| The runtime answers `tools` for every module it serves | the runtime's test: a module registering two tools answers three names, and a module naming one of its own `tools` is refused at load |
| The console's list is what the modules answer | the client's test against a real bus: two modules up, a third the catalogue holds and nothing serves, and the list carries the two and names the third as not answering |
| A call from the console reaches a module over the bus | the same test, and the live mesh: the console assigned to a workstation answers `tools/list` on its loopback and a call to the forge returns repositories |
| The console listens on loopback and nowhere else | its manifest declares `from: machine`, and the filter composed for the machine opens nothing for it |
## References
- [issue 147](../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md) — the surface outside the mesh
- [ADR 0095](0095-the-control-plane-is-the-way-to-ask-a-module.md) — extended: a grant to call, for a module as for a person
- [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) — where a seat's tools live, and the sentence that named the surface
- [ADR 0035](0035-one-implementation-several-surfaces.md) — three surfaces over one implementation
- [ADR 0034](0034-the-local-account-owns-the-mesh.md), [ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md) — why loopback is the authority boundary
- [33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §5, §6 — discovery, and what serves it to an agent
- [34 — The console](../03-DESIGN/01-to-be/34-the-console.md) — the design this record authorises
- mesh-tools `src/client.ts`, `src/mcp.ts`, `src/mesh.ts` — the client this makes a module of
+2
View File
@@ -200,6 +200,7 @@ python3 00-META/checks/index.py fail if stale
- **0109** — [A package registry seat is one per ecosystem, not one for all of them](0109-a-package-registry-seat-is-one-per-ecosystem.md)
- **0126** — [A module declares its own seats; the mesh reserves its own](0126-a-module-declares-its-own-seats.md)
- **0148** — [The mesh's names are resolved, not copied into every container](0148-the-meshs-names-are-resolved-not-copied-into-containers.md)
- **0151** — [A route's internal name is composed under the node that serves it](0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md)
### What runs on them, and how it gets there
@@ -255,6 +256,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)
- **0147** — [A module anchors the mesh's authority on a machine, and takes it away again](0147-a-module-anchors-the-meshs-authority.md)
- **0150** — [A module's own code runs as supervised processes under the module's one account](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md)
- **0152** — [The operator's surface is a module the mesh assigns: the console](0152-the-operators-surface-is-a-module-the-console.md)
### How it is built
+17 -5
View File
@@ -10,6 +10,7 @@ code:
updated: 2026-09-30
decisions:
- 02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md
- 02-DECISIONS/0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md
- 02-DECISIONS/0147-a-module-anchors-the-meshs-authority.md
- 02-DECISIONS/0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md
- 02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md
@@ -305,10 +306,11 @@ and [135](../../04-ISSUES/135-a-containers-mesh-names-are-not-compared/00-report
**A container resolves the mesh's names through its machine's resolver, at the moment it asks, and
nothing is copied.** The resolver is a machine-level process rather than a container, so nothing
circular is being asked for. This is gated on a container being able to reach the resolver from any of
the runtime's networks, which it cannot today
([issue 110](../../04-ISSUES/110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/00-report.md)) —
until that lands the mesh keeps copying and keeps comparing, and the order is stated in the record.
circular is being asked for. It was gated on a container being able to reach the resolver from any of
the runtime's networks
([issue 110](../../04-ISSUES/110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/00-report.md)),
and landed the day that did, 2026-09-30: the controller writes no mesh name into a container and the
host's digest carries only what the module declared for itself.
The paragraph below states the old boundary, and 0148 deliberately gives it up: a container somebody
started by hand resolves the same names as everything else, because the resolver answers the machine,
@@ -324,6 +326,14 @@ machine — declared or not — is what a nameserver in `resolv.conf` would be f
service, the rest is the node — so what resolves is *anything under a node's name*, going to that
node. What routes it once it arrives is a proxy's, and stays separate.
*2026-09-30.* **So the node in a route's internal name is the one whose proxy answers it**
([ADR 0151](../../02-DECISIONS/0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md)).
Composed from the node the module ran on, the name sent a client to a machine with nothing listening
whenever the proxy ran elsewhere
([issue 139](../../04-ISSUES/139-an-internal-route-name-resolves-to-the-consumers-node/00-report.md));
composed from the serving node, the rule above holds without exception. The public name stays the
module's node's, which is where the operator put it.
**The mesh writes the data and runs no daemon.** One wildcard per machine, from the same set that
writes the hosts file. A resolver is third-party software and runs *on* the mesh rather than being
*of* it: the mesh has no business shipping one, choosing which one, or knowing its configuration
@@ -399,7 +409,9 @@ can reach from the outside but cannot resolve from the inside is a name it canno
authority of its own.
**So a granted route is published into internal resolution as well** — the routed name to the node
that serves it, mesh-wide, by the same mechanism that writes the node names. It is *given by the
that serves it, mesh-wide, by the same mechanism that writes the node names — and as itself: a routed
name has no mesh form, and the suffixed alias the roster once added beside it resolved to a refusal
([issue 157](../../04-ISSUES/157-a-routed-names-internal-alias-is-served-by-nothing/00-report.md)). It is *given by the
mesh, not chosen by a module*, for the same reason the node names are: a module listing the routes
would go stale the day one changes. The mesh propagates the names it was told to serve and still
knows nothing about what they mean
+28 -1
View File
@@ -7,9 +7,10 @@ code:
- mesh-controller internal/catalogue/build.go
- mesh-controller internal/inventory/secrets.go
- mesh-controller cmd/mesh-builder
updated: 2026-09-12
updated: 2026-09-30
decisions:
- 02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md
- 02-DECISIONS/0037-where-a-module-lives.md
- 02-DECISIONS/0009-modules-and-the-graph.md
- 02-DECISIONS/0010-delivery.md
- 02-DECISIONS/0005-the-node-host.md
@@ -60,6 +61,32 @@ module from a repository and a path, and the root-only reading left every existi
unbuildable — pointed at the catalogue the builder finds no manifest, pointed at a module's source
it finds no manifest either.*
## A manifest is checked where it is written
*Added 2026-09-30, from [issue 148](../../04-ISSUES/148-a-manifest-outside-this-catalogue-has-no-check/00-report.md).*
The check the mesh applies at registration — the manifest parses strictly, every name in it is a
usable one, its routes and events and seats are well formed, and no two manifests given together
declare one seat — is a verb on the controller's binary, `module check <manifest>…`, and it needs no
mesh. It reads the files it is given, runs the same functions registration runs, prints every problem
in the manifest's own words, and exits non-zero if there was one. Somebody describing their own
application in their own repository — the case [ADR 0037](../../02-DECISIONS/0037-where-a-module-lives.md)
calls the one that matters most — runs it before pushing, and finds out there rather than when a
running mesh refuses the registration, or later, when a machine applies something that resolved and
should not have.
**What it cannot know, it says.** A seat another module declares elsewhere is unknown to a check that
was not handed that module's manifest, and the output says so rather than refusing: pass the other
manifest too. The mesh's own seats it knows from the binary, which is the one place that set may be
read without a store ([ADR 0122](../../02-DECISIONS/0122-a-seat-is-data-a-rename-is-a-database-update.md)
keeps the store authoritative, so a claim on a mesh seat is judged fully only at registration, and the
check says that too).
*How it is checked:* the controller's test runs the check over the catalogue checkout beside it and
over a manifest with a known fault, and asserts the first passes and the second names the fault; the
test that used to be the only check, `TestEveryCatalogueManifestParses`, now stands beside a command
anybody can run.
## The manifest in the repository is not the manifest the mesh holds
A resource names an artifact:
+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).
*Built, and then made a module — 2026-09-30.* (1) and (2) exist: `operator issue` and the `mesh`
client. What (2) describes as a program on the workstation is now the recovery path; the surface an
operator uses is a module the mesh assigns to the machine, holding a credential the mesh minted —
[ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md),
[34 — The console](34-the-console.md). The tool list it asks for is no longer
`catalog_tools`, which nothing served: each runtime answers `tools` for its own module.
## 8. What a module sees, and what the wire does
**The contract a module is written against does not change.** `publish` on an envelope becomes a
@@ -102,6 +102,12 @@ being something a person carries and becomes something the mesh runs, on a node,
An agent's authority can then be role-shaped: *the forge's tools*, rather than a list of
module-specific names that changes the day the forge is replaced.
*Decided and designed on 2026-09-30:* the module is the console —
[ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md),
[34 — The console](34-the-console.md). It builds the second half of §5 now (a module's tools are
asked of the module, through a `tools` verb every runtime answers) and lists a role's tools when the
records carry them.
## 7. Versioning
A seat's tools are an interface and change like one. Additive within a version. A change that would
+116
View File
@@ -0,0 +1,116 @@
---
layer: to-be
status: in-progress
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 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
@@ -1,7 +1,7 @@
---
status: located
opened: 2026-08-23
located-in: [hal, hq]
located-in: [hq README.md, and the agent ADR 0025 names — not built]
fixed-by:
amended-design: 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
---
@@ -169,3 +169,16 @@ them into. The record stays open, and its answer is no longer "index this reposi
is whatever the mesh grows as its own knowledge surface, if it grows one. Until then the honest fix
is the README, which should stop claiming a property nothing provides.
## Where this stands, 2026-09-30
The README no longer claims a property nothing provides: it says the indexing never existed, that the
store it named is unreachable since the cut-over, and that ADR 0025's answer — read, not copied, by an
agent that consults this repository — is decided and not built. That was the honest fix the previous
note asked for, and it is done.
The record stays open on ADR 0025's build, and on nothing else. The mesh's own operator surface is now
a module ([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)); the
reader ADR 0025 describes is the mesh session of design 15, which would answer through that surface
like any tool. What closes this is still the check 0025 names: search for a phrase that appears only in
a design document here, and get it back.
@@ -1,8 +1,8 @@
---
status: located
status: resolved
opened: 2026-09-24
located-in: [mesh-host internal/apply]
fixed-by:
located-in: [mesh-controller internal/catalogue/declaration.go (every container was given the roster at creation)]
fixed-by: mesh-controller PR 161 — no container is given a mesh name; it resolves through its machine's resolver (ADR 0148, landed 2026-09-30 once issue 110 did)
amended-design:
---
@@ -73,3 +73,13 @@ copying: a container resolves through its machine's resolver at the moment it as
record reports then has nowhere to occur. It is gated on
[issue 110](../110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/00-report.md), so
until that lands the mesh still copies and still compares.
## Resolved (2026-09-30)
110 landed the same day ([its resolution](../110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/01-resolution.md)),
and mesh-controller PR 161 then removed the copy: no container is given a mesh name or a mesh address,
and a module's own declared entries are the only `host` lines it carries. Verified on the control-node
after its containers were recreated once — the last time a name will do that: the forge's container
carries no extra hosts and resolves another machine and a routed name through the machine's resolver,
so the shape this record describes has nowhere to occur. Checked in the controller's tests: a
container's declaration is byte-for-byte the same under a roster of one machine and a roster of three.
@@ -1,8 +1,9 @@
---
status: open
status: resolved
opened: 2026-09-24
located-in: []
fixed-by:
located-in:
- mesh-catalog modules/dnsmasq (the runtime was never told; the resolver answered by interface)
fixed-by: mesh-catalog PR 175 (the runtime is reloaded and keeps its containers over a restart) and PR 176 (the resolver answers by address, so a query from a bridge is admitted) — measured 2026-09-30, 01-resolution.md
amended-design:
---
@@ -67,3 +68,6 @@ and [135](../135-a-containers-mesh-names-are-not-compared/00-report.md)).
network is the case with no DNS at all, and it is the case the mesh's own forge runs in. Two of four
machines also bind the resolver to loopback only, so the runtime hands their containers a public
resolver. Both halves are this issue.
*Later the same day: the second half was wrong, and the first had a different cause than the one above.
[01-resolution.md](01-resolution.md) has what was actually found.*
@@ -0,0 +1,64 @@
# 110 — resolved: a container on any network reaches the resolver, and is answered
*2026-09-30. Measured on the three converged machines; the adopted one holds its resolver module until it
is taken and is not covered.*
## What was actually wrong
Not what the report predicted. The report named the filter: a container on the runtime's default
network asks from a bridge address, and the converged filter admitted queries by source address only.
That was true when it was written and was fixed before this issue was ever tested — the filter admits
by the link a packet arrives on ([ADR 0144](../../02-DECISIONS/0144-anything-on-a-machine-may-call-anything-on-it.md)),
and a container's bridge is admitted whole. Tested on every machine: the query arrives, the filter
passes it.
Three other things were wrong, each hiding the next.
**The runtime had never been told.** The resolver module writes the runtime's `dns` key into the
runtime's own configuration file. The runtime reads that key when it starts and not on a reload, and on
two machines the runtime predated the file — so every container they started got a public resolver, and
`novox.internal` came back as not existing. Nothing reported this: the file was present and current,
the resolver ran, and a name not existing is a valid answer. Fixed in mesh-catalog PR 175: the module
also sets `live-restore` and reloads the runtime when its file changes, so the one restart the `dns` key
needs no longer stops every container. The restart is then the operator's, once per machine; done on
both today, with every running container kept.
**The resolver dropped the query.** With the runtime corrected, a container's query reached the resolver
— and got no answer, on every machine, including the one whose runtime had been right all along. The
socket was bound to the private address; the filter admitted the packet; dnsmasq received it and
discarded it without a line of log. Its configuration said `interface=mesh0`, and dnsmasq admits a
query by the interface it arrives on when told an interface: a container's query is addressed to the
private address but arrives on the runtime's bridge, and the bridge is not `mesh0`. Fixed in mesh-catalog
PR 176: the resolver is told the address to answer on, not the interface that carries it, and a query to
that address is admitted whatever bridge brings it. The bridges are the runtime's to name.
**The report's second half was wrong.** "Two of four machines bind the resolver to loopback only" was
an inference from the containers' behaviour, and the behaviour had the cause above. The resolver bound
the private address on all four; nothing had asked it there.
## What is verified
From a container on the runtime's default network, started by hand and given nothing, on each of the
three converged machines: `novox.internal` answers with the hub's private address, through the machine's
own resolver. That is the fourth check of
[ADR 0148](../../02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md) — "on every
network the runtime offers" — and its first step; the record's step 2 (the runtime told per machine, as
a file) was already how the module works. Step 3 may now begin.
## What checks it
By hand, today. Nothing in the mesh asserts that a container can resolve a mesh name: the resolver's
own tests cover what it answers, not who can ask. The check that would have caught all three faults is
the one the report asked for and 0148 lists — a container on the default network resolving a mesh name
— and it is not built. It belongs with the reachability check of
[issue 145](../145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md),
which is parked; until then this is a thing a person verifies after touching the resolver, the filter,
or the runtime's configuration.
## What this cost to find
The three faults produced one symptom — a container that cannot resolve — and each fix revealed the
next. The first was found by reading the runtime's own view of its configuration rather than the file;
the second by capturing the query on the bridge and finding it arrive and go unanswered; the third only
by admitting the first belief was wrong. A machine that had been believed to work all day had never
worked either.
@@ -1,9 +1,9 @@
---
status: open
status: resolved
opened: 2026-09-28
located-in: [mesh-controller internal/catalogue]
fixed-by:
amended-design:
located-in: [mesh-controller internal/catalogue/declaration.go (composeName took the consumer's own name as the internal domain)]
fixed-by: mesh-controller PR 163 — the internal name composes under the node whose proxy serves the route (ADR 0151, 2026-09-30)
amended-design: 03-DESIGN/01-to-be/08-connectivity.md
---
# 139 — An internal route name resolves to the consumer's node, not the one that serves it
@@ -49,3 +49,15 @@ resolve whether or not anything answers.
and if so, is `route` still one mesh-wide provision or a node-scoped seat with a mesh-wide fallback?
- What certifies the name in either case? The certificate is obtained by whoever terminates TLS, and
that is the question above in another form.
## Answered (2026-09-30)
[ADR 0151](../../02-DECISIONS/0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md):
the internal name is composed under the node that serves the route — the machine the request arrives
at — because `<x>.<node>.internal` means *goes to that node* and nothing else. The public name stays
the consumer node's, which is where the operator put it. The first question is answered that way; the
second, a per-node route holder, is a decision about seats and is left where it is; the third is
unchanged, since the proxy that terminates the name is given it and certifies it.
mesh-controller PR 163 carries it. On this mesh every route is served beside its module, so no name
changed; the controller's tests hold the case where it would.
@@ -1,7 +1,9 @@
---
status: located
status: resolved
opened: 2026-09-29
located-in: [nothing in the mesh — the surface in use is the predecessor's, installed on the workstation]
located-in: [mesh-catalog modules/mesh-console, mesh-tools src/mesh.ts, mesh-controller internal/broker]
fixed-by: ADR 0152; mesh-controller PR 164 (invokes); mesh-tools PR 20 (mesh serve, the tools verb); mesh-catalog PR 181 (mesh-console)
amended-design: 03-DESIGN/01-to-be/34-the-console.md
---
# 147 — the operator's tools still dial the bus that was removed
@@ -73,3 +75,21 @@ outside the mesh.
- It fails identically for the local machine, which rules out reachability and points at the
transport alone.
- The mesh's own traffic over the new bus is unaffected: nodes report, declarations apply.
## Answered, 2026-09-30
The work order's question — does the mesh grow its own operator surface, or is the surface an
ordinary module — is answered by [ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md):
**the console is a module.** `mesh-console` is assigned to the machine a person sits at, holds a
credential the mesh minted, calls tools under a grant its manifest declares (`invokes`), and serves the
mesh's tools on that machine's loopback to an agent over MCP and to a person through the same endpoint.
Designed in [34 — The console](../../03-DESIGN/01-to-be/34-the-console.md).
What that leaves, said plainly so nobody reads this record as closed on the whole of its first
paragraph: the console reaches every tool a *module* serves. The mesh's own questions — what a node
runs, what is assigned — are the `mesh-controller` seat's tools under ADR 0132 and are not on the bus
yet; for those a shell is still the way, and design 33 is where that closes.
The predecessor's program on the workstation is not replaced by the mesh; it is left where it is and
the assistant is pointed at the console beside it. The `hal` entry in the assistant's configuration
still names things that are not the mesh's.
@@ -1,7 +1,9 @@
---
status: open
status: resolved
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
@@ -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.
- `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.
## Built, 2026-09-30
`mesh-controller module check <manifest>…` runs what registration runs — the strict parse, the
per-manifest problems, and the cross-manifest rules over every manifest given — with no store and no
mesh, prints every problem in the manifest's words, and exits non-zero on any. What it cannot judge
without a store it says: a claim on one of the mesh's own seats is judged fully only at registration
([ADR 0122](../../02-DECISIONS/0122-a-seat-is-data-a-rename-is-a-database-update.md)), and a seat
declared by a module whose manifest was not passed reads as unknown. Written into
[12 — A module repository](../../03-DESIGN/01-to-be/12-a-module-repository.md) as the section *a
manifest is checked where it is written*. The console's own manifest was the first checked with it.
@@ -1,10 +1,10 @@
---
status: open
status: resolved
opened: 2026-09-29
located-in:
- mesh-host internal/apply/apply.go (containerSpecReading hashes every `host` entry)
- mesh-controller internal/catalogue/declaration.go (withMeshNames gives every container the mesh's names)
fixed-by:
fixed-by: mesh-controller PR 161 — the roster left every container's declaration and so its digest (ADR 0148 step 3, 2026-09-30)
---
# 151 — A new name recreates every container in the mesh
@@ -92,3 +92,15 @@ copying names until a container can reach the resolver from any of the runtime's
([issue 110](../110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/00-report.md)),
which it cannot on two of four machines today. Removing the copy first reintroduces 109 and 135
silently, on a live mesh, which is how both were found. The order is in the record.
## Resolved (2026-09-30)
Step 3 landed the day 110 did. mesh-controller PR 161 stops writing the roster into any container, so
a container's digest no longer carries a name that is not its own. The controller's tests hold the
record's check — a container's declaration does not move when the mesh's roster does, and does move
when the module's own declared entries do.
The first push after the change recreated every container once, because every digest lost its host
entries at the same moment. That was the last such event: from here a name added or moved on one
machine changes no container anywhere, and the record's second check — add a routed name, watch every
other machine's apply report say nothing changed — is what the next module assignment will show.
@@ -1,9 +1,9 @@
---
status: located
status: resolved
opened: 2026-09-30
located-in:
- mesh-controller internal/catalogue/roster.go (the roster's entries for a routed name)
fixed-by:
fixed-by: mesh-controller PR 163 — a routed name is published as itself, once, with no suffixed alias (ADR 0151, 2026-09-30)
amended-design:
---
@@ -61,3 +61,10 @@ composition produces `keycloak.novox.be.internal`, which is not a name anything
The fix is a judgement about what a routed name's internal form is, and 139 is the record that asks it;
this one is the evidence that the current answer publishes a third thing that is neither.
## Resolved (2026-09-30)
The judgement 139 asked for is [ADR 0151](../../02-DECISIONS/0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md):
a routed name has no mesh form. The roster now publishes it as itself, once, at the serving node's
address; the `<domain>.internal` line is gone from every machine's hosts file, and a controller test
refuses it coming back.
@@ -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
is authored is then a separate question.
**That indexing does not exist.** It was checked on 2026-08-23 and returns nothing; it appears
never to have existed. Until it does, the objection stands unanswered and this repository is
the fourth knowledge system it was argued not to be. Recorded as
[`04-ISSUES/006`](04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md), and
left standing 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.
**That indexing never existed, and the store it would have indexed into is gone.** It was checked
on 2026-08-23 and returned nothing; the knowledge base it named was the predecessor's, and since the
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 answered
by [ADR 0025](02-DECISIONS/0025-the-design-record-is-read-not-copied.md): these documents are
**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: