Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49b1136ded |
@@ -75,18 +75,6 @@ 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,14 +47,6 @@ 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,13 +103,6 @@ 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.
|
||||
@@ -150,16 +143,6 @@ 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
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,160 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -200,7 +200,6 @@ 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
|
||||
|
||||
@@ -256,7 +255,6 @@ 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
|
||||
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
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]
|
||||
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
|
||||
---
|
||||
|
||||
# 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.
|
||||
|
||||
## What it does not answer
|
||||
|
||||
The mesh's own verbs. `status`, `push`, `assign` and the rest are not served on the bus — they are the
|
||||
`mesh-controller` seat's tools under ADR 0132, whose three prerequisites are not built — so a person
|
||||
still opens a shell on the control node for them. The console's handshake says so.
|
||||
|
||||
## 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.
|
||||
@@ -21,7 +21,6 @@ Where the two disagree, the implementation wins and the disagreement is stated.
|
||||
| [`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 |
|
||||
| [`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
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ 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
|
||||
@@ -306,11 +305,10 @@ 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. 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.
|
||||
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.
|
||||
|
||||
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,
|
||||
@@ -326,14 +324,6 @@ 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
|
||||
@@ -409,9 +399,7 @@ 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 — 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
|
||||
that serves it, mesh-wide, by the same mechanism that writes the node names. 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
|
||||
|
||||
@@ -7,10 +7,9 @@ code:
|
||||
- mesh-controller internal/catalogue/build.go
|
||||
- mesh-controller internal/inventory/secrets.go
|
||||
- mesh-controller cmd/mesh-builder
|
||||
updated: 2026-09-30
|
||||
updated: 2026-09-12
|
||||
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
|
||||
@@ -61,32 +60,6 @@ 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:
|
||||
|
||||
@@ -361,13 +361,6 @@ 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,12 +102,6 @@ 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
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
status: located
|
||||
opened: 2026-08-23
|
||||
located-in: [hq README.md, and the agent ADR 0025 names — not built]
|
||||
located-in: [hal, hq]
|
||||
fixed-by:
|
||||
amended-design: 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
|
||||
---
|
||||
@@ -169,16 +169,3 @@ 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: resolved
|
||||
status: located
|
||||
opened: 2026-09-23
|
||||
located-in: [mesh-controller internal/link, mesh-host internal/link]
|
||||
fixed-by: mesh-host PR 59 (the host refuses an older sequence and drains by it), mesh-controller PR 160 (each send is numbered under the node's hold) — measured 2026-09-30, 02-resolution.md
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
|
||||
@@ -66,11 +66,3 @@ Left `located`. The owner is unchanged, the shape of the fix is agreed, and the
|
||||
[issue 142](../142-the-host-is-the-one-thing-the-mesh-does-not-deliver/00-report.md) rather than anything
|
||||
in this record. **This is a judgement about order, not a refusal** — it is cheap to overrule, and the
|
||||
code is a day's work once a host can be delivered.
|
||||
|
||||
## The gate has opened (2026-09-30, evening)
|
||||
|
||||
The mesh delivers the host now — built by its own toolchain, published to its own registry, delivered
|
||||
over the bus and started by the launcher, on all four machines
|
||||
([issue 142](../142-the-host-is-the-one-thing-the-mesh-does-not-deliver/01-progress.md)). A declaration
|
||||
field is a build and a push, not an expedition. The order this record asked for — hosts first, then
|
||||
the controller — is now two commands and a status line that says when the first has finished.
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
# 107 — resolved: a declaration carries its order
|
||||
|
||||
*2026-09-30. Measured on the mesh.*
|
||||
|
||||
## What was done
|
||||
|
||||
**Hosts first, then the controller** — the order [issue 087](../087-the-controller-cannot-tell-a-host-is-too-old/00-report.md)
|
||||
says a new declaration field needs, and now a build and a push rather than an expedition
|
||||
([issue 142](../142-the-host-is-the-one-thing-the-mesh-does-not-deliver/01-progress.md)).
|
||||
|
||||
The host understands a `sequence` on a declaration and tolerates its absence: absent reads as "no
|
||||
order claimed", not "first", so a controller that sends none is still understood and a host that kept
|
||||
a declaration before it understood the field compares nothing. It refuses a declaration with a lower
|
||||
sequence than the one it kept, whole, and says why; and the drain that picks one declaration from a
|
||||
batch keeps the highest sequence rather than the last to arrive — which is the case the report
|
||||
constructed, a backlog drained out of order.
|
||||
|
||||
The controller numbers each send: the next number for that node, taken under the node's hold, before
|
||||
the body exists, so the number is inside what the mesh signs and a replayed older declaration cannot
|
||||
borrow a newer one's.
|
||||
|
||||
## Measured
|
||||
|
||||
```
|
||||
push shanks; push shanks
|
||||
sequence in kept declaration: 2
|
||||
node sequence
|
||||
novox 2
|
||||
shanks 2
|
||||
ace (none — not sent since numbering)
|
||||
g14 (none)
|
||||
status: nobody "not running what the mesh would send them"
|
||||
```
|
||||
|
||||
Both applies went through; neither was refused; the machine holding the earlier one accepted the later.
|
||||
|
||||
## The subtlety, which would have read every machine as behind for ever
|
||||
|
||||
The mesh decides a machine is behind by comparing the digest of what it **would** send against what it
|
||||
**did** send. A number changes the bytes. So the read-only comparison composes with the number the
|
||||
machine was *last* sent — not a fresh one — and is byte for byte what was sent when nothing else
|
||||
changed. Without that, numbering would have made `status` name all four machines as out of date on
|
||||
every reading, permanently.
|
||||
|
||||
## The open questions
|
||||
|
||||
- *A per-node `sequence` under the controller's node hold?* Yes, as described. **`supersedes` — the
|
||||
previous digest — is not added.** A strictly-greater sequence gives the ordering; a chain of digests
|
||||
would give continuity, which nothing here needs yet and which every re-composition would break.
|
||||
- *Genesis signing its bundle as sequence zero?* Zero is "no order claimed", which is what the bundle
|
||||
carries by carrying nothing. Same rule, no genesis branch.
|
||||
- *A marker for a mode change?* Not needed for the incident it guards: a replayed converged declaration
|
||||
reaching a node returned to adopted is already refused **by mode**, before this check runs.
|
||||
|
||||
## How it is checked
|
||||
|
||||
Host: an older sequence is refused, a newer or equal one is not, and no order claimed on either side
|
||||
compares nothing; the drain keeps the highest sequence, and falls back to arrival when none is claimed.
|
||||
Controller: a send carries its number inside the signed bytes, an unnumbered send is byte for byte what
|
||||
it was before, and each node's counter is one higher per send and readable for the comparison.
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
status: resolved
|
||||
status: located
|
||||
opened: 2026-09-24
|
||||
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)
|
||||
located-in: [mesh-host internal/apply]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
@@ -73,13 +73,3 @@ 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.
|
||||
|
||||
+3
-7
@@ -1,9 +1,8 @@
|
||||
---
|
||||
status: resolved
|
||||
status: open
|
||||
opened: 2026-09-24
|
||||
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
|
||||
located-in: []
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
@@ -68,6 +67,3 @@ 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.*
|
||||
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
# 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: resolved
|
||||
status: open
|
||||
opened: 2026-09-28
|
||||
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
|
||||
located-in: [mesh-controller internal/catalogue]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 139 — An internal route name resolves to the consumer's node, not the one that serves it
|
||||
@@ -49,15 +49,3 @@ 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.
|
||||
|
||||
@@ -138,48 +138,3 @@ push. The control node is worth last.
|
||||
One thing this found on the way out: an archive cannot be undeclared, and the attempt stops the machine
|
||||
applying anything at all — [issue 162](../162-an-archive-cannot-be-undeclared/00-report.md). It is how
|
||||
undoing the first delivery froze the workstation, and it is not specific to the host.
|
||||
|
||||
## Every machine self-updates (2026-09-30, evening)
|
||||
|
||||
```
|
||||
shanks 76f4566bef3d/nox-mesh-host active
|
||||
g14 76f4566bef3d/nox-mesh-host active
|
||||
novox 76f4566bef3d/nox-mesh-host active
|
||||
ace 76f4566bef3d/nox-mesh-host active
|
||||
|
||||
mesh-controller status: (no host split)
|
||||
```
|
||||
|
||||
The last delivery was unattended on all four: the fixed host was built, pushed, each machine stood
|
||||
aside exactly once for the genuinely newer version, and the delivered launcher started it — no
|
||||
restart by hand. A following push that delivered nothing new was applied and reported by every
|
||||
machine and stood nobody aside, which is the check
|
||||
[issue 163](../163-a-delivered-host-stood-aside-on-every-push-and-reported-nothing/00-report.md) asks
|
||||
for.
|
||||
|
||||
**Two more faults on the way, both mine, both found by reading the machine rather than the success
|
||||
line.** A delivered host compared the newest delivered version against its link-time stamp rather
|
||||
than the version it was running, so it stood aside on every push and — because standing aside cancels
|
||||
the report — never reported again (163). And the adopted machine kept its found launcher as the
|
||||
adoption rule says, so the delivery there needed a `take` before the launcher moved.
|
||||
|
||||
**The crossover needs one restart of the unit per machine, once.** The launcher process that was
|
||||
running on each machine was the old script, executing from its own inode; a new file beside it
|
||||
changes nothing until the unit restarts. Every subsequent delivery is unattended.
|
||||
|
||||
**Timing, measured:** on a machine, hearing a declaration to reporting it applied is about three
|
||||
seconds. A push as the operator sees it takes 17–20 seconds, and the difference is the control plane
|
||||
composing the declaration before it sends. A `--wait` shorter than that reads as "did not report" for
|
||||
a machine that did; the three-minute default read as slowness for a machine that never would. Neither
|
||||
number is a defect being chased here, and both are worth knowing before reading a push's answer.
|
||||
|
||||
## What this leaves
|
||||
|
||||
- [Issue 162](../162-an-archive-cannot-be-undeclared/00-report.md): an archive cannot be undeclared, so
|
||||
the host module — and any module with an archive — cannot be unassigned, and trying stops the machine
|
||||
applying anything.
|
||||
- [Issue 107](../107-a-declaration-carries-no-order/00-report.md) is unblocked: a declaration field is
|
||||
now a build and a push rather than an expedition.
|
||||
- Three stale version directories on the workstation from the first attempts, moved aside under
|
||||
`/var/lib/mesh-host/versions-held-back/`, and a backup of the adopted machine's hand-placed binary
|
||||
beside its state. Both are safe to delete and are not the mesh's to delete.
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
---
|
||||
status: resolved
|
||||
status: located
|
||||
opened: 2026-09-29
|
||||
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
|
||||
located-in: [nothing in the mesh — the surface in use is the predecessor's, installed on the workstation]
|
||||
---
|
||||
|
||||
# 147 — the operator's tools still dial the bus that was removed
|
||||
@@ -75,28 +73,3 @@ 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.
|
||||
|
||||
**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,9 +1,7 @@
|
||||
---
|
||||
status: resolved
|
||||
status: open
|
||||
opened: 2026-09-29
|
||||
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
|
||||
located-in: [mesh-controller cmd/mesh-controller]
|
||||
---
|
||||
|
||||
# 148 — a manifest outside this catalogue has no check
|
||||
@@ -37,14 +35,3 @@ 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: resolved
|
||||
status: open
|
||||
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: mesh-controller PR 161 — the roster left every container's declaration and so its digest (ADR 0148 step 3, 2026-09-30)
|
||||
fixed-by:
|
||||
---
|
||||
|
||||
# 151 — A new name recreates every container in the mesh
|
||||
@@ -92,15 +92,3 @@ 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: resolved
|
||||
status: located
|
||||
opened: 2026-09-30
|
||||
located-in:
|
||||
- mesh-controller internal/catalogue/roster.go (the roster's entries for a routed name)
|
||||
fixed-by: mesh-controller PR 163 — a routed name is published as itself, once, with no suffixed alias (ADR 0151, 2026-09-30)
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
@@ -61,10 +61,3 @@ 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.
|
||||
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
---
|
||||
status: resolved
|
||||
opened: 2026-09-30
|
||||
located-in: [mesh-host cmd/mesh-host/main.go (the successor check after an apply)]
|
||||
fixed-by: mesh-host PR 58 — the check asks with the running version, read from the binary's path, not the link-time stamp
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 163 — A delivered host stood aside on every push, and reported nothing
|
||||
|
||||
## What was observed
|
||||
|
||||
*2026-09-30, rolling the mesh-built host onto the last two machines.*
|
||||
|
||||
Every push to a machine running a delivered host produced, in order:
|
||||
|
||||
```
|
||||
host 093231796eb0 is delivered; standing aside so the launcher runs it
|
||||
applied 333 resource(s)
|
||||
applied, and could not tell the mesh: reporting: context canceled
|
||||
nox-mesh-host-launch: the host exited cleanly; starting it again
|
||||
nox-mesh-host-launch: running /usr/lib/nox-mesh-host/versions/093231796eb0/nox-mesh-host
|
||||
```
|
||||
|
||||
— for the version it was **already running**. It restarted itself on every push, for ever, and the mesh
|
||||
never received a single report from it: `node show` kept the version from before the crossover, and
|
||||
the operator's push waited its full three minutes for an answer that was never coming.
|
||||
|
||||
Read as healthy throughout: unit active, bus link up, "hearing what this node should be".
|
||||
|
||||
## Why
|
||||
|
||||
After an apply the host asks whether a newer host has been delivered than the one running, and the
|
||||
question was asked with the **link-time version stamp**. Since
|
||||
[issue 161](../161-a-delivered-host-carries-none-of-its-link-time-facts/01-resolution.md) a delivered
|
||||
host's version comes from where it sits and its stamp is `development build` — so the comparison never
|
||||
matched the newest delivered version, and "a newer host is waiting" was always true.
|
||||
|
||||
Standing aside cancels the context the report is published with, so the report was lost on every one
|
||||
of those applies. Two faults from one wrong argument.
|
||||
|
||||
The change that moved the version to the path was applied to the report and to the known-good record,
|
||||
and not here. Half a change, and the half left behind was the one that decides whether to exit.
|
||||
|
||||
## Why the three-minute wait made it invisible
|
||||
|
||||
The push's `--wait` timing out read as *slow*. It was not slow: **the report was never going to arrive.**
|
||||
The operator put it exactly: *if you don't get a response in five seconds, something is wrong.* A wait
|
||||
long enough to absorb a machine's whole apply is a wait long enough to hide that the machine never
|
||||
answered.
|
||||
|
||||
## How it is checked
|
||||
|
||||
A machine running a delivered host is pushed a declaration that delivers nothing new; it applies,
|
||||
reports, and does not stand aside. A machine running a delivered host is pushed a genuinely newer
|
||||
version; it stands aside once, and the next push it does not.
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-09-30
|
||||
located-in:
|
||||
- mesh-controller internal/inventory/secrets.go (SecretFor mints a pair credential nobody accepted)
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 164 — A credential that must be accepted is minted anyway
|
||||
|
||||
## What was observed
|
||||
|
||||
Provisioning ace's modules. Several providers hold exactly one credential they did not get from the
|
||||
mesh and cannot take one from it: a Servarr app's API key (sonarr, radarr, lidarr), jackett's API key,
|
||||
plex's X-Plex-Token, nzbget's ControlPassword, qBittorrent's WebUI password. Their consumers' pair
|
||||
credential must be **accepted** by the operator (ADR 0092). Until it is, `SecretFor` mints a random
|
||||
value, seals it to both ends, and reports nothing: the value can never work.
|
||||
|
||||
Every consumer therefore had to learn to detect it — try the credential against the provider first,
|
||||
refuse a value the provider rejects, print the `secret accept` command — six write-in steps, one probe
|
||||
each (ombi, home-assistant, and the four download-stack consumers). qBittorrent bans an address after
|
||||
five failed logins, so a consumer retrying a minted value locks itself out.
|
||||
|
||||
## What would be right
|
||||
|
||||
A provision (or a provider's `serves`) can declare its pair credential **accepted-only**. The plan then
|
||||
refuses the pair — naming the accept command — instead of minting, and a consumer is never handed a
|
||||
value the mesh knows cannot work.
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-09-30
|
||||
located-in:
|
||||
- mesh-controller internal/inventory/secrets.go (AcceptSecretForPair is per consumer)
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 165 — One accepted value must be accepted once per consumer
|
||||
|
||||
## What was observed
|
||||
|
||||
On ace, jackett's API key is the pair credential for sonarr, radarr, lidarr and bookshelf; sonarr's is
|
||||
the credential for ombi, bazarr and home-assistant. It is **one value**, owned by the provider — yet
|
||||
`secret accept` is per pair, so ace's download stack alone needs 12 accepts of 3 values, and rotating
|
||||
a provider's key means finding and re-accepting every pair. Missing one leaves that consumer on a
|
||||
stale (or minted, 164) value.
|
||||
|
||||
## What would be right
|
||||
|
||||
A provider-level accept: "this provider's credential for `<provision>` is X" — delivered to every
|
||||
consumer pair, current and future, and rotated in one place. Pairs whose credential is genuinely per
|
||||
consumer (postgres, keycloak, mosquitto, influxdb — minted and created by a provisioner) are unaffected.
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-09-30
|
||||
located-in:
|
||||
- mesh-controller internal/catalogue (requires is a list of hard requirements)
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 166 — A requirement cannot be optional
|
||||
|
||||
## What was observed
|
||||
|
||||
Making every dependency on ace a provision turned soft dependencies into hard ones. grafana now
|
||||
requires `influxdb-api` (a data source), ombi requires `sonarr-api`, `radarr-api` and `lidarr-api`,
|
||||
home-assistant requires the Servarr APIs and `mqtt-topic`. Each is optional to the software — grafana
|
||||
runs without a data source, ombi without lidarr — but a mesh without influxdb cannot assign grafana at
|
||||
all, and a mesh without lidarr cannot run ombi.
|
||||
|
||||
## What would be right
|
||||
|
||||
A requirement a module can run without: resolved and bound when a provider exists, absent (with its
|
||||
`${bound:…}` placeholders refused or defaulted explicitly, never rendered empty) when none does — so
|
||||
the module description stays true on every mesh.
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-09-30
|
||||
located-in:
|
||||
- mesh-catalog (each module builds from its own directory, ADR 0069)
|
||||
- mesh-sdk
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 167 — Code several modules share has no home
|
||||
|
||||
## What was observed
|
||||
|
||||
The download-stack write-in step (register download clients and torznab indexers through the Servarr
|
||||
API) is identical for sonarr, radarr, lidarr and bookshelf. Because a module builds from its own
|
||||
directory, it now exists as four byte-identical copies under `modules/<m>/downloads/`, kept honest by a
|
||||
test that fails when one differs. The same shape repeats: an MQTT probe copied into two modules, and a
|
||||
"write the provider into the app through its API, idempotently, refuse a minted value" step in ombi,
|
||||
home-assistant, nodered, tautulli and the four downloaders.
|
||||
|
||||
## What would be right
|
||||
|
||||
A home for shared module code the builder can use — an sdk helper (a write-in step harness: read
|
||||
bindings and pair credentials, probe the provider, diff, write, report) or a shared package the
|
||||
catalogue builds once — so a fix lands in one place.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-09-30
|
||||
located-in:
|
||||
- mesh-controller internal/catalogue/settings.go (settle: every key but `ports` merges into every mergeable file and every contribution)
|
||||
- mesh-controller internal/catalogue/declaration.go (a provider's settings are laid over what it serves)
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 168 — A setting reaches every file and every contribution
|
||||
|
||||
## What was observed
|
||||
|
||||
Settings merge key by key into **every** `"merge": "json"` file of a module **and** every contribution
|
||||
it makes; a provider's settings are also laid over what it serves. Seen on ace:
|
||||
|
||||
- searxng's `endpoints` and a route `label` land in searxng's own `settings.yml`; nodered's
|
||||
`timeZone` and `mqtt` keys land in mosquitto's grants file; keycloak's `issuer` lands in its
|
||||
`postgres-database` and `route` contributions.
|
||||
- every consumer's `plex-api` binding carries plex's `endpoints` and `expose` settings — and a provider
|
||||
setting named `port` would silently redirect every consumer.
|
||||
- a module cannot have two configurable files: searxng's sidecar config had to stop being mergeable
|
||||
so searxng's keys would not reach it.
|
||||
|
||||
Harmless today only because every receiver happens to ignore unknown keys.
|
||||
|
||||
## What would be right
|
||||
|
||||
A setting is aimed: at a file (by resource id), at a contribution (by requirement), or at what the
|
||||
module serves — declared settable by the module (ADR 0046 already says settings drive "the fields the
|
||||
manifest marks") — and an unaimed key is refused like any unknown setting.
|
||||
@@ -1,78 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -87,15 +87,12 @@ 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 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.
|
||||
**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.
|
||||
|
||||
Answered separately, a repository of its own is the better home:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user