Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af6a397840 | ||
|
|
748f95d417 |
@@ -54,6 +54,4 @@ whose failure has never been observed is a guess about its own correctness.
|
|||||||
The development cycle, checked ([ADR 0080](../../02-DECISIONS/0080-the-development-cycle-is-checked.md)):
|
The development cycle, checked ([ADR 0080](../../02-DECISIONS/0080-the-development-cycle-is-checked.md)):
|
||||||
a to-be design names a decision, an in-progress/implemented design names its owning code, a
|
a to-be design names a decision, an in-progress/implemented design names its owning code, a
|
||||||
located/fixed issue names its owner, a fixed/resolved issue says what fixed it, a graduated
|
located/fixed issue names its owner, a fixed/resolved issue says what fixed it, a graduated
|
||||||
research overview says what it became, and no two issue records share a number (issue 155 — the
|
research overview says what it became. `python3 00-META/checks/cycle.py`
|
||||||
number is how a record is cited, and `main` lags every open pull request, so two people reading it
|
|
||||||
allocate the same one). `python3 00-META/checks/cycle.py`
|
|
||||||
|
|||||||
+1
-23
@@ -14,8 +14,7 @@ What is enforced:
|
|||||||
its owning code (`code:`) -- no development without a design that says where.
|
its owning code (`code:`) -- no development without a design that says where.
|
||||||
issues a known `status:`; once `located`, `located-in:` names the owner;
|
issues a known `status:`; once `located`, `located-in:` names the owner;
|
||||||
once `resolved`, `fixed-by:` says what fixed it (prose counts --
|
once `resolved`, `fixed-by:` says what fixed it (prose counts --
|
||||||
"nothing, the capability existed" is an answer). And no two records share a
|
"nothing, the capability existed" is an answer).
|
||||||
number -- the number is how a record is cited.
|
|
||||||
research a known `status:`; a `graduated` overview says what it `became:`, and every
|
research a known `status:`; a `graduated` overview says what it `became:`, and every
|
||||||
target it names exists.
|
target it names exists.
|
||||||
decisions every accepted record is REACHABLE from the cycle: cited by a design doc's
|
decisions every accepted record is REACHABLE from the cycle: cited by a design doc's
|
||||||
@@ -110,27 +109,6 @@ def main():
|
|||||||
"without a design that says where" % status)
|
"without a design that says where" % status)
|
||||||
|
|
||||||
# ---- issues ------------------------------------------------------------------------
|
# ---- issues ------------------------------------------------------------------------
|
||||||
# Two records may not share a number. Numbers are taken as "next free after main", and work
|
|
||||||
# sits on unmerged branches for days -- so two people reading the same main allocate the same
|
|
||||||
# number, and nothing said so. It happened twice in one evening between two machines, and the
|
|
||||||
# second collision landed on main with all three checks passing (issue 155). An issue number is
|
|
||||||
# how every other record cites this one; two records answering to it means a pointer that
|
|
||||||
# resolves to whichever the reader happened to open.
|
|
||||||
seen = {}
|
|
||||||
for folder in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", ""))):
|
|
||||||
name = os.path.basename(os.path.normpath(folder))
|
|
||||||
number = name.split("-", 1)[0]
|
|
||||||
if not number.isdigit():
|
|
||||||
continue
|
|
||||||
if number in seen:
|
|
||||||
bad(os.path.join("04-ISSUES", name),
|
|
||||||
"is numbered %s, and so is %s -- an issue number is how it is cited, and two "
|
|
||||||
"records answering to one means a citation that resolves to whichever the reader "
|
|
||||||
"opened. Take the next free number across main AND every open pull request"
|
|
||||||
% (number, seen[number]))
|
|
||||||
else:
|
|
||||||
seen[number] = name
|
|
||||||
|
|
||||||
for path in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md"))):
|
for path in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md"))):
|
||||||
front = frontmatter(path)
|
front = frontmatter(path)
|
||||||
if front is None:
|
if front is None:
|
||||||
|
|||||||
@@ -164,12 +164,6 @@ def check_rests_on(failures, records):
|
|||||||
# decision is exactly what as-is is for."
|
# decision is exactly what as-is is for."
|
||||||
if rel(path).startswith("03-DESIGN/00-as-is/"):
|
if rel(path).startswith("03-DESIGN/00-as-is/"):
|
||||||
continue
|
continue
|
||||||
# A withdrawn record's citations are history. It instructs nobody -- every reader
|
|
||||||
# is sent to its superseder -- so what it was built on may itself be withdrawn.
|
|
||||||
# Refusing that would mean rewriting the lineage of a record whose reasoning is
|
|
||||||
# the thing the immutability rule protects.
|
|
||||||
if frontmatter(read(path)).get("status") == "superseded":
|
|
||||||
continue
|
|
||||||
# An extension that supersedes legitimately names what it replaced.
|
# An extension that supersedes legitimately names what it replaced.
|
||||||
this = ADR_FILE.match(os.path.basename(path))
|
this = ADR_FILE.match(os.path.basename(path))
|
||||||
supersedes = records[number]["front"].get("superseded-by", "")
|
supersedes = records[number]["front"].get("superseded-by", "")
|
||||||
@@ -247,20 +241,13 @@ def check_supersession_symmetry(failures, records):
|
|||||||
failures.add("supersession", rel(record["path"]), f"superseder does not exist: {by}")
|
failures.add("supersession", rel(record["path"]), f"superseder does not exist: {by}")
|
||||||
continue
|
continue
|
||||||
other = records[match.group(1)]
|
other = records[match.group(1)]
|
||||||
# `supersedes:` may name one record or several. One decision replacing two is a real
|
claims = os.path.basename(str(other["front"].get("supersedes", "")))
|
||||||
# situation -- two records that built and refined the same wrong mechanism are withdrawn
|
if claims != record["name"]:
|
||||||
# by the one record that removes it -- and a check that allows only one would force
|
|
||||||
# either a chain of pro-forma records or an unmarked supersession.
|
|
||||||
claimed = other["front"].get("supersedes", "")
|
|
||||||
if isinstance(claimed, str):
|
|
||||||
claimed = [claimed] if claimed else []
|
|
||||||
claims = [os.path.basename(str(entry)) for entry in claimed]
|
|
||||||
if record["name"] not in claims:
|
|
||||||
failures.add(
|
failures.add(
|
||||||
"supersession",
|
"supersession",
|
||||||
rel(other["path"]),
|
rel(other["path"]),
|
||||||
f"ADR {number} says this supersedes it; this record does not say so "
|
f"ADR {number} says this supersedes it; this record does not say so "
|
||||||
f"(supersedes: {', '.join(claims) or 'absent'})",
|
f"(supersedes: {claims or 'absent'})",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+3
-18
@@ -40,7 +40,7 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
|
|||||||
- **the deprecated broker** — the lavinmq module. It was the mesh's bus and is not any more. It
|
- **the deprecated broker** — the lavinmq module. It was the mesh's bus and is not any more. It
|
||||||
keeps running as an **ordinary provider** of the `amqp` provision, for modules that need a
|
keeps running as an **ordinary provider** of the `amqp` provision, for modules that need a
|
||||||
message broker of their own the way something needs a database
|
message broker of their own the way something needs a database
|
||||||
([ADR 0127](../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md))) — no seat, not foundation,
|
([ADR 0127](../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md)) — no seat, not foundation,
|
||||||
never raised at genesis, and a mesh that never installs it is complete.
|
never raised at genesis, and a mesh that never installs it is complete.
|
||||||
|
|
||||||
Say *the deprecated broker*, not "the compatibility broker" (it serves the mesh's own modules,
|
Say *the deprecated broker*, not "the compatibility broker" (it serves the mesh's own modules,
|
||||||
@@ -52,11 +52,8 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
|
|||||||
|
|
||||||
- **package** — what code resolves when it is **compiled**: an npm/cargo/pypi dependency, by
|
- **package** — what code resolves when it is **compiled**: an npm/cargo/pypi dependency, by
|
||||||
**version**. Served by the **package-registry** (gitea). Only a builder talks to it.
|
**version**. Served by the **package-registry** (gitea). Only a builder talks to it.
|
||||||
- **artifact** — anything a build produces and the mesh delivers to a machine by **digest**: an
|
- **artifact** — what the mesh delivers to a machine to **install and run**: an OCI image, by
|
||||||
`image`, a mirrored `upstream` image, a `bundle` of the module's own code, an `archive`. Served by
|
**digest**. Served by the **artifact-store** (distribution). Every node pulls from it.
|
||||||
the **artifact-store**, an OCI registry that holds every kind as content-addressed blobs
|
|
||||||
([ADR 0156](../02-DECISIONS/0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)).
|
|
||||||
Every node pulls from it. An image is one kind of artifact, and a module is not an image.
|
|
||||||
- These are two protocols, not one store being weak — see [ADR 0075](../02-DECISIONS/0075-two-stores-and-which-provides-what.md).
|
- These are two protocols, not one store being weak — see [ADR 0075](../02-DECISIONS/0075-two-stores-and-which-provides-what.md).
|
||||||
|
|
||||||
## How modules relate to the mesh
|
## How modules relate to the mesh
|
||||||
@@ -78,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
|
is a role you occupy, and the two meet where a seat delivers a provision: occupying the seat is
|
||||||
what makes a module *the* provider of it.
|
what makes a module *the* provider of it.
|
||||||
|
|
||||||
## The surfaces
|
|
||||||
|
|
||||||
- **console** — the module (`mesh-console`) that puts the mesh's tools in front of whoever is on a
|
|
||||||
machine: an MCP endpoint on the machine's loopback for an agent, the same endpoint for a person. It
|
|
||||||
is assigned like any module, holds a credential the mesh minted, and calls tools under a grant its
|
|
||||||
manifest declares (`invokes`). Loopback is the authority boundary: whoever is on the machine owns the
|
|
||||||
mesh there ([ADR 0152](../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)).
|
|
||||||
Not "the tool bridge", "the brain" or "the MCP server" — those name the predecessor's program or a
|
|
||||||
protocol, and the console is a module.
|
|
||||||
- **invokes** — the manifest word for the tools a module calls, `<module>.<tool>` each or `*` for
|
|
||||||
every one. A grant on the publish side and nothing else; a module that declares none calls nothing.
|
|
||||||
|
|
||||||
## How this page is kept
|
## How this page is kept
|
||||||
|
|
||||||
A new name for an existing thing lands here first, in the same change that introduces it in code. A
|
A new name for an existing thing lands here first, in the same change that introduces it in code. A
|
||||||
|
|||||||
@@ -21,12 +21,7 @@ incident someone must **clear**.
|
|||||||
|
|
||||||
## Steps
|
## Steps
|
||||||
|
|
||||||
1. Take the next free number — **across `main` and every open pull request**, not `main` alone.
|
1. Take the next free number. Create `04-ISSUES/NNN-short-name/00-report.md`:
|
||||||
Work sits on unmerged branches for days, so two people both reading `main` allocate the same
|
|
||||||
number; it happened twice in one hour between two machines, and the second collision reached
|
|
||||||
`main` with every check passing (issue 155). `cycle.py` now refuses two records sharing a number,
|
|
||||||
which catches a collision but does not prevent one. Create
|
|
||||||
`04-ISSUES/NNN-short-name/00-report.md`:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
---
|
---
|
||||||
@@ -47,14 +42,6 @@ incident someone must **clear**.
|
|||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
- Closed issues are never deleted — they are the mesh's symptom-to-component memory.
|
- Closed issues are never deleted — they are the mesh's symptom-to-component memory.
|
||||||
- `fixed-by:` names something that will still exist: a commit or a pull request, never a branch. A
|
|
||||||
branch is deleted when it merges, so a branch name there is a pointer that resolves to nothing by
|
|
||||||
the time anybody follows it.
|
|
||||||
- A fix that turns out to have broken something else is written back into the record that asked for
|
|
||||||
it, pointing at the new issue. Somebody arriving at a record to learn why the code is the way it
|
|
||||||
is must not have to already know there was a sequel.
|
|
||||||
- Renumbering a collision happens once, in the branch that lands last. Renumbering a branch whose
|
|
||||||
author is still pushing only moves the race.
|
|
||||||
- An issue whose answer is a general lesson should also be written to the knowledge base, so
|
- An issue whose answer is a general lesson should also be written to the knowledge base, so
|
||||||
the next person searching a symptom finds it. Both, not either.
|
the next person searching a symptom finds it. Both, not either.
|
||||||
- `status: wontfix` is legitimate and requires a sentence saying why.
|
- `status: wontfix` is legitimate and requires a sentence saying why.
|
||||||
|
|||||||
@@ -13,14 +13,6 @@ decisions taken over three days; the reasoning is kept, the fragmentation is not
|
|||||||
|
|
||||||
The environment a change is run against before it reaches real machines.
|
The environment a change is run against before it reaches real machines.
|
||||||
|
|
||||||
> **Still the lab, no longer the test bed — 2026-09-30, by [ADR 0149](0149-the-live-mesh-is-the-test-bed.md).**
|
|
||||||
> Everything here stands. What changed is what the lab is *for*: a change is verified against the mesh
|
|
||||||
> that is running, because the faults that cost the most are faults of a mesh that already exists —
|
|
||||||
> bound consumers, containers made against an older roster, an adopted machine — and a bed is by
|
|
||||||
> construction a mesh that does not. Raising a mesh from bare is now the lab's whole job, which is the
|
|
||||||
> one thing the live mesh cannot be asked to do. 0149 also supersedes
|
|
||||||
> [ADR 0068](0068-the-lab-takes-requests.md), which extended this one and was never built.
|
|
||||||
|
|
||||||
## A node in the lab is a virtual machine
|
## A node in the lab is a virtual machine
|
||||||
|
|
||||||
It boots a stock Linux image, runs the real install, and becomes a node. **It is not a model of
|
It boots a stock Linux image, runs the real install, and becomes a node. **It is not a model of
|
||||||
|
|||||||
@@ -72,12 +72,6 @@ answers from it. Nothing flows back: this repository is public, the mesh is not,
|
|||||||
would be how installation-specific detail arrives into documents that must not carry it
|
would be how installation-specific detail arrives into documents that must not carry it
|
||||||
([`README.md`](../README.md)).
|
([`README.md`](../README.md)).
|
||||||
|
|
||||||
> **The mechanism changed — 2026-09-30, by ADR 0153.** What stands: read where it is written, no copy,
|
|
||||||
> one-way. What moved: the reader is a module the mesh assigns (`records`, a checkout at a commit every
|
|
||||||
> answer names) rather than the agent session of design 15, which is not built; and "the search consults
|
|
||||||
> the agent" has no store to consult since the cut-over — the console's tool list is where the record
|
|
||||||
> appears beside everything else. [ADR 0153](0153-the-record-is-read-by-a-module-and-the-console-lists-it.md).
|
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
**This repository stops being a fourth knowledge system, properly.** The original objection was
|
**This repository stops being a fourth knowledge system, properly.** The original objection was
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
topic: building it
|
topic: building it
|
||||||
status: accepted
|
status: proposed
|
||||||
date: 2026-09-01
|
date: 2026-09-01
|
||||||
deciders: jochen
|
deciders: jochen
|
||||||
reconstructed: false
|
reconstructed: false
|
||||||
@@ -101,19 +101,3 @@ the digest down after building.
|
|||||||
**Whether kind 4 deserves a module at all.** Thirty-five descriptions that say *install this and
|
**Whether kind 4 deserves a module at all.** Thirty-five descriptions that say *install this and
|
||||||
write these files* may be better as one module with settings than as thirty-five modules. Left
|
write these files* may be better as one module with settings than as thirty-five modules. Left
|
||||||
open deliberately; it is a question about the shape of the catalogue, not about whether to have one.
|
open deliberately; it is a question about the shape of the catalogue, not about whether to have one.
|
||||||
|
|
||||||
## Accepted, 2026-09-29, against what was built
|
|
||||||
|
|
||||||
*Marked in a grooming pass: the mesh was built to this record and the record still said `proposed`.*
|
|
||||||
|
|
||||||
The proposal is the arrangement that exists. `mesh-catalog` holds descriptions of software we did
|
|
||||||
not write and the programs that provision it, and holds neither the mesh's own components nor an
|
|
||||||
application's own module. The mesh's list of modules is a table in the control plane, filled by
|
|
||||||
`module add`, and every module records the source it came from with the commit it was read at.
|
|
||||||
|
|
||||||
**One half is not built: `module check` as a command on the control plane's binary.** A manifest is
|
|
||||||
still validated by a test that reaches into the control plane's internals — which works for this
|
|
||||||
catalogue and gives nothing at all to somebody describing their own application in their own
|
|
||||||
repository, which this record says is the case that matters most. That is
|
|
||||||
[issue 148](../04-ISSUES/148-a-manifest-outside-this-catalogue-has-no-check/00-report.md).
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,14 +26,6 @@ runtime is per-module, not per-node, and treating the audit-logger as special le
|
|||||||
modules' code with nothing to run it: the conversion produced tools and events that, as it stands,
|
modules' code with nothing to run it: the conversion produced tools and events that, as it stands,
|
||||||
never execute.
|
never execute.
|
||||||
|
|
||||||
> **The hosting form is settled elsewhere — 2026-09-30.** Where this record says "a container", read
|
|
||||||
> [ADR 0150](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md): a module's own
|
|
||||||
> code runs as supervised processes under this record's one account. Nothing else here changes — the
|
|
||||||
> per-module runtime, the per-tool key and the single scoped account are the argument this record made
|
|
||||||
> and they are why 0150 goes the way it does. The note is here because two design documents chose the
|
|
||||||
> other form without knowing this record existed
|
|
||||||
> ([issue 117](../04-ISSUES/117-a-modules-own-code-is-a-container-and-a-process/00-report.md)).
|
|
||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
### A module with tools or events runs a process of its own
|
### A module with tools or events runs a process of its own
|
||||||
|
|||||||
@@ -80,15 +80,6 @@ reaching the routed name, which the clause above has just made resolvable inside
|
|||||||
three are one decision: **compose the name, propagate it, certify it** — each is meaningless without
|
three are one decision: **compose the name, propagate it, certify it** — each is meaningless without
|
||||||
the one before it.
|
the one before it.
|
||||||
|
|
||||||
> **The mechanism changed — 2026-09-30, by [ADR 0148](0148-the-meshs-names-are-resolved-not-copied-into-containers.md).**
|
|
||||||
> A routed name still reaches every asker in the mesh, which is what this record decided and it stands.
|
|
||||||
> It no longer reaches them by being written into each declared container: copying the roster in made the
|
|
||||||
> roster part of every container's identity, so one name moving replaced every container in the mesh
|
|
||||||
> ([issue 151](../04-ISSUES/151-a-new-name-recreates-every-container-in-the-mesh/00-report.md)). A
|
|
||||||
> container resolves through its machine's resolver instead. The consequence below — that an internal
|
|
||||||
> issuer's challenge needs the routed name resolvable inside the mesh — holds unchanged, by the means the
|
|
||||||
> machine itself already uses.
|
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
- **Lab-versus-production is one node-level `public-domain` setting**, not an override on every
|
- **Lab-versus-production is one node-level `public-domain` setting**, not an override on every
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
---
|
---
|
||||||
topic: building it
|
topic: building it
|
||||||
status: superseded
|
status: proposed
|
||||||
date: 2026-09-12
|
date: 2026-09-12
|
||||||
deciders: jochen
|
deciders: jochen
|
||||||
reconstructed: false
|
reconstructed: false
|
||||||
extends: 0016-the-lab.md
|
extends: 0016-the-lab.md
|
||||||
superseded-by: 02-DECISIONS/0149-the-live-mesh-is-the-test-bed.md
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 68. The lab takes requests, one at a time, and runs each from its own copy
|
# 68. The lab takes requests, one at a time, and runs each from its own copy
|
||||||
|
|
||||||
> **Superseded — 2026-09-30, by [ADR 0149](0149-the-live-mesh-is-the-test-bed.md).** Never built. The
|
|
||||||
> live mesh became the test bed, because the faults that cost the most are faults of a mesh that
|
|
||||||
> already exists — bound consumers, containers made against an older roster, an adopted machine — and a
|
|
||||||
> bed is by construction a mesh that does not. The rule worth keeping from below is that a run reads a
|
|
||||||
> copy that is not anybody's working tree.
|
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
**The lab is exclusive hardware, and today a person holds it.** Raising a scenario takes over
|
**The lab is exclusive hardware, and today a person holds it.** Raising a scenario takes over
|
||||||
|
|||||||
@@ -35,16 +35,6 @@ The development cycle is enforced mechanically, to the extent frontmatter can ca
|
|||||||
capability existed" is an answer).
|
capability existed" is an answer).
|
||||||
- **No silent graduation** — a `graduated` research overview says what it `became:`, and the
|
- **No silent graduation** — a `graduated` research overview says what it `became:`, and the
|
||||||
targets exist.
|
targets exist.
|
||||||
- **No two records answering to one number** — added 2026-09-30; see the insight below.
|
|
||||||
|
|
||||||
> **Progressive insight — 2026-09-30.** The list above named four things `cycle.py` enforces, and
|
|
||||||
> now names five. Nothing enforced that two issue records hold different numbers: two machines
|
|
||||||
> filing issues within one hour both read `main`, both took "the next free number", and collided
|
|
||||||
> twice — the second collision reaching `main` with `records.py`, `cycle.py` and `index.py` all
|
|
||||||
> reporting success (04-ISSUES/155). A number is how every other record cites one, so two records
|
|
||||||
> answering to it is a citation that resolves to whichever folder the reader opened. `cycle.py`
|
|
||||||
> refuses it now. The decision here stands exactly as written: this is one more thing frontmatter
|
|
||||||
> and file names can carry, found by its absence rather than by reasoning.
|
|
||||||
|
|
||||||
[`00-META/checks/cycle.py`](../00-META/checks/cycle.py) refuses violations, beside `records.py`
|
[`00-META/checks/cycle.py`](../00-META/checks/cycle.py) refuses violations, beside `records.py`
|
||||||
and `index.py`; all three run before any merge here. What frontmatter cannot see — that code work
|
and `index.py`; all three run before any merge here. What frontmatter cannot see — that code work
|
||||||
|
|||||||
@@ -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
|
harder: nothing outside the control plane can, and the control plane's connection is one more
|
||||||
thing on the path of every question — a cost accepted for the audit it buys.
|
thing on the path of every question — a cost accepted for the audit it buys.
|
||||||
|
|
||||||
> **The mechanism changed — 2026-09-30, by ADR 0152.** What stands: `ask` on the control plane, and
|
|
||||||
> that every call passes an account whose permission list says what it may ask. What moved: "nothing
|
|
||||||
> outside the control plane can" stopped being true when a person's account gained a publish grant per
|
|
||||||
> tool (design 25 §7, 2026-09-28), and [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)
|
|
||||||
> takes the first option above for a module as well — a manifest declares `invokes`, and the bus grants
|
|
||||||
> exactly that publish side. The audit the second option bought is the bus's permission list, which
|
|
||||||
> derives both.
|
|
||||||
|
|
||||||
## How it is checked
|
## How it is checked
|
||||||
|
|
||||||
A tools-only bed asks a served tool through the control plane and asserts an answer arrived —
|
A tools-only bed asks a served tool through the control plane and asserts an answer arrived —
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
topic: what runs on it
|
topic: what runs on it
|
||||||
status: accepted
|
status: proposed
|
||||||
date: 2026-09-25
|
date: 2026-09-25
|
||||||
deciders: jochen
|
deciders: jochen
|
||||||
reconstructed: false
|
reconstructed: false
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
topic: what runs on it
|
topic: what runs on it
|
||||||
status: accepted
|
status: proposed
|
||||||
date: 2026-09-25
|
date: 2026-09-25
|
||||||
deciders: jochen
|
deciders: jochen
|
||||||
reconstructed: false
|
reconstructed: false
|
||||||
@@ -275,11 +275,3 @@ On acceptance, each of these is superseded or amended by this record, not edited
|
|||||||
everything a module needs is a requirement
|
everything a module needs is a requirement
|
||||||
- [Issue 095](../04-ISSUES/095-a-module-assigned-after-genesis-has-no-broker-account/00-report.md),
|
- [Issue 095](../04-ISSUES/095-a-module-assigned-after-genesis-has-no-broker-account/00-report.md),
|
||||||
[issue 103](../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md): what fails today
|
[issue 103](../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md): what fails today
|
||||||
|
|
||||||
## Accepted, 2026-09-29, against what was built
|
|
||||||
|
|
||||||
*Marked in a grooming pass.* The vault is a module providing `secret` at mesh scope, and six
|
|
||||||
modules in the catalogue require it — so a shared secret is a requirement answered by the vault,
|
|
||||||
which is what this record asks for. Private keys are still made where they are used and never
|
|
||||||
travel, which is the other half and was never in question.
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
topic: what runs on it
|
topic: what runs on it
|
||||||
status: accepted
|
status: proposed
|
||||||
date: 2026-09-26
|
date: 2026-09-26
|
||||||
deciders: jochen
|
deciders: jochen
|
||||||
reconstructed: false
|
reconstructed: false
|
||||||
@@ -189,23 +189,6 @@ On acceptance, each of these is amended by this record, not edited:
|
|||||||
credential is no longer all-or-nothing with a window. It overlaps, with each step confirmed.
|
credential is no longer all-or-nothing with a window. It overlaps, with each step confirmed.
|
||||||
A single-party credential is staged, not replaced.
|
A single-party credential is staged, not replaced.
|
||||||
|
|
||||||
## Accepted, 2026-09-30, and not scheduled
|
|
||||||
|
|
||||||
Accepted as written. The separation it draws — a consumer's *resource* and a *credential that reaches
|
|
||||||
it* are different things with different lifecycles — is the part that had to be settled, because the
|
|
||||||
alternative is what the record was written against: retiring a credential taking the data it reached
|
|
||||||
with it. That is a data-loss shape, and a record that names it should not sit unresolved while the
|
|
||||||
code that could hit it is being written.
|
|
||||||
|
|
||||||
**It is not built, and accepting it does not schedule it.** The SDK's provisioner adapter is still
|
|
||||||
`create` / `remove` / `holds` rather than the four operations above, and no provider implements the
|
|
||||||
two-credential rotation. Accepted-and-not-built is an ordinary state here — 0141 and 0142 are both in
|
|
||||||
it — and it is the honest one: leaving this `proposed` made it invisible to anyone reading what the
|
|
||||||
mesh has decided, while changing nothing about what runs.
|
|
||||||
|
|
||||||
The work it implies belongs with the provisioner contract, beside
|
|
||||||
[issue 124](../04-ISSUES/124-a-consumer-cannot-be-told-what-its-provider-derived/00-report.md).
|
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
- **Every credential provider's adapter changes**, in two steps. The first separates *retire a
|
- **Every credential provider's adapter changes**, in two steps. The first separates *retire a
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
topic: what runs on it
|
topic: what runs on it
|
||||||
status: accepted
|
status: proposed
|
||||||
date: 2026-09-26
|
date: 2026-09-26
|
||||||
deciders: jochen
|
deciders: jochen
|
||||||
extends: 0112-a-module-definition-names-no-node-mesh-or-path.md
|
extends: 0112-a-module-definition-names-no-node-mesh-or-path.md
|
||||||
@@ -47,10 +47,3 @@ other boundary already is: the module name.
|
|||||||
node runs one of each (ADR 0115)" — instead of failing on whichever name collides first.
|
node runs one of each (ADR 0115)" — instead of failing on whichever name collides first.
|
||||||
- Multi-tenant asks are answered in the catalogue (a second module definition), not in the
|
- Multi-tenant asks are answered in the catalogue (a second module definition), not in the
|
||||||
control plane.
|
control plane.
|
||||||
|
|
||||||
## Accepted, 2026-09-29, against what was built
|
|
||||||
|
|
||||||
*Marked in a grooming pass.* The rule is enforced where it cannot be forgotten: `assignment`'s
|
|
||||||
primary key is `(node, module)`, so a second assignment of one module to one machine is not a thing
|
|
||||||
the mesh can hold. The record read `proposed` while the schema had already settled it.
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ extends: 0102-the-mesh-writes-into-a-shared-file-never-over-it.md
|
|||||||
## Context
|
## Context
|
||||||
|
|
||||||
When a resource stops being declared — its module unassigned, the node sent a
|
When a resource stops being declared — its module unassigned, the node sent a
|
||||||
deliberately-empty declaration ([issue 149](../04-ISSUES/149-a-declaration-that-shrinks-to-empty-is-skipped-not-sent/00-report.md)),
|
deliberately-empty declaration ([issue 127](../04-ISSUES/127-a-declaration-that-shrinks-to-empty-is-skipped-not-sent/00-report.md)),
|
||||||
or a new catalogue version renaming its id — the host undoes it. The host's own code states
|
or a new catalogue version renaming its id — the host undoes it. The host's own code states
|
||||||
the rule it means to follow: **it removes what it made and leaves what it merely configured.**
|
the rule it means to follow: **it removes what it made and leaves what it merely configured.**
|
||||||
For almost every resource it does exactly that:
|
For almost every resource it does exactly that:
|
||||||
|
|||||||
+1
-3
@@ -81,9 +81,7 @@ closed set stays what its name says it is: the *system's* roles, not everyone's.
|
|||||||
- **`the-uplink` → `node-uplink`** ([ADR 0125](0117-a-machines-uplink-is-a-seat.md)). Unheld, so it
|
- **`the-uplink` → `node-uplink`** ([ADR 0125](0117-a-machines-uplink-is-a-seat.md)). Unheld, so it
|
||||||
renames with no migration.
|
renames with no migration.
|
||||||
- **The registry seats — `the-artifact-store`, `npm-package-registry` (→ `mesh-artifact-store`,
|
- **The registry seats — `the-artifact-store`, `npm-package-registry` (→ `mesh-artifact-store`,
|
||||||
`mesh-npm-package-registry`) — and `git` (→ `mesh-git`) — are decided but deferred.** *(The
|
`mesh-npm-package-registry`) — and `git` (→ `mesh-git`) — are decided but deferred.** They each
|
||||||
mechanism changed — 2026-09-30, by ADR 0156: `the-artifact-store` is renamed, one update and one
|
|
||||||
alias under ADR 0122; the other two stay deferred.)* They each
|
|
||||||
*deliver* a provision, so renaming them is a delivering-seat migration: a holder that stops
|
*deliver* a provision, so renaming them is a delivering-seat migration: a holder that stops
|
||||||
resolving mid-flight takes a provision away from every consumer. That risk is not worth carrying in
|
resolving mid-flight takes a provision away from every consumer. That risk is not worth carrying in
|
||||||
the same pass as the node-* renames, so they keep their names until done deliberately.
|
the same pass as the node-* renames, so they keep their names until done deliberately.
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
topic: the mesh
|
topic: the mesh
|
||||||
status: superseded
|
status: accepted
|
||||||
superseded-by: 0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
|
||||||
date: 2026-09-26
|
date: 2026-09-26
|
||||||
deciders: jochen
|
deciders: jochen
|
||||||
reconstructed: false
|
reconstructed: false
|
||||||
|
|||||||
@@ -4,18 +4,11 @@ status: accepted
|
|||||||
date: 2026-09-26
|
date: 2026-09-26
|
||||||
deciders: jochen
|
deciders: jochen
|
||||||
reconstructed: false
|
reconstructed: false
|
||||||
extends: 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
extends: 02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md
|
||||||
---
|
---
|
||||||
|
|
||||||
# 128. The mesh bus is required, not ambient
|
# 128. The mesh bus is required, not ambient
|
||||||
|
|
||||||
|
|
||||||
> **Pointer repointed, 2026-09-27.** This record was written extending
|
|
||||||
> [ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)), which
|
|
||||||
> [ADR 0131](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md) has since superseded — AMQP is
|
|
||||||
> not a provision at all. Nothing decided here changes; the frontmatter now rests on the live record,
|
|
||||||
> and the citations below are read with that in mind.
|
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
[Design 29](../03-DESIGN/01-to-be/32-what-a-module-declares.md) opened by saying the bus is
|
[Design 29](../03-DESIGN/01-to-be/32-what-a-module-declares.md) opened by saying the bus is
|
||||||
@@ -79,7 +72,7 @@ process in the path and nothing waiting on a bus account to create bus accounts.
|
|||||||
whose provider is the mesh itself.
|
whose provider is the mesh itself.
|
||||||
|
|
||||||
**A module may also provide a NATS server of its own, and that is a different interface.** Exactly
|
**A module may also provide a NATS server of its own, and that is a different interface.** Exactly
|
||||||
as the AMQP broker provides `amqp` ([ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md))), a module
|
as the AMQP broker provides `amqp` ([ADR 0127](0127-amqp-is-a-provision-not-the-bus.md)), a module
|
||||||
may run its own NATS and offer it as a backing service. That interface is **`nats`**; the mesh's
|
may run its own NATS and offer it as a backing service. That interface is **`nats`**; the mesh's
|
||||||
own bus is **`mesh-bus`**; the two are never the same name, because a manifest that said `nats`
|
own bus is **`mesh-bus`**; the two are never the same name, because a manifest that said `nats`
|
||||||
could mean either and the difference is the whole architecture. The rule from 0119 decides which
|
could mean either and the difference is the whole architecture. The rule from 0119 decides which
|
||||||
@@ -116,7 +109,7 @@ is legitimate: a private bus is a backing service, never a channel to another mo
|
|||||||
|
|
||||||
- [ADR 0125](0125-the-bus-is-the-only-broker.md) — superseded by 0119; its bootstrap argument is
|
- [ADR 0125](0125-the-bus-is-the-only-broker.md) — superseded by 0119; its bootstrap argument is
|
||||||
narrowed here to the case it supports.
|
narrowed here to the case it supports.
|
||||||
- [ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)) — a broker as a backing service; this
|
- [ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) — a broker as a backing service; this
|
||||||
applies the same shape to the mesh's own bus and separates the two names.
|
applies the same shape to the mesh's own bus and separates the two names.
|
||||||
- [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md) — authority from
|
- [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md) — authority from
|
||||||
declarations, which this leaves untouched.
|
declarations, which this leaves untouched.
|
||||||
|
|||||||
@@ -4,21 +4,14 @@ status: accepted
|
|||||||
date: 2026-09-27
|
date: 2026-09-27
|
||||||
deciders: jochen
|
deciders: jochen
|
||||||
reconstructed: false
|
reconstructed: false
|
||||||
extends: 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
extends: 02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md
|
||||||
---
|
---
|
||||||
|
|
||||||
# 130. The predecessor is ending, and its broker goes with it
|
# 130. The predecessor is ending, and its broker goes with it
|
||||||
|
|
||||||
|
|
||||||
> **Pointer repointed, 2026-09-27.** This record was written extending
|
|
||||||
> [ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)), which
|
|
||||||
> [ADR 0131](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md) has since superseded — AMQP is
|
|
||||||
> not a provision at all. Nothing decided here changes; the frontmatter now rests on the live record,
|
|
||||||
> and the citations below are read with that in mind.
|
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
[ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)) settled that the old broker is an ordinary
|
[ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) settled that the old broker is an ordinary
|
||||||
provider of the `amqp` provision rather than a compatibility module with an end date. It rejected
|
provider of the `amqp` provision rather than a compatibility module with an end date. It rejected
|
||||||
giving it a retirement condition, and said why: *"its clients are not only the predecessor's, so the
|
giving it a retirement condition, and said why: *"its clients are not only the predecessor's, so the
|
||||||
retirement condition describes a day that will not come."*
|
retirement condition describes a day that will not come."*
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
---
|
|
||||||
topic: the mesh
|
|
||||||
status: accepted
|
|
||||||
date: 2026-09-27
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
supersedes: 0127-amqp-is-a-provision-not-the-bus.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 131. Everything on the mesh speaks to the broker seat, and AMQP is not a provision
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
[ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) settled the old broker as an ordinary provider
|
|
||||||
of an ordinary provision, `amqp`, kept for whatever wanted a message broker of its own. The day the
|
|
||||||
bus moved was the day that framing was tested, and it failed in a way that took the control plane
|
|
||||||
down for an evening.
|
|
||||||
|
|
||||||
Three things came out of the wreckage. **The protocol had leaked into the seat's contract**: for a
|
|
||||||
module to hold `mesh-broker`, it had to provide what the seat delivers, and what it delivered was
|
|
||||||
`amqp` — so the module that will carry the bus on NATS could not hold the seat that names the bus,
|
|
||||||
while the module the mesh was leaving could. **A consumer of `amqp` is not asking for AMQP.** The two
|
|
||||||
modules requiring it wanted the mesh's messaging — to emit an event, to hear a topic — and named the
|
|
||||||
wire protocol only because that was the word available. **And AMQP and NATS are not interchangeable
|
|
||||||
at the wire.** A provision named after a protocol can only ever be answered by that protocol, so once
|
|
||||||
the bus is NATS an `amqp` provision has one possible provider, and it is the thing being retired.
|
|
||||||
|
|
||||||
The operator's position, stated during the outage: modules depend on the broker *seat*, not on a
|
|
||||||
protocol; AMQP is obsolete as anything the mesh's core knows about; a module that depends on `amqp`
|
|
||||||
is wrong; and everything should reach the mesh's bus and be able to emit events and consume topics
|
|
||||||
through it.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**A module that needs messaging uses the mesh's bus, and the mesh's bus is whatever holds
|
|
||||||
`mesh-broker`.** Emitting an event and consuming a topic go through the sdk, which is handed the
|
|
||||||
bus by the mesh with the module's own credential. No manifest names a wire protocol to get it.
|
|
||||||
|
|
||||||
**`amqp` is neither a provision nor a requirement.** Registration refuses a manifest that provides
|
|
||||||
it or requires it. The `mesh-broker` seat delivers `mesh-bus`, and its holder is the module that
|
|
||||||
provides `mesh-bus` — today the nats module, and only it.
|
|
||||||
|
|
||||||
**The old broker's module and the two modules that required it leave the catalogue.** They are
|
|
||||||
removed, not converted: one was a proof that a grant worked end to end, the other forwards mail off a
|
|
||||||
queue, and both are re-done against the bus if wanted, as new modules under this record.
|
|
||||||
|
|
||||||
**The controller's AMQP transport is deleted once every node reports on the new bus**, and the
|
|
||||||
switch that selects a transport goes with it — one bus, so nothing to select.
|
|
||||||
|
|
||||||
The predecessor's own broker is outside the mesh and not this record's concern
|
|
||||||
([ADR 0130](0130-the-predecessor-is-ending-and-its-broker-goes-with-it.md)): what the predecessor's
|
|
||||||
tooling loses when it stops is accepted there.
|
|
||||||
|
|
||||||
## Options considered
|
|
||||||
|
|
||||||
1. **Keep 0127: AMQP stays an ordinary provision with the old broker as its provider.** Rejected. It
|
|
||||||
is what put the protocol into the seat's contract, it is why the seat could be left with no valid
|
|
||||||
holder mid-change, and it keeps two transports in the control plane indefinitely for the benefit of
|
|
||||||
two modules that did not want AMQP in the first place.
|
|
||||||
2. **Bridge it: the old broker's module also provides `mesh-bus`, so both can hold the seat during the
|
|
||||||
change.** Rejected. It makes the retiring broker a legitimate mesh bus for exactly as long as
|
|
||||||
nobody removes the line, which in practice is forever, and it leaves `amqp` as a thing the core
|
|
||||||
still knows the name of.
|
|
||||||
3. **The seat is the dependency; the protocol is nobody's business but the holder's.** Adopted.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- **The change of holder is a handover, and it needs a command.** Nothing today moves a seat from
|
|
||||||
one assignment to another as one act, and a seat the control plane dereferences cannot be empty
|
|
||||||
in between — that emptiness is the outage this record comes from. The command takes a seat and the
|
|
||||||
assignment taking it over. Designed and built before the cutover, under
|
|
||||||
[28 — Building the bus](../03-DESIGN/01-to-be/28-building-the-bus.md).
|
|
||||||
- **The seat's row moves to `mesh-bus` before the new holder registers, and that is safe.** The
|
|
||||||
control plane composes its own bus address through the seat *by name*
|
|
||||||
(`${seat:mesh-broker:…}`), and the overview derives holders by name; only registration and the
|
|
||||||
provision-to-seat resolution read what a seat delivers. So the row can change under the current
|
|
||||||
holder without unseating it, the new holder can then register its claim, and the handover happens
|
|
||||||
when both are running. Verified in the code during the outage, not assumed.
|
|
||||||
- **Registration gains two refusals**: a manifest providing `amqp`, and one requiring it.
|
|
||||||
- **The `rollout check` stops saying the old broker stays.** It said so under 0127; it now lists
|
|
||||||
unassigning it as the last step of the move.
|
|
||||||
- **What got harder**: a third party that genuinely wants an AMQP broker on a mesh node runs one as
|
|
||||||
any application module, with no provision and no seat, and nothing on the mesh routes to it. That
|
|
||||||
is the cost of the mesh not knowing the word.
|
|
||||||
|
|
||||||
## How this is checked
|
|
||||||
|
|
||||||
| Rule | Checked by |
|
|
||||||
|---|---|
|
|
||||||
| No manifest provides or requires `amqp` | a registration test refusing each, naming this record; and a whole-catalogue test asserting no registered manifest names it |
|
|
||||||
| `mesh-broker` delivers `mesh-bus`, and only a `mesh-bus` provider may hold it | the existing registration test for a delivering seat, with the row's value read from the store (mesh-controller#89) |
|
|
||||||
| The seat's row can change without unseating the holder | a test composing the control plane's own address and the overview under a row that the current holder does not satisfy |
|
|
||||||
| The rollout does not leave the old broker running | `rollout check` output, asserted in its test |
|
|
||||||
| The AMQP transport is gone | the package does not compile with it referenced; the switch variable is refused as unknown at start |
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
---
|
|
||||||
topic: the mesh
|
|
||||||
status: accepted
|
|
||||||
date: 2026-09-28
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
extends: 02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 132. A seat carries the tools its holder must serve
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
[ADR 0129](0129-a-seat-carries-the-protocol-of-its-role.md) gave a seat the protocol of its role in
|
|
||||||
three parts: the work it accepts, the events it emits, and the verbs it **serves** — request and
|
|
||||||
reply, awaited. The bus already derives authority from all three: a holder subscribes
|
|
||||||
`mesh.seat.<seat>.tool.<verb>`, and a module that uses the seat may publish it and nothing else.
|
|
||||||
|
|
||||||
**The serving third has never been used.** The mesh defines 14 seats, 8 mesh-scoped and 6
|
|
||||||
node-scoped. Exactly one carries a protocol at all — the build machine, which accepts `build` and
|
|
||||||
emits `built`. Not one seat declares a single verb it serves. The mechanism is built, enforced, and
|
|
||||||
empty.
|
|
||||||
|
|
||||||
Meanwhile every tool on the mesh is addressed to a module. Of 72 modules in the catalogue, 45 serve
|
|
||||||
tools, about 203 of them, each on `mesh.mod.<module>.tool.<name>`. So a caller binds to the module
|
|
||||||
that happens to hold a role rather than to the role, and replacing that module breaks every caller —
|
|
||||||
which is the thing seats exist to prevent everywhere else.
|
|
||||||
|
|
||||||
**Nothing can say what tools exist.** Measured on 2026-09-28, with the bus carrying the whole mesh: a
|
|
||||||
workstation client holding an operator credential connected, the bus accepted the account, and
|
|
||||||
`mesh call gitea.gitea_list_repos` answered with real repositories. The same client's `mesh tools`
|
|
||||||
found nothing, because it asks `mesh-catalog.catalog_tools` and no module serves that: the catalogue
|
|
||||||
serves `catalog_modules`, `catalog_module`, `catalog_provides`, `catalog_dependents` and
|
|
||||||
`catalog_stale`. An agent can therefore call any tool it already knows the name of and discover none.
|
|
||||||
MCP's `tools/list` is that same question, so the MCP surface is a working transport over an empty
|
|
||||||
catalogue.
|
|
||||||
|
|
||||||
**And there is nowhere for a tool's definition to live.** A manifest has a `tools` field: 0 of the 45
|
|
||||||
modules that serve tools fill it. That is not neglect, it is the arrangement failing — the field was
|
|
||||||
the bus grant's source for what a module may subscribe, and because nothing filled it every module
|
|
||||||
that served a tool was refused its own subscription on the new bus, live, until the grant was changed
|
|
||||||
to the module's own namespace. Today a tool's name, description and argument schema exist only in the
|
|
||||||
module's code.
|
|
||||||
|
|
||||||
Two facts about the machinery matter for what follows. A seat's protocol is not in the store: the seat
|
|
||||||
rows lack the ADR 0129 columns, so the protocol comes from compiled defaults and is merged in when a
|
|
||||||
row is read. And `seatSubject` is flat — `mesh.seat.<seat>.<kind>.<verb>` with no node in it — so a
|
|
||||||
node-scoped seat's tool call would reach every node's holder at once, and the holders' queue group
|
|
||||||
would hand it to whichever answered first.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**A seat's protocol carries its tools in full**: the verb, what it does, and the schema of its
|
|
||||||
arguments and of its answer. The seat is the definition of the role's interface; the holder is an
|
|
||||||
implementation of it.
|
|
||||||
|
|
||||||
**Serving the seat's tools is a condition of holding the seat.** A module that does not serve every
|
|
||||||
verb the seat declares may not occupy it. This is checked where the other conditions of holding are
|
|
||||||
checked — registration and handover — and refused by naming the verbs that are missing.
|
|
||||||
|
|
||||||
**A role's tools are addressed to the role.** `mesh.seat.<seat>.tool.<verb>` mesh-wide. A node-scoped
|
|
||||||
seat carries the node in the address, because one subject reaching six machines' holders is not an
|
|
||||||
address, and the queue group that made it look like one would silently pick a winner.
|
|
||||||
|
|
||||||
**A module keeps its own tools, and both exist.** `gitea_list_repos` stays, because gitea can run
|
|
||||||
without holding the `git` seat — a second forge, an instance kept for one purpose. The module's name
|
|
||||||
answers *this gitea*; the seat's verb answers *whoever is the forge*. Which of the two a caller wants
|
|
||||||
is a decision in the running session, not one the mesh makes for it.
|
|
||||||
|
|
||||||
**What answers "what tools exist" follows where the definition lives.** A seat's tools are read from
|
|
||||||
the mesh's own records. A module's own tools are answered by the module, from the code that defines
|
|
||||||
them. Discovery is therefore a read for the durable half and a question to the running mesh for the
|
|
||||||
free half.
|
|
||||||
|
|
||||||
**A seat's tools are an interface, and change like one.** Additive within a version; a change that
|
|
||||||
would break a caller takes the version token the subject already has room for (design 29 §8), and the
|
|
||||||
two run side by side until nothing is bound to the old one.
|
|
||||||
|
|
||||||
**The mesh's own verbs are the `mesh-controller` seat's tools.** `status`, `push`, `build`, `assign`
|
|
||||||
and the rest are a role's interface, not a container's, and the audit point [ADR 0095](0095-the-control-plane-is-the-way-to-ask-a-module.md)
|
|
||||||
asks for is the seat's holder.
|
|
||||||
|
|
||||||
## Options considered
|
|
||||||
|
|
||||||
1. **The manifest declares each module's tools.** Rejected. The list is then written twice — in the
|
|
||||||
manifest and in the code — and a schema in a manifest goes stale silently, which is the worst kind
|
|
||||||
of wrong for something an agent reads to decide what to call. It is also the arrangement that has
|
|
||||||
already failed once: the field exists, 0 of 45 modules fill it, and the grant that depended on it
|
|
||||||
refused every tool subscription on the mesh.
|
|
||||||
2. **Every runtime answers an introspection call, and something aggregates them.** Rejected as the
|
|
||||||
shape for a role's tools, kept for a module's own. An aggregator needs permission to publish into
|
|
||||||
every module's namespace, which is a widening the mesh otherwise gives only to the control plane;
|
|
||||||
and the answer is only as available as the modules are, so a mesh whose catalogue cannot say what a
|
|
||||||
role answers while its holder is down cannot plan against it.
|
|
||||||
3. **The control plane answers everything.** Rejected. It puts a tool surface on the control plane for
|
|
||||||
tools it does not implement, and makes discovery depend on the one component that must stay
|
|
||||||
answerable while it is itself being replaced. The mesh's own verbs are its to answer, and it answers
|
|
||||||
them as the holder of a seat.
|
|
||||||
4. **Seats only; no module tools.** Rejected. Most modules hold no seat, and inventing a seat per
|
|
||||||
module to give its tools a home would dilute what a seat is: one holder of a role the mesh needs
|
|
||||||
exactly one of.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
**One capability can have two names, deliberately.** A forge that holds the `git` seat answers both
|
|
||||||
`mesh.seat.git.tool.list_repos` and `mesh.mod.gitea.tool.gitea_list_repos`. This is the one place the
|
|
||||||
mesh accepts two names for one thing, because they are answers to different questions and the second
|
|
||||||
one survives the module not holding the seat. The glossary rule stands everywhere else.
|
|
||||||
|
|
||||||
**A seat becomes a contract to implement.** Adding a verb to a seat is a change every holder must
|
|
||||||
make, and a claim that was valid becomes invalid until it does. That is the point, and it is also the
|
|
||||||
reason a seat's tools should be few and durable while a module's own stay free.
|
|
||||||
|
|
||||||
**Three prerequisites, none of them in place.** The seat's protocol must be in the store rather than in
|
|
||||||
compiled defaults, or discovery reads a binary rather than the mesh. The protocol must become richer
|
|
||||||
than a list of verbs, because a verb without a schema is not something an agent can call. And a
|
|
||||||
node-scoped seat needs the node in its subject before any of its tools can exist.
|
|
||||||
|
|
||||||
**Discovery becomes cheap for the half that matters.** What roles the mesh has and what each answers is
|
|
||||||
a query, with no fan-out and nothing to be up. An agent's authority can then be role-shaped — *the
|
|
||||||
forge's tools* — rather than a list of module-specific names that changes when a module is replaced.
|
|
||||||
|
|
||||||
**The MCP surface belongs inside the mesh.** Once the tools are the mesh's own records, the thing that
|
|
||||||
serves them to an agent is 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.
|
|
||||||
|
|
||||||
## How this is checked
|
|
||||||
|
|
||||||
- **Holding is refused without the verbs.** The condition sits with the other conditions of holding a
|
|
||||||
seat, so registration and a handover both refuse a module that does not serve what the seat declares,
|
|
||||||
and the refusal names the missing verbs. A test per condition, as the other seat conditions have.
|
|
||||||
- **The grant is derived from the seat, and already is.** A holder's subscription and a user's publish
|
|
||||||
come from the seat's protocol, so a verb nobody declared is a subject nobody may use, and a verb the
|
|
||||||
seat declares reaches exactly its holder. The golden composition of the bus's user list is the test
|
|
||||||
that keeps it honest.
|
|
||||||
- **Discovery is a read, and is tested as one.** What the mesh answers for a seat's tools equals what
|
|
||||||
the seat's records declare — no call to a module in the path, so the test needs no running module.
|
|
||||||
- **A node-scoped seat's subject carries its node**, checked by the same test that checks the subject
|
|
||||||
table: two nodes holding one node-scoped seat derive two addresses.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0129](0129-a-seat-carries-the-protocol-of-its-role.md) — the protocol this widens
|
|
||||||
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) — the role, its work and its events
|
|
||||||
- [ADR 0095](0095-the-control-plane-is-the-way-to-ask-a-module.md) — a tool call passes one process where an audit belongs
|
|
||||||
- [ADR 0126](0126-a-module-declares-its-own-seats.md) — an event is addressed to its emitter, for the same reason a role's verb is addressed to its role
|
|
||||||
- [`03-DESIGN/01-to-be/26-the-seats.md`](../03-DESIGN/01-to-be/26-the-seats.md) — how a seat is held and handed over
|
|
||||||
- mesh-controller #116, #117, #118 — the grants as they now stand: a module serves its own namespace, the control plane may ask any tool
|
|
||||||
- Measured 2026-09-28 on the live mesh: an operator credential calling a module's tool over the bus answers; `tools/list` finds nothing
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
---
|
|
||||||
topic: what runs on it
|
|
||||||
status: superseded
|
|
||||||
date: 2026-09-28
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
extends: 02-DECISIONS/0052-a-step-that-runs-once-before-a-container.md
|
|
||||||
superseded-by: 02-DECISIONS/0135-a-module-version-prepares-its-state-before-it-runs.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 133. A module owns its migrations, and the mesh owns when they run
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
On 2026-09-28 the mesh replaced its own control plane, through its own upgrade path, with a build
|
|
||||||
carrying a migration. Nothing applied it. For the next three quarters of an hour every build the mesh
|
|
||||||
made was refused by the store with one line — *column "built_contexts" does not exist* — which reached
|
|
||||||
only whoever happened to be waiting on that build's reply. The images were built and published, so the
|
|
||||||
registry filled with artifacts the mesh has no record of, and the overview went on reporting that every
|
|
||||||
module was current ([issue 133](../04-ISSUES/133-the-control-planes-schema-is-migrated-at-birth-and-never-again/00-report.md)).
|
|
||||||
|
|
||||||
The schema had been created once, at genesis, by an action in the foundation bundle. Nothing ran it
|
|
||||||
again, through many updates of the control plane since.
|
|
||||||
|
|
||||||
**The mechanism to do this right already existed and one module used it wrong.**
|
|
||||||
[ADR 0052](0052-a-step-that-runs-once-before-a-container.md) makes a run-once container a step the host
|
|
||||||
runs to completion before whatever the declaration places after it, and names migrating a schema as the
|
|
||||||
case it exists for. Three facts about how it is used today:
|
|
||||||
|
|
||||||
- The control plane's manifest had no step at all. The immediate fix was to write one by hand, and that
|
|
||||||
hand-written step repeats three environment variables and three volume mounts from the server
|
|
||||||
resource it precedes — six chances to drift from the thing it prepares.
|
|
||||||
- Two other modules hand-write the same shape for the same reason: gitea's admin bootstrap and
|
|
||||||
mosquitto's dynsec seed, each repeating its sibling's image, environment and mounts. One of them
|
|
||||||
ends in `|| true`, which is a lock implemented as a shrug.
|
|
||||||
- The catalogue module takes the other road: it migrates its own schema in its own code when it starts.
|
|
||||||
That failure mode is a crash loop rather than a stop — the catalogue restarted 338 times this
|
|
||||||
morning on an unrelated start-time failure, and nothing anywhere said the mesh's graph had a gap.
|
|
||||||
|
|
||||||
**What the mesh already has, and what HAL needed stages for.** Ordering a provider before its consumer
|
|
||||||
is `providersFirst`, which topologically orders a node's modules. Ordering within a module is
|
|
||||||
declaration order, and a run-once container gates everything after it. Remembering that a step has
|
|
||||||
already run is the digest of its declaration, recorded only after it exits 0
|
|
||||||
([ADR 0018](0018-a-picture-is-read-from-what-runs.md)) — and the image is part of that digest, so a new
|
|
||||||
build re-runs it. Three of the four things a stage system provides are therefore already here. The
|
|
||||||
fourth — that a module has a schema at all — is the only thing missing.
|
|
||||||
|
|
||||||
**Nothing in the catalogue ships a migrations directory.** Of 72 modules, none has one; the modules that
|
|
||||||
migrate do it in their own code. So this is not a decision about where SQL files live. It is a decision
|
|
||||||
about who runs them and when.
|
|
||||||
|
|
||||||
**Two facts bound what is safely expressible.** A node converges toward its own declaration without
|
|
||||||
waiting on any other node. And of the five modules that run on more than one machine today — dnsmasq,
|
|
||||||
fail2ban, networking, networkmanager, sshd — not one wants a store; every module with a database is on
|
|
||||||
exactly one machine.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**A container may declare steps to run before it.** The same container, run to completion, with
|
|
||||||
different arguments, in order, before it starts. The mesh derives the run-once resources from that
|
|
||||||
declaration, so the image, the environment, the volumes, the network and the credentials come from the
|
|
||||||
one place they are already described and cannot drift from it.
|
|
||||||
|
|
||||||
**A module's migrations are the first user of this, and the module owns them entirely.** The SQL, the
|
|
||||||
order, the idempotence, the lock, and which dialect it speaks. The mesh never learns that postgres and
|
|
||||||
mssql differ, because it runs the module's own image with the module's own arguments against the
|
|
||||||
module's own binding and requires exit 0. A module needing both stores runs one step that does both.
|
|
||||||
|
|
||||||
**The mesh owns the moment, and the gate is the guarantee.** Whether a version may serve when its
|
|
||||||
schema is not there yet is a deployment question, and the mesh is the only thing that can answer it,
|
|
||||||
because the mesh is what starts the container. A step that fails stops the container it precedes, so
|
|
||||||
a failed migration is a version that does not serve rather than a version serving against a store it
|
|
||||||
does not match.
|
|
||||||
|
|
||||||
**Per node, and there is no level.** The step runs wherever the module runs. A step that ran "once,
|
|
||||||
somewhere" would leave every other machine with no gate at all, and additive migrations protect old
|
|
||||||
code against a new schema, never new code against an old one. The cost is an obligation a migration
|
|
||||||
runner already carries: a version table and a lock.
|
|
||||||
|
|
||||||
**"Once, mesh-wide" is what holding a seat means.** A step that is not idempotent — seeding an
|
|
||||||
account, sending a notice, taking a backup — belongs to a module that holds a seat, where the mesh
|
|
||||||
already guarantees one holder, on record, handed over deliberately. That is the answer to the level
|
|
||||||
question rather than a field that has to invent an election and keep it somewhere.
|
|
||||||
|
|
||||||
**Migrations are forward-only and additive.** The step runs before the *new* container starts, so the
|
|
||||||
old one is still running against the new schema for the length of the apply.
|
|
||||||
|
|
||||||
**Declared, never inferred.** The control plane cannot see inside an image, so a module that ships
|
|
||||||
migrations and declares no step is not refusable at registration; it breaks on its first upgrade. This
|
|
||||||
record says so rather than implying a check that cannot exist.
|
|
||||||
|
|
||||||
## Options considered
|
|
||||||
|
|
||||||
1. **Each module migrates itself when it starts** — what the catalogue does today. Rejected: it turns a
|
|
||||||
schema failure into a crash loop instead of a stop, it is invisible in the declaration so nothing can
|
|
||||||
say the module even has a schema, and two machines running the module both migrate at start with
|
|
||||||
nothing sequencing them.
|
|
||||||
2. **The mesh applies migrations itself**, with a driver and a version table per store — HAL's shape.
|
|
||||||
Rejected: the mesh would have to know one store type from another, hold another module's store
|
|
||||||
credentials, and reach a machine to use them, which [ADR 0005](0005-the-node-host.md) forbids. It is
|
|
||||||
also the reason that shape needs levels: something central has to decide where the once happens.
|
|
||||||
3. **A hook lifecycle** — pre-build, post-build, pre-deploy, post-deploy. Rejected: there is no deploy
|
|
||||||
event here to hook. A declaration is a desired state applied in order and reconciled forever, so
|
|
||||||
"pre-deploy" is exactly "a step before this container", pre- and post-build are what a Dockerfile and
|
|
||||||
the artifact list already are, and "post-deploy" has no moment to name.
|
|
||||||
4. **A hook level** — once per module, or once per module-node assignment. Rejected as a field, kept as
|
|
||||||
a property: see the decision. A once-per-module step needs cross-node ordering underneath it to be
|
|
||||||
safe, and a node converging without waiting on its neighbours is worth losing on purpose rather than
|
|
||||||
by accident.
|
|
||||||
5. **Every module hand-writes its own run-once step** — the immediate fix for the control plane.
|
|
||||||
Rejected as the general answer: it duplicates the resource it precedes, in three places already, and
|
|
||||||
a hand-written step is one the next module forgets. Forgetting it is the fault this record exists
|
|
||||||
for.
|
|
||||||
6. **Record a schema level per module in the store.** Rejected: gating makes the invariant true by
|
|
||||||
construction, so a level is a second account of the same fact and the first one to go stale.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
**Three hand-written steps collapse into one line each**, and the control plane's own migrate step stops
|
|
||||||
repeating its server's environment and mounts.
|
|
||||||
|
|
||||||
**The catalogue's self-migration becomes the exception to remove.** One shape, and the mesh's own
|
|
||||||
control plane is not an exception to it either.
|
|
||||||
|
|
||||||
**A module on two machines with one shared store must lock.** Today none is, so this is an obligation
|
|
||||||
stated before it is needed rather than discovered by two concurrent migrations.
|
|
||||||
|
|
||||||
**There is still no readiness-gated step.** Only an action carries `verify`; a container has no health
|
|
||||||
notion, so "run this once the service answers" remains unexpressible and seeding through a running
|
|
||||||
service's API has no home. That is its own decision about a container's readiness, and this record does
|
|
||||||
not make it.
|
|
||||||
|
|
||||||
**Genesis keeps its own action.** At birth there is no control plane to derive anything from, which is
|
|
||||||
what [ADR 0067](0067-genesis-is-a-pivot.md) already says about that moment.
|
|
||||||
|
|
||||||
## How this is checked
|
|
||||||
|
|
||||||
- **The composition carries the step.** A test on a node's composed declaration: every container that
|
|
||||||
declares steps before it is preceded by them, and the derived step's image, environment, volumes and
|
|
||||||
network equal the container's — so the two cannot drift, which is the failure the hand-written kind
|
|
||||||
has.
|
|
||||||
- **A failed step stops what follows.** The host already refuses to go on past a run-once step that did
|
|
||||||
not exit 0; the test for that is extended to a derived one, so the gate is checked rather than
|
|
||||||
assumed.
|
|
||||||
- **The mesh's own schema is covered by the same mechanism as everything else.** The control plane
|
|
||||||
declares its step in its own manifest, so the case that failed on 2026-09-28 is the case the test
|
|
||||||
covers.
|
|
||||||
- **A module claiming a seat for a once-only step is checked where seats are checked** — the conditions
|
|
||||||
of holding, not a new mechanism.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0052](0052-a-step-that-runs-once-before-a-container.md) — the step this extends
|
|
||||||
- [ADR 0018](0018-a-picture-is-read-from-what-runs.md) — a digest is the record that something happened
|
|
||||||
- [ADR 0005](0005-the-node-host.md) — the control plane decides and never touches a machine
|
|
||||||
- [ADR 0067](0067-genesis-is-a-pivot.md) — why genesis does it differently, once
|
|
||||||
- [issue 133](../04-ISSUES/133-the-control-planes-schema-is-migrated-at-birth-and-never-again/00-report.md) — the failure that produced this record
|
|
||||||
- [`03-DESIGN/01-to-be/32-what-a-module-declares.md`](../03-DESIGN/01-to-be/32-what-a-module-declares.md) §6 — the lifecycle this sits in
|
|
||||||
- Measured 2026-09-28: three hand-written run-once steps repeating their sibling's resource; 0 of 72 modules with a migrations directory; 5 modules on more than one machine, none of them wanting a store
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
---
|
|
||||||
topic: the mesh
|
|
||||||
status: accepted
|
|
||||||
date: 2026-09-28
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# 134. The mesh says what it applied
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
The pipeline is observable on the bus from a merge to an artifact, and modules already plug into it:
|
|
||||||
the forge emits `pull.merged`, the build machine's seat emits `built`, the catalogue emits `registered`,
|
|
||||||
`upgraded` and `rebuild-needed`, providers emit `postgres.database.provisioned` and its siblings. Things
|
|
||||||
consume them today — the catalogue consumes `built`, each provider consumes its own provisioning events,
|
|
||||||
model-usage consumes `*.usage.*`, the audit logger consumes `**`. Nothing had to be invented for any of
|
|
||||||
that; subscribing *is* plugging in.
|
|
||||||
|
|
||||||
**It goes dark at the moment it touches a machine.** A host applies a declaration and reports to the
|
|
||||||
control plane on the control branch, which only the control plane may read — correctly, because a report
|
|
||||||
carries what a machine is and enrolment travels the same way. So nothing on the mesh says *this machine
|
|
||||||
now runs version Y of module Z*, or that it refused to, or why.
|
|
||||||
|
|
||||||
What that cost on 2026-09-28, in one morning:
|
|
||||||
|
|
||||||
- A build result the store refused was visible only to whoever was waiting on that build's reply. For
|
|
||||||
three quarters of an hour the mesh built things and recorded none of them, while the overview said
|
|
||||||
every module was current ([issue 133](../04-ISSUES/133-the-control-planes-schema-is-migrated-at-birth-and-never-again/00-report.md)).
|
|
||||||
- A module crash-looping at start — 338 restarts — was found by reading a container's logs by hand.
|
|
||||||
Nothing on the bus said the mesh's graph had stopped learning.
|
|
||||||
- A run-once step that fails now stops an upgrade by design ([ADR 0133](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md)),
|
|
||||||
and the same silence would cover it: the version simply would not appear.
|
|
||||||
|
|
||||||
**And the one thing the control plane does emit is refused by its own account.** Answering a catalogue
|
|
||||||
that asks to catch up, it publishes each recorded build under `mesh.mod.control-plane.event.…` — a
|
|
||||||
module namespace for a module that does not exist. Its own permissions refuse it, so a catalogue that
|
|
||||||
restarts gets nothing and keeps its gap. The control plane has facts to state and nowhere to state
|
|
||||||
them.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**The mesh emits the deploy half of the pipeline as facts on the bus.** What a machine now runs, and
|
|
||||||
what it refused to run and why. Both are facts about the mesh doing its work, in the same form as every
|
|
||||||
other fact on the bus, so anything that wants them subscribes the way the catalogue subscribes to
|
|
||||||
`built`.
|
|
||||||
|
|
||||||
**The control plane states them, as the holder of the `mesh-controller` seat.** Its facts live under the
|
|
||||||
seat's own namespace, which is where a role's events belong
|
|
||||||
([ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md),
|
|
||||||
[ADR 0129](0129-a-seat-carries-the-protocol-of-its-role.md)) and which survives the control plane being
|
|
||||||
replaced. That is also what gives the catch-up replay a subject it may publish instead of an invented
|
|
||||||
module namespace.
|
|
||||||
|
|
||||||
**Emitted when what a machine runs changes, not on every convergence pass.** A host reconciles
|
|
||||||
continuously and reports each time; a fact per pass would be a fact per minute per machine that says
|
|
||||||
nothing. The report carries the declaration it applied and what changed, so the control plane has what
|
|
||||||
it needs to speak only when there is something to say.
|
|
||||||
|
|
||||||
**A refusal is a fact with a subject in it** — which machine, which resource, and the reason as the host
|
|
||||||
gave it. A refusal that names only the machine is the silence this record is about, one level up.
|
|
||||||
|
|
||||||
**Reports stay where they are.** A node's report remains control traffic that only the control plane
|
|
||||||
reads. The deploy facts are derived from it, which makes them second-hand on purpose: one emitter, one
|
|
||||||
ordering, and no widening of the narrowest account in the mesh.
|
|
||||||
|
|
||||||
## Options considered
|
|
||||||
|
|
||||||
1. **Leave it as it is, and let whatever cares ask the control plane.** Rejected: asking for a fact that
|
|
||||||
already arrives is the shape the mesh removed everywhere else, and nothing can react at the moment a
|
|
||||||
machine changes — which is exactly when a graph, an audit or an operator wants to know.
|
|
||||||
2. **Each node emits its own facts.** Rejected: it widens every host's account to an event namespace, and
|
|
||||||
a host's authority is deliberately the narrowest in the mesh. Its report already reaches the one thing
|
|
||||||
that can speak for it.
|
|
||||||
3. **Widen who may read the control branch.** Rejected: that branch carries what machines say *to* the
|
|
||||||
control plane, enrolment included. Widening its readers widens that too, for an unrelated reason.
|
|
||||||
4. **A registry of deploy hooks** — something registers interest and is called. Rejected: an event is
|
|
||||||
already the mechanism; there is nothing to register, and a callback is an address the mesh spent
|
|
||||||
[issue 102](../04-ISSUES/102-an-address-recorded-at-genesis-or-build-does-not-follow-the-nodes-ports/00-report.md)
|
|
||||||
learning not to keep.
|
|
||||||
5. **Put the facts on the node's own declaration stream.** Rejected: that stream is last-per-subject by
|
|
||||||
design, so a machine away for an hour gets exactly the current declaration and nothing older. A
|
|
||||||
history of what happened cannot live in a stream built to forget.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
**The audit logger gets the deploy half for nothing**, because it consumes everything.
|
|
||||||
|
|
||||||
**A failure becomes visible where the mesh is watched** rather than where someone happened to be
|
|
||||||
looking. That answers the open question [issue 133](../04-ISSUES/133-the-control-planes-schema-is-migrated-at-birth-and-never-again/00-report.md)
|
|
||||||
left about a record the store refused.
|
|
||||||
|
|
||||||
**The catch-up replay stops being a burst of events.** With the control plane able to state its own
|
|
||||||
facts, replaying history as if it were happening now is a choice rather than the only option — and the
|
|
||||||
better shape is the question the catalogue is actually asking, answered once
|
|
||||||
([design 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md)).
|
|
||||||
|
|
||||||
**The facts are second-hand.** The control plane says what a machine reported, so a machine that cannot
|
|
||||||
reach the bus produces no fact at all. Absence is not health, and what a machine was last heard from
|
|
||||||
stays the place that says so.
|
|
||||||
|
|
||||||
**The events stream carries more.** Bounded by emitting on change rather than on every pass, and each
|
|
||||||
fact is small; the stream's own limits remain what keeps it finite.
|
|
||||||
|
|
||||||
## How this is checked
|
|
||||||
|
|
||||||
- **The control plane's grant names exactly the subjects it emits**, derived from its seat like every
|
|
||||||
other principal's, and the composed user list is compared against a golden file — so a fact it cannot
|
|
||||||
publish fails a test rather than a catalogue's replay.
|
|
||||||
- **A convergence that changed nothing emits nothing.** A test with two identical reports and one
|
|
||||||
expected fact, because the failure this guards against is a fact per minute per machine.
|
|
||||||
- **A refusal names its resource.** A test where a host reports a failed resource and the emitted fact
|
|
||||||
carries which one and why, not merely that something went wrong.
|
|
||||||
- **What the mesh emits is what something consumes.** The subject a module declares it consumes derives
|
|
||||||
to the subject the control plane publishes — the same agreement test that already keeps the
|
|
||||||
controller's own subscriptions honest.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0041](0041-events-are-a-relationship.md) — an event is a relationship, not a call
|
|
||||||
- [ADR 0126](0126-a-module-declares-its-own-seats.md) — an event is addressed to its emitter, because the emitter's identity is the meaning
|
|
||||||
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0129](0129-a-seat-carries-the-protocol-of-its-role.md) — a role's events belong to the role
|
|
||||||
- [ADR 0083](0083-one-push-leaves-the-mesh-consistent.md) — a report is held for the store rather than lost
|
|
||||||
- [ADR 0133](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md) — the gate whose failure this makes visible
|
|
||||||
- [`03-DESIGN/01-to-be/32-what-a-module-declares.md`](../03-DESIGN/01-to-be/32-what-a-module-declares.md) §6 — the lifecycle, which ends today at a report nobody else may read
|
|
||||||
- Measured 2026-09-28: 45 minutes of builds recorded nowhere with the overview reporting health; a module at 338 restarts found by hand; the control plane's only emitted event refused by its own permissions
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
---
|
|
||||||
topic: what runs on it
|
|
||||||
status: accepted
|
|
||||||
date: 2026-09-28
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
supersedes: 0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 135. A module version prepares its state before it runs
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
[ADR 0133](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md) settled who runs a
|
|
||||||
module's migrations and when, and it said so in the wrong vocabulary. It put the declaration on a
|
|
||||||
*container* — "a container may declare steps to run before it" — and derived the scope of the work from
|
|
||||||
the *machine*. Both are wrong at the level a module author works at, and the second is wrong on the
|
|
||||||
facts.
|
|
||||||
|
|
||||||
**A container is one resource kind the host applies.** A module has code, state and a version; whether
|
|
||||||
its artifact is an image, a bundle or something later is the mesh's business. The module-facing
|
|
||||||
vocabulary for a module's own code already exists and has nothing to do with a container runtime: a
|
|
||||||
module declares **entrypoints** — this file is my tools, this file is my provisioner — and the mesh runs
|
|
||||||
them. A manifest that says "run this container with these arguments, and here are the volumes and
|
|
||||||
environment again" has an author writing down the machine's business twice.
|
|
||||||
|
|
||||||
**And the scope is not the machine's to decide, because the mesh already decided what a state is.** A
|
|
||||||
consumer is a module *on a machine* (migration 0015, from
|
|
||||||
[issue 022](../04-ISSUES/022-one-credential-per-node-per-provision-not-per-module/00-report.md)):
|
|
||||||
the mesh derives a login per consumer and the provider creates a database owned by exactly that login
|
|
||||||
([ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)). So a module on three machines is three
|
|
||||||
consumers, three credentials and three databases. There is no shared state for two machines to race over,
|
|
||||||
and ADR 0133's central caveat — that a module's migrations must take a lock because two machines might
|
|
||||||
migrate at once — describes a situation the mesh does not currently produce.
|
|
||||||
|
|
||||||
That correction makes the whole "level" question HAL answered with stages disappear: the scope of
|
|
||||||
preparation is the scope of the state, and the mesh knows it.
|
|
||||||
|
|
||||||
What the earlier record got right and this one keeps: the module owns the work, the mesh owns the moment,
|
|
||||||
the gate is the guarantee, migrations stay forward-only, and none of it can be inferred from inside an
|
|
||||||
artifact. What produced it also stands — the control plane was replaced with a build carrying a migration,
|
|
||||||
nothing applied it, and for three quarters of an hour every build was refused by the store with one line
|
|
||||||
that reached only whoever was waiting on a reply
|
|
||||||
([issue 133](../04-ISSUES/133-the-control-planes-schema-is-migrated-at-birth-and-never-again/00-report.md)).
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**A module version declares an entrypoint that prepares its state.** One name in the manifest, in the
|
|
||||||
same vocabulary as the entrypoints it already declares for its tools and its provisioner. No container,
|
|
||||||
no command line, no environment, no mounts — those are how a machine runs the module's code, and the
|
|
||||||
module already said that once.
|
|
||||||
|
|
||||||
**The mesh runs it as it runs that module's own code, to completion, in the module's own context.** Every
|
|
||||||
binding, credential and setting the module's code would receive, because it *is* the module's code. How a
|
|
||||||
machine does that is the host's business and stays there: for an image artifact it is the step
|
|
||||||
[ADR 0052](0052-a-step-that-runs-once-before-a-container.md) already defines, and a later kind of artifact
|
|
||||||
changes the host, not the manifest.
|
|
||||||
|
|
||||||
**Preparation gates the version.** A version whose preparation did not succeed does not run — anywhere.
|
|
||||||
Since the rollout already sends machines one at a time and stops at the first that does not take a
|
|
||||||
version, a preparation that fails stops the rollout there, leaving every other machine on the version
|
|
||||||
that works.
|
|
||||||
|
|
||||||
**Preparation is scoped to the state, and the mesh derives that scope.** State the mesh provisions is per
|
|
||||||
consumer — a module on a machine — so preparation happens once per consumer. State the module keeps on
|
|
||||||
the machine is per machine, which is the same answer. A module that holds an exclusive seat has one of
|
|
||||||
itself, so its preparation happens once by definition. No level, no election, no cross-node ordering, and
|
|
||||||
no lock obligation invented for a race the mesh does not create.
|
|
||||||
|
|
||||||
**Once per version per state.** A version bump attempts preparation once against each state it has; the
|
|
||||||
module's own runner decides there is nothing to do, which is what a runner with a version table does
|
|
||||||
anyway. A retry after a partial failure runs it again, so the work is the module's to make safe against
|
|
||||||
that — the one obligation no design can remove.
|
|
||||||
|
|
||||||
**Forward-only and additive.** Preparation runs while the previous version is still serving, so a
|
|
||||||
migration that removes or renames what the old code reads breaks the mesh in the window between the two.
|
|
||||||
|
|
||||||
**Declared, never inferred.** The control plane cannot see inside an artifact, so a module that ships
|
|
||||||
migrations and declares no entrypoint is not refusable at registration. It breaks on its first upgrade,
|
|
||||||
and this record says so rather than implying a check that cannot exist.
|
|
||||||
|
|
||||||
## Options considered
|
|
||||||
|
|
||||||
1. **A container declares steps before it** — [ADR 0133](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md).
|
|
||||||
Superseded, not because the mechanism is wrong but because the *declaration* is in the wrong place: it
|
|
||||||
makes every module author restate the machine's arrangement, and it ties a module's own lifecycle to
|
|
||||||
one resource kind. The host-side mechanism it named is retained and is now an implementation detail.
|
|
||||||
2. **Each module prepares itself when it starts** — what the catalogue does today. Rejected: a schema
|
|
||||||
failure becomes a crash loop rather than a stop, nothing in the declaration says the module has a
|
|
||||||
state to prepare, and the version serves the moment it starts rather than after the state is right.
|
|
||||||
3. **The mesh applies migrations itself**, with a driver and a version table per store type. Rejected:
|
|
||||||
the mesh would have to know one store from another, hold another module's credentials and reach a
|
|
||||||
machine with them, which [ADR 0005](0005-the-node-host.md) forbids. It is also what forces a stage
|
|
||||||
system: something central has to decide where the work happens.
|
|
||||||
4. **A hook lifecycle** — pre-build, post-build, pre-deploy, post-deploy. Rejected: a declaration is a
|
|
||||||
desired state reconciled forever, so there is no deploy moment to hook. "Pre-deploy" is exactly this
|
|
||||||
record; pre- and post-build are what a recipe and the artifact list already are; "post-deploy" names
|
|
||||||
nothing that happens.
|
|
||||||
5. **A declared level** — once per module, or once per assignment. Rejected: the mesh already knows what a
|
|
||||||
state is, so asking an author to choose is asking them to restate a fact the mesh holds, with a chance
|
|
||||||
of contradicting it.
|
|
||||||
6. **Record a preparation level per module in the store.** Rejected for the reason ADR 0133 gave and this
|
|
||||||
record keeps: gating makes the invariant true by construction, and a level is a second account of the
|
|
||||||
same fact.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
**An author's whole contract is one line, once.** Write the migration in the module's code, name the
|
|
||||||
entrypoint that runs it, and every later version rolls out as: build, prepare, run — with nothing
|
|
||||||
per-version to remember and nothing about the machine to restate. That is the property this exists for.
|
|
||||||
|
|
||||||
**Three hand-written steps in the catalogue collapse**, and the control plane's own migrate step stops
|
|
||||||
repeating its server's environment and mounts.
|
|
||||||
|
|
||||||
**The catalogue's self-preparation becomes the exception to remove.** One shape, and the mesh's own
|
|
||||||
control plane is not an exception either.
|
|
||||||
|
|
||||||
**A module scaled across machines with one shared state is not expressible**, and this record does not
|
|
||||||
make it so. The mesh gives each consumer its own state; a deliberately shared one is a different
|
|
||||||
provision model, and the place the "once, mesh-wide" question would genuinely return. Named here so it is
|
|
||||||
a decision when it happens rather than a surprise.
|
|
||||||
|
|
||||||
**There is still no readiness-gated step.** Only an action carries `verify`; nothing declares that a
|
|
||||||
service answers, so preparation that must happen *after* something is serving — seeding through its own
|
|
||||||
API — remains unexpressible.
|
|
||||||
|
|
||||||
**Genesis keeps its own action.** At birth there is no control plane to derive anything, which is what
|
|
||||||
[ADR 0067](0067-genesis-is-a-pivot.md) says about that moment.
|
|
||||||
|
|
||||||
## How this is checked
|
|
||||||
|
|
||||||
- **The composition carries the preparation, in the module's own context.** A test on a node's composed
|
|
||||||
declaration: a version declaring a preparation entrypoint is preceded by it, and what it is given
|
|
||||||
equals what the module's own code is given — asserted equal rather than written twice, which is the
|
|
||||||
drift the superseded shape invited.
|
|
||||||
- **A preparation that fails stops the version.** The host does not go past a step that did not complete,
|
|
||||||
and the rollout stops at the first machine that did not take a version. Both are existing behaviours
|
|
||||||
with existing tests; the test for preparation asserts the two together — the machine does not run it,
|
|
||||||
and the machines after it are left alone.
|
|
||||||
- **Once per version per state.** A test that a second convergence of the same version prepares nothing,
|
|
||||||
and that a new version prepares again.
|
|
||||||
- **The mesh's own control plane declares one.** The case that failed on 2026-09-28 is the case the tests
|
|
||||||
cover, rather than a case a comment says is covered.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0133](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md) — what this supersedes, and why
|
|
||||||
- [ADR 0052](0052-a-step-that-runs-once-before-a-container.md) — the host-side step that implements it for an image artifact
|
|
||||||
- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md), [issue 022](../04-ISSUES/022-one-credential-per-node-per-provision-not-per-module/00-report.md) — a consumer is a module on a machine, which is what makes the scope derivable
|
|
||||||
- [ADR 0018](0018-a-picture-is-read-from-what-runs.md) — a digest is the record that something happened
|
|
||||||
- [ADR 0005](0005-the-node-host.md) — the control plane decides and never touches a machine
|
|
||||||
- [ADR 0134](0134-the-mesh-says-what-it-applied.md) — what makes a failed preparation visible
|
|
||||||
- [issue 133](../04-ISSUES/133-the-control-planes-schema-is-migrated-at-birth-and-never-again/00-report.md) — the failure that produced both records
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
---
|
|
||||||
topic: what runs on it
|
|
||||||
status: accepted
|
|
||||||
date: 2026-09-28
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
extends: 02-DECISIONS/0052-a-step-that-runs-once-before-a-container.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 136. A step gates its module, not the machine
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
[ADR 0052](0052-a-step-that-runs-once-before-a-container.md) made a run-once container a step the host
|
|
||||||
runs to completion, and gave it the same reach a failed action has: it stops everything the declaration
|
|
||||||
places after it. When the only steps on the mesh were a broker's seed and a forge's admin account, that
|
|
||||||
reach was invisible — the thing after the step was the container the step existed for, in the same
|
|
||||||
module.
|
|
||||||
|
|
||||||
[ADR 0135](0135-a-module-version-prepares-its-state-before-it-runs.md) made a step something the mesh
|
|
||||||
derives for **any** module that prepares its state, and that turns the reach into a fault. A module
|
|
||||||
whose database is briefly unreachable now stops every module declared after it on that machine, for as
|
|
||||||
long as it is unreachable.
|
|
||||||
|
|
||||||
**The host already rejected this for every other shape, and says why in its own loop.** From
|
|
||||||
[issue 011](../04-ISSUES/011-one-broken-module-blocks-every-other/00-report.md):
|
|
||||||
|
|
||||||
> It used to stop at the first one, and that made one broken resource hold the whole machine hostage: a
|
|
||||||
> module declaring a package that does not exist meant every module ordered after it was never applied,
|
|
||||||
> for ever, and the mesh reported "failed" without saying that the rest had not been tried. A machine
|
|
||||||
> with one bad module and nine good ones ran none of the nine.
|
|
||||||
|
|
||||||
Everything is attempted and every failure reported — except an action and a run-once step, kept as the
|
|
||||||
deliberate exceptions. So the mesh has two rules about the same question and the wider one is now
|
|
||||||
reachable by any module that declares a schema.
|
|
||||||
|
|
||||||
**And it deadlocks a case the catalogue already named.** The catalogue migrates its own schema when it
|
|
||||||
starts rather than in a step, and says why in its code: *a schema step that had to reach the provider
|
|
||||||
over the overlay would block the very apply that brings the overlay up*. With a machine-wide gate that
|
|
||||||
is exactly right — the step fails, the apply stops, the overlay module after it is never applied, and
|
|
||||||
the next reconcile is blocked the same way. The module that most obviously wants a step could not have
|
|
||||||
one.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**A step gates its own module.** A run-once container that does not complete stops the rest of *that
|
|
||||||
module's* resources and nothing else. Every other module on the machine is attempted, as every other
|
|
||||||
shape already is.
|
|
||||||
|
|
||||||
**An action still gates the machine.** Genesis is a row of actions, each making the next possible, and
|
|
||||||
they belong to no module — there is nothing narrower for their reach to be.
|
|
||||||
|
|
||||||
**What was not attempted is reported, not inferred from silence.** A skipped resource appears in the
|
|
||||||
machine's account of the apply as skipped, with the reason, because "not attempted" and "nothing to do"
|
|
||||||
are different answers and only one of them is somebody's to fix.
|
|
||||||
|
|
||||||
**A module is the part of a resource's identity before the first dot**, which is how the mesh composes
|
|
||||||
them. What the mesh declares in its own right — a guard, an opening, the adoption's own resources —
|
|
||||||
belongs to no module, and its gate is therefore the machine's.
|
|
||||||
|
|
||||||
## Options considered
|
|
||||||
|
|
||||||
1. **Leave the reach as it is.** Rejected: it reintroduces, through a mechanism now derived for every
|
|
||||||
module, exactly the fault issue 011 removed. A mesh where one module's unreachable database stops a
|
|
||||||
machine converging is worse than one where that module alone is behind.
|
|
||||||
2. **Make preparation not a gate at all** — run it and carry on. Rejected: then a version serves against
|
|
||||||
a state nobody shaped, which is the whole of what ADR 0135 exists to prevent.
|
|
||||||
3. **Order every module's step before everything else on the machine**, so a gate stops nothing that
|
|
||||||
matters. Rejected: it inverts the order a module needs — its files and directories are declared before
|
|
||||||
its step because the step reads them — and it would still stop later modules.
|
|
||||||
4. **Let a module declare how far its step reaches.** Rejected: the answer is the same for every module,
|
|
||||||
and a field would let one be wrong about it.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
**The catalogue can move to a step.** The reason it migrates at start — that a step blocks the apply
|
|
||||||
that would make its provider reachable — stops being true: the step fails, that module waits, the
|
|
||||||
overlay comes up, and the next reconcile prepares it. One shape for the whole mesh, which is what
|
|
||||||
ADR 0135 asked for and could not have had.
|
|
||||||
|
|
||||||
**A module can sit behind while the machine is otherwise current.** That is the honest state and it is
|
|
||||||
what the report now says. It also means a preparation that never succeeds is a module that never
|
|
||||||
upgrades, quietly, until somebody reads the report — which is an argument for
|
|
||||||
[ADR 0134](0134-the-mesh-says-what-it-applied.md) rather than against this.
|
|
||||||
|
|
||||||
**A module's resources must be ordered within the module for the gate to mean anything.** They already
|
|
||||||
are: the mesh composes a module's resources in the order its manifest declares them, and its own
|
|
||||||
workload comes after the files it reads.
|
|
||||||
|
|
||||||
## How this is checked
|
|
||||||
|
|
||||||
- **A failed step stops its module and nothing else.** A test with two modules: the one whose step
|
|
||||||
failed does not start its workload, the other starts, and the error still says the failure gated
|
|
||||||
something. It fails against the previous behaviour, which is how it was written.
|
|
||||||
- **An action still stops the machine.** The existing test for a failed action is unchanged, and a step
|
|
||||||
with no module in its identity — which is what genesis carries — takes the same path.
|
|
||||||
- **The report names what was skipped.** Asserted in the same test, because a gate nobody can see is
|
|
||||||
indistinguishable from a module that had nothing to do.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0052](0052-a-step-that-runs-once-before-a-container.md) — the step this narrows
|
|
||||||
- [ADR 0135](0135-a-module-version-prepares-its-state-before-it-runs.md) — what made the reach reachable
|
|
||||||
- [issue 011](../04-ISSUES/011-one-broken-module-blocks-every-other/00-report.md) — the same fault, removed once already
|
|
||||||
- [ADR 0134](0134-the-mesh-says-what-it-applied.md) — how a module left behind becomes visible
|
|
||||||
- mesh-host `internal/apply` — the loop whose own comment argued this case for every other shape
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
---
|
|
||||||
topic: what runs on it
|
|
||||||
status: superseded
|
|
||||||
date: 2026-09-28
|
|
||||||
deciders: jochen
|
|
||||||
extends: 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
|
|
||||||
reconstructed: false
|
|
||||||
superseded-by: 02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 137. A machine says which networks it routes
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
The filter the mesh derives denies forwarding by default, because without a forward chain it says
|
|
||||||
nothing about a container's published port
|
|
||||||
([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md), [issue 047](../04-ISSUES/047-the-firewall-does-not-cover-published-container-ports/00-report.md)).
|
|
||||||
To keep a machine's own containers working it then allows two ranges: the container runtime's
|
|
||||||
default bridge pool, and the pool its compose files are given. Those two are named in the
|
|
||||||
controller's code, with a comment saying what the gap is:
|
|
||||||
|
|
||||||
> A machine whose runtime is configured with something else needs this to say so — which is a thing
|
|
||||||
> the mesh cannot derive and a reason this list is named here rather than computed.
|
|
||||||
|
|
||||||
**There was no way to say so.** The list was a constant. A machine whose guests live anywhere else
|
|
||||||
was filtered by a rule that looked deliberate and was a guess.
|
|
||||||
|
|
||||||
**Measured, on the day a workstation was converged.** Flipping it cut egress for five of its
|
|
||||||
container networks at once, and for every network its test beds create — the beds allocate a fresh
|
|
||||||
range per run, from a pool neither default covers. Nothing reported a fault. The containers could
|
|
||||||
not reach anything, the machine went on reporting that it had applied what it was told, and the
|
|
||||||
converge preview had said nothing about it either, because the preview lists what *listens* and
|
|
||||||
routing is not a listener.
|
|
||||||
|
|
||||||
**And two questions, not one.** A guest also asks its host for an address and for names. Both arrive
|
|
||||||
at the input chain, where nothing declared them, so denying by default left the guests of a routed
|
|
||||||
network with no address and no resolution — which is not a closed port but a network that does not
|
|
||||||
function, asked for by this machine's own guest.
|
|
||||||
|
|
||||||
**Why the machine cannot simply be read.** A test bed creates its bridge while it runs, between one
|
|
||||||
declaration and the next, so a filter derived from what the machine last reported would be correct
|
|
||||||
only for the networks that already existed when it was composed. A declared range covers the ones
|
|
||||||
that do not exist yet.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**A machine says which networks it routes for what it hosts, and the filter forwards them.** A
|
|
||||||
node-level fact, beside the node's public domain
|
|
||||||
([ADR 0066](0066-public-routing-is-name-agnostic.md)) and for the same reason: the
|
|
||||||
machine routes them, and the module that loads the filter holds a seat and may be replaced.
|
|
||||||
|
|
||||||
**Added to the runtime's defaults, never replacing them.** A machine that names one range has not
|
|
||||||
stopped hosting whatever was already on the runtime's own pools, and replacing would trade one
|
|
||||||
silent breakage for another.
|
|
||||||
|
|
||||||
**Their guests keep address and name service.** For a network that was named, the input chain admits
|
|
||||||
that network's own DHCP and DNS, and nothing else: everything else a guest might want from its host
|
|
||||||
is a port somebody declares, like every other port on this machine.
|
|
||||||
|
|
||||||
**Said in CIDR form and checked when it is said.** An entry that does not parse is a line nftables
|
|
||||||
refuses, and a refused ruleset is a machine filtering nothing while its unit reports a fault — so
|
|
||||||
the refusal happens where a person can read it, not on the machine.
|
|
||||||
|
|
||||||
**A machine that says nothing is filtered exactly as before.** Every machine already converged is
|
|
||||||
untouched by this.
|
|
||||||
|
|
||||||
## Options considered
|
|
||||||
|
|
||||||
1. **Leave it constant and edit the code per installation.** Rejected: the value is a property of
|
|
||||||
one machine, the code is the whole mesh's, and the two ranges as they stand describe a machine
|
|
||||||
whose runtime was left at its defaults. It is also how this got here.
|
|
||||||
2. **Derive it from what the machine reports.** Rejected as insufficient, not as wrong: it cannot
|
|
||||||
cover a network created between two declarations, which is precisely the case that was broken. It
|
|
||||||
would also make the filter follow whatever appeared on the machine, which is a firewall that
|
|
||||||
widens itself.
|
|
||||||
3. **A per-node setting on the module that loads the filter.** Rejected: the machine routes the
|
|
||||||
networks. The filter module holds a node-scoped seat and is meant to be replaceable, and a
|
|
||||||
replacement must not lose the machine's own truth.
|
|
||||||
4. **Replace the defaults with what is said.** Rejected: see the decision. The first machine to name
|
|
||||||
its bed range would lose its containers.
|
|
||||||
5. **Admit all input from a routed network, not only address and name service.** Rejected: that is
|
|
||||||
every port on the machine open to anything it hosts, which is the derivation abandoned.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
**The converge preview says what a machine routes**, including when it routes nothing but the
|
|
||||||
defaults, with the command that changes it. The preview's own sentence about traffic it cannot
|
|
||||||
preview stays, because a tunnel and the found firewall's NAT are still not previewable.
|
|
||||||
|
|
||||||
**A machine whose guests are already broken by an earlier flip is fixed by saying its networks and
|
|
||||||
pushing**, with no flip to undo.
|
|
||||||
|
|
||||||
**The list is one more thing that can be wrong and stale.** A range removed from the machine and
|
|
||||||
left here keeps forwarding for a network that no longer exists, which admits nothing, because there
|
|
||||||
is no guest on it to admit. That is the safe direction of being out of date.
|
|
||||||
|
|
||||||
## How this is checked
|
|
||||||
|
|
||||||
- **What a machine says it routes is forwarded, and its guests keep address and name service.** A
|
|
||||||
test renders a ruleset for a machine that names one range and asserts both chains, per chain body
|
|
||||||
so a line in the wrong chain cannot pass it. It fails against the previous behaviour, which is how
|
|
||||||
it was written.
|
|
||||||
- **The runtime's own defaults survive naming a range.** Asserted in the same test.
|
|
||||||
- **A machine that names nothing renders byte-identically to one that names nil**, so every machine
|
|
||||||
already behind this filter is untouched.
|
|
||||||
- **Each family is matched in its own syntax.** A test with one v4 and one v6 network asserts
|
|
||||||
`ip saddr` and `ip6 saddr`, because one set holding both is a syntax error and a ruleset that does
|
|
||||||
not load is a machine filtering nothing.
|
|
||||||
- **An entry that is not a network is refused where it is said**, by the parse in the setter.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md) — the derived filter this completes
|
|
||||||
- [ADR 0066](0066-public-routing-is-name-agnostic.md) — the precedent for a node-level fact
|
|
||||||
- [issue 047](../04-ISSUES/047-the-firewall-does-not-cover-published-container-ports/00-report.md) — why there is a forward chain at all
|
|
||||||
- [issue 137](../04-ISSUES/137-converging-a-machine-cut-off-its-own-guests/00-report.md) — the measurement that produced this
|
|
||||||
- mesh-controller `internal/catalogue/filtering.go` — the constant whose own comment named this gap
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
---
|
|
||||||
topic: what runs on it
|
|
||||||
status: accepted
|
|
||||||
date: 2026-09-28
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
extends: 02-DECISIONS/0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 138. An assignment binds an endpoint and says how far it reaches
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
[ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md) settled that a machine's
|
|
||||||
packet filter is derived from what its modules declare they listen on, and that the `from` of a
|
|
||||||
listen "is the whole of public-versus-internal". That was true of the packet filter, and it turned
|
|
||||||
out to be true of nothing else.
|
|
||||||
|
|
||||||
Reachability is now settled three times, in three places, by three mechanisms that cannot disagree
|
|
||||||
out loud ([issue 140](../04-ISSUES/140-an-endpoints-reach-is-not-declared/00-report.md)):
|
|
||||||
|
|
||||||
- **The filter** reads a listen's source, and a per-node setting may override it. That setting has
|
|
||||||
exactly one caller in the control plane — the function that builds the node's rules.
|
|
||||||
- **The names** come from a route contribution, which names a label and a port and says nothing
|
|
||||||
about reach. The reverse proxy composes a **public** name and an **internal** name for every route
|
|
||||||
it is given, because it can.
|
|
||||||
- **The certificate authority** follows from which names exist. Measured on the control-node: an
|
|
||||||
identity provider carries a public certificate valid 90 days and an internal one valid 24 hours and
|
|
||||||
renewed daily. No assignment asked for either.
|
|
||||||
|
|
||||||
So *this endpoint must not be public* cannot be written. It is therefore enforced by nothing, while a
|
|
||||||
public certificate for that very name is obtained automatically — the fault
|
|
||||||
[how-we-build.md](../00-META/how-we-build.md) names, an unenforced rule being indistinguishable from
|
|
||||||
a wrong one, with the additional cost that the wrong thing is done eagerly.
|
|
||||||
|
|
||||||
And a port that is not routed cannot be spoken about at all beyond the filter. The forge serves git
|
|
||||||
over ssh; that endpoint has no name, no certificate and no way to be called public except a key only
|
|
||||||
the filter reads.
|
|
||||||
|
|
||||||
**Two per-node settings already exist and are half of this.** One gives a module's declared port a
|
|
||||||
machine port. One overrides a declared port's source. They key on port numbers, so nothing ties a
|
|
||||||
port to the route that serves it: a route contribution names a port too, and the two are equal only
|
|
||||||
by coincidence.
|
|
||||||
|
|
||||||
**Where this belongs is already decided.** [ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md)
|
|
||||||
says a module's configuration is its assignments. Whether the forge answers git-over-ssh from the
|
|
||||||
public internet is a fact about one installation and one machine, not a property of the software —
|
|
||||||
and [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) already refuses an
|
|
||||||
installation's decisions in a definition.
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
1. **Leave reach in the manifest, as `from` today.** Rejected: it is an installation's decision
|
|
||||||
written into the definition, and it cannot differ between two machines running the same module —
|
|
||||||
which is exactly the case the forge presents.
|
|
||||||
2. **Extend the existing source override to the names and the certificate, without naming
|
|
||||||
endpoints.** Rejected: it keys on a port number. A module's route contribution names a port as
|
|
||||||
well, and nothing says the two are the same thing, so one statement cannot be made to reach all
|
|
||||||
three mechanisms. Naming the endpoint is what makes that possible.
|
|
||||||
3. **Derive reach from whether the node has a public domain recorded.** Rejected: that is a property
|
|
||||||
of the machine, and two endpoints on one machine differ — a database and a web front end on the
|
|
||||||
same host.
|
|
||||||
4. **A fourth reach for "public name, internal authority"** — a name that resolves publicly and must
|
|
||||||
not appear in a public issuance log, obtained by DNS-01. Deferred, not rejected: it is a real case
|
|
||||||
and it is a question about which challenge an authority uses, not about how far an endpoint
|
|
||||||
reaches. Left to the certificate work as an open question.
|
|
||||||
5. **Make the manifest silent on reach and require every assignment to state it.** Rejected for the
|
|
||||||
transition: every endpoint reachable today would close until an assignment named it, which is a
|
|
||||||
flag day across the whole catalogue.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**A module declares named endpoints.** An endpoint is one port the module serves, with a name the
|
|
||||||
module chooses, its protocol, and what it is for. A route contribution **names the endpoint it
|
|
||||||
routes** rather than repeating a port number. The manifest says what the module serves and what it
|
|
||||||
would serve it to by default; it does not say what this installation does with it.
|
|
||||||
|
|
||||||
**An assignment binds each endpoint and says how far it reaches.** Per node: the machine port the
|
|
||||||
endpoint is published on, and its **reach** — one of `internal`, `public` or `both`. An assignment
|
|
||||||
that states nothing keeps the manifest's default, so no machine changes until an assignment says so.
|
|
||||||
|
|
||||||
**Reach means all three mechanisms at once, and is the only thing that decides them.**
|
|
||||||
|
|
||||||
- `internal` — the filter opens the machine port to the private network; the proxy serves the
|
|
||||||
internal name and not the public one; the certificate comes from the mesh's own authority.
|
|
||||||
- `public` — the filter opens it to anywhere; the proxy serves the public name; the certificate
|
|
||||||
comes from the public authority.
|
|
||||||
- `both` — both names, each from its own authority, and the filter opens to anywhere.
|
|
||||||
|
|
||||||
**An endpoint that is not routed is reached but never named.** An endpoint with no route contribution
|
|
||||||
yields filter rules and nothing else: no name is composed and no certificate is requested. Git over
|
|
||||||
ssh is that case, and it is the case the model could not express.
|
|
||||||
|
|
||||||
**The authority stops being chosen by which names happen to exist.** The proxy composes the names the
|
|
||||||
assignments asked for, and asks each name's own authority for it. A name nobody asked for is not
|
|
||||||
composed, so it is not certified.
|
|
||||||
|
|
||||||
**The two existing settings are this, completed.** The per-node port mapping becomes the endpoint's
|
|
||||||
binding. The per-node source override becomes its reach, widened from the filter alone to the names
|
|
||||||
and the certificate as well.
|
|
||||||
|
|
||||||
## Progressive insight — 2026-09-29, from building it
|
|
||||||
|
|
||||||
**Reach does not mean the same thing to the filter for an endpoint the proxy serves.** The decision
|
|
||||||
above says `internal` means "the filter opens the machine port to the private network" and `public`
|
|
||||||
means "the filter opens it to anywhere". For a routed endpoint the second half is wrong, and
|
|
||||||
[ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md) already said so before this
|
|
||||||
record was written: *a public service is exposed through the proxy, not by opening its own port* — it
|
|
||||||
listens `from: mesh`, only the proxy reaches it, and it is exposed by name.
|
|
||||||
|
|
||||||
Found by trying to express one real module, not by review. Its routed name must be public, because
|
|
||||||
browsers post to it; its machine-side port must not be, because that port serves the dashboard in
|
|
||||||
cleartext. Under one value driving both, saying "public" would have reopened a port an operator had
|
|
||||||
just closed. Measured the same evening: that module's routed name answered from the internet over TLS
|
|
||||||
while its machine-side port was refused from the same place. The port is not the path.
|
|
||||||
|
|
||||||
So the reach of a **routed** endpoint asks for names, and its port keeps what the manifest said. The
|
|
||||||
reach of an **unrouted** endpoint — git over ssh, a mail port, the bus — governs the port, because
|
|
||||||
there is no name and the port is the only way in. That is the same split this record already draws in
|
|
||||||
*an endpoint that is not routed is reached but never named*; what it got wrong was carrying the filter
|
|
||||||
across it.
|
|
||||||
|
|
||||||
This corrects a fact, not the decision: one statement per endpoint, three things derived from it and
|
|
||||||
none of them deciding on its own, all stand. The table in the decision should be read with the filter
|
|
||||||
column applying to an unrouted endpoint.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- **A manifest gains endpoint names, and a route contribution names an endpoint instead of a port.**
|
|
||||||
Every routed module's manifest changes. The word ships one release before any manifest uses it, and
|
|
||||||
reaches the build machine and the control plane first.
|
|
||||||
- **One derived value is read by three things** — the filter's rules, the proxy's contributions, the
|
|
||||||
certificate request — so they can no longer disagree, and a disagreement becomes a refusal at the
|
|
||||||
assignment rather than a surprise on a machine.
|
|
||||||
- **A name that must not be public becomes writable, and therefore checkable.** It also gives
|
|
||||||
[issue 129](../04-ISSUES/129-nothing-makes-a-machine-trust-the-meshs-authority/00-report.md) a
|
|
||||||
declared answer to read: which endpoints are internal is what says whose root must be installed
|
|
||||||
where.
|
|
||||||
- **[Issue 139](../04-ISSUES/139-an-internal-route-name-resolves-to-the-consumers-node/00-report.md)
|
|
||||||
becomes answerable**: the endpoint's assignment names the machine that serves it, which is the fact
|
|
||||||
the internal name should be composed from.
|
|
||||||
- **Reach becomes reportable.** The mesh can say, per endpoint, where it is reachable from and which
|
|
||||||
authority holds its certificate — neither of which `status` can say today.
|
|
||||||
- **This narrows [ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md).** Its
|
|
||||||
decision stands: the firewall is derived and host-applied, not a provider. What no longer holds is
|
|
||||||
that a listen's `from` is the whole of public-versus-internal; it is the filter's share of a
|
|
||||||
statement that also governs names and certificates.
|
|
||||||
- **What got harder:** every endpoint needs a name, including a module that serves exactly one port
|
|
||||||
and had no reason to name it. And an installation that wants a module public must now say so on the
|
|
||||||
assignment rather than inheriting it from the definition, which is more to say and the reason it is
|
|
||||||
right.
|
|
||||||
|
|
||||||
## How it is checked
|
|
||||||
|
|
||||||
- **One module, two endpoints, different reach.** A module declaring an internal endpoint and a
|
|
||||||
public one renders a filter opening one to the private network and one to anywhere, asserted per
|
|
||||||
chain body so a rule in the wrong chain cannot pass.
|
|
||||||
- **The names follow the reach.** The same module's routed endpoint composes the internal name only
|
|
||||||
when internal, the public name only when public, and both when both — and a certificate is
|
|
||||||
requested from the matching authority for each name composed and for no other. This fails against
|
|
||||||
the previous behaviour, where both names and both certificates are always composed, which is how
|
|
||||||
it is written.
|
|
||||||
- **An unrouted endpoint is filtered and never named.** Asserted for an endpoint with reach and no
|
|
||||||
route contribution: rules rendered, no contribution, no certificate request.
|
|
||||||
- **An assignment naming an endpoint the module does not declare is refused where it is said**, as is
|
|
||||||
a reach that is not one of the three — before it reaches a machine, because a ruleset that does not
|
|
||||||
load is a machine filtering nothing.
|
|
||||||
- **An assignment that states nothing renders byte-identically to today**, so every machine already
|
|
||||||
converged is untouched until its assignment says otherwise.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md) — narrowed here
|
|
||||||
- [ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md) — where reach belongs
|
|
||||||
- [ADR 0066](0066-public-routing-is-name-agnostic.md) — the public name this composes
|
|
||||||
- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) — why reach is not a definition's
|
|
||||||
- [issue 140](../04-ISSUES/140-an-endpoints-reach-is-not-declared/00-report.md) — the measurement
|
|
||||||
- [issue 139](../04-ISSUES/139-an-internal-route-name-resolves-to-the-consumers-node/00-report.md),
|
|
||||||
[issue 129](../04-ISSUES/129-nothing-makes-a-machine-trust-the-meshs-authority/00-report.md)
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
---
|
|
||||||
topic: what runs on it
|
|
||||||
status: superseded
|
|
||||||
date: 2026-09-28
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
extends: 02-DECISIONS/0137-a-machine-says-which-networks-it-routes.md
|
|
||||||
superseded-by: 02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 139. A network is forwarded because a module declared it
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
[ADR 0137](0137-a-machine-says-which-networks-it-routes.md), decided the same week, gave a machine a
|
|
||||||
way to say which networks it routes for its guests. It was written because the derived filter's
|
|
||||||
forward chain allowed two ranges named as constants in the control plane's source — the container
|
|
||||||
runtime's bridge pool, and part of the pool its compose files are given — with a comment admitting
|
|
||||||
the gap: *a machine whose runtime is configured with something else needs this to say so, which is a
|
|
||||||
thing the mesh cannot derive.*
|
|
||||||
|
|
||||||
**It can be derived, and from the right place.** Measured on the last machine still to be converged
|
|
||||||
([issue 141](../04-ISSUES/141-the-forward-chain-does-not-follow-the-modules/00-report.md)): twenty-one
|
|
||||||
container networks, nine inside the runtime's bridge pool, twelve in the other private range, and six
|
|
||||||
of those outside the constant's lower bound — so the flip would have cut their guests off exactly as
|
|
||||||
it did on the workstation that produced 0137.
|
|
||||||
|
|
||||||
Naming a range to cover the six is what 0137 provides for, and it is the wrong instrument. Of those
|
|
||||||
six networks, **four are networks the mesh's own modules declare**, present as network resources in
|
|
||||||
the node's plan and created by the host because a module asked for them. **Two are the predecessor's
|
|
||||||
leftovers** — compose networks of services the mesh does not run. Any range wide enough to keep the
|
|
||||||
four forwards the two as well: a firewall widened by hand to protect networks that should not exist.
|
|
||||||
|
|
||||||
The mesh already knows which of the twenty-one are its own, because it made them.
|
|
||||||
|
|
||||||
**And the node's configuration is meant to follow the modules assigned to it.** That is the mesh's
|
|
||||||
founding shape — the machine runs modules, and its files, its filter and its accounts are composed
|
|
||||||
from what runs there ([ADR 0005](0005-the-node-host.md),
|
|
||||||
[ADR 0010](0010-delivery.md),
|
|
||||||
[ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md)). The forward chain is the
|
|
||||||
one derived thing that consults a constant and a list a person types.
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
1. **Keep 0137 as it stands** — two constants plus a named list. Rejected: the list is written in
|
|
||||||
addresses, and addresses are what the runtime allocates, so the only entry safe enough to keep a
|
|
||||||
machine working is wider than the truth. It cannot distinguish a network the mesh made from one
|
|
||||||
left behind, which is the distinction that decides whether forwarding it is correct.
|
|
||||||
2. **Derive it from what the machine reports.** Still rejected, on 0137's own grounds: a test bed
|
|
||||||
creates its bridge between one declaration and the next, and a filter that follows whatever
|
|
||||||
appeared on a machine is a firewall that widens itself. **This decision is not that** — see below.
|
|
||||||
3. **Have the control plane allocate each module network's range from a pool it owns,** so it can
|
|
||||||
render the address itself. Rejected: more machinery for no gain. The runtime already allocates and
|
|
||||||
the host already knows, and taking allocation over means the mesh owning an address space it has no
|
|
||||||
other reason to own.
|
|
||||||
4. **Have each module declare its network's range.** Rejected by
|
|
||||||
[ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md): a definition names no address,
|
|
||||||
and the same definition runs on machines whose runtimes have allocated differently.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**A network is forwarded because a module declared it.** Per node, the forward chain forwards the
|
|
||||||
networks of the modules assigned there, and by default nothing else. A module unassigned stops being
|
|
||||||
forwarded at the next reconcile.
|
|
||||||
|
|
||||||
**The host resolves a declared network to its addresses.** A network resource carries a name; the
|
|
||||||
runtime allocates the subnet when the network is created. So the control plane declares *forward the
|
|
||||||
networks these modules asked for* and the host — which made them, and already resolves a container by
|
|
||||||
its name — renders the addresses. [ADR 0005](0005-the-node-host.md) holds: the host applies, it does
|
|
||||||
not decide.
|
|
||||||
|
|
||||||
**Deriving from the declaration is not deriving from the machine.** Both of 0137's objections fall
|
|
||||||
away. The set is known before the network exists, because a module declared it, so a network created
|
|
||||||
between two declarations is already in the one that asked for it. And it cannot widen itself: a
|
|
||||||
network nobody declared is never forwarded, however it appeared on the machine.
|
|
||||||
|
|
||||||
**The runtime's own default bridge is forwarded, from what the runtime reports.** Containers that name
|
|
||||||
no module network attach to it, and it belongs to the runtime rather than to any module — so the host
|
|
||||||
renders it from what the runtime says, not from a range named in the control plane. The constants go.
|
|
||||||
|
|
||||||
**What a machine says is for guests no module declares.** A test bed is not a module and its range is
|
|
||||||
not a module's; that is the case 0137's mechanism is for, and it keeps it — added to the derived set,
|
|
||||||
never replacing it, as 0137 decided. Narrowed to that, it is named for it.
|
|
||||||
|
|
||||||
**Their guests keep address and name service**, per declared network, unchanged from
|
|
||||||
[ADR 0137](0137-a-machine-says-which-networks-it-routes.md): the input chain admits that network's own
|
|
||||||
DHCP and DNS and nothing else.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- **The two constants are removed**, and with them the class of fault that a machine's guests depend
|
|
||||||
on a range that describes some other machine.
|
|
||||||
- **This is a behaviour change, not a refactor.** On the machine measured, the derived set and the
|
|
||||||
constant do not cover the same ground — that is the whole reason for the record. A machine whose
|
|
||||||
module networks happen to fall inside the old ranges renders the same rules.
|
|
||||||
- **A range that exists only to keep a leftover alive becomes visible as such**, because it will not
|
|
||||||
be in the derived set and has to be said out loud to survive.
|
|
||||||
- **`node networks` narrows** to guests no module declares, and the preview says which of a machine's
|
|
||||||
networks are the mesh's and which are not, so the difference is readable before a flip rather than
|
|
||||||
after.
|
|
||||||
- **A module's declaration gains nothing.** It already declares its network; what changes is that the
|
|
||||||
filter reads it.
|
|
||||||
- **What got harder:** the host renders part of the forward chain from what it created, so the
|
|
||||||
control plane no longer holds the whole rule set as text. The rule the mesh states is the set of
|
|
||||||
networks; the addresses are the machine's.
|
|
||||||
|
|
||||||
## How it is checked
|
|
||||||
|
|
||||||
- **Only declared networks are forwarded.** A node with two modules that declare networks renders
|
|
||||||
forward rules for exactly those two, and none for a third network present on the machine that no
|
|
||||||
module declared. This fails against the previous behaviour, which forwards by range and cannot tell
|
|
||||||
them apart, and that is how it is written.
|
|
||||||
- **Unassigning a module removes its network's rule** at the next reconcile, asserted on the rendered
|
|
||||||
chain rather than on the intent.
|
|
||||||
- **Guests of a declared network keep address and name service**, asserted per chain body so a line in
|
|
||||||
the wrong chain cannot pass — carried from 0137.
|
|
||||||
- **The runtime's own default bridge comes from the runtime**, asserted by rendering for a runtime
|
|
||||||
whose default bridge is somewhere other than the range the constant named.
|
|
||||||
- **A machine that names a range for guests no module declares still gets it**, added to the derived
|
|
||||||
set and not replacing it.
|
|
||||||
- **Each family is matched in its own syntax**, carried from 0137: one set holding both is a syntax
|
|
||||||
error, and a ruleset that does not load is a machine filtering nothing while its unit reports a
|
|
||||||
fault.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0137](0137-a-machine-says-which-networks-it-routes.md) — narrowed here; its mechanism keeps the
|
|
||||||
case it is right for
|
|
||||||
- [ADR 0005](0005-the-node-host.md) — the host applies; the addresses are the machine's
|
|
||||||
- [ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md) — the filter is derived from
|
|
||||||
what runs there
|
|
||||||
- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) — why a module does not name its
|
|
||||||
range
|
|
||||||
- [issue 141](../04-ISSUES/141-the-forward-chain-does-not-follow-the-modules/00-report.md) — the
|
|
||||||
measurement
|
|
||||||
- [issue 137](../04-ISSUES/137-converging-a-machine-cut-off-its-own-guests/00-report.md) — the
|
|
||||||
breakage that produced 0137
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
---
|
|
||||||
topic: what runs on it
|
|
||||||
status: accepted
|
|
||||||
date: 2026-09-28
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
extends: 02-DECISIONS/0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md
|
|
||||||
supersedes:
|
|
||||||
- 02-DECISIONS/0137-a-machine-says-which-networks-it-routes.md
|
|
||||||
- 02-DECISIONS/0139-a-network-is-forwarded-because-a-module-declared-it.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 140. The filter constrains what arrives from outside, and says nothing about a machine's own guests
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
The filter the mesh derives blocks traffic passing *through* a machine unless something allows it,
|
|
||||||
because a container's published port is traffic passing through rather than traffic arriving at the
|
|
||||||
machine itself ([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md),
|
|
||||||
[issue 047](../04-ISSUES/047-the-firewall-does-not-cover-published-container-ports/00-report.md)).
|
|
||||||
Having blocked all of it, the filter then had to let the machine's own containers reach outward again.
|
|
||||||
It does that by listing the address ranges those containers sit on.
|
|
||||||
|
|
||||||
As rendered on a converged workstation today:
|
|
||||||
|
|
||||||
```
|
|
||||||
policy drop
|
|
||||||
ct state established,related accept
|
|
||||||
ip saddr 172.16.0.0/12 accept
|
|
||||||
ip saddr 192.168.128.0/17 accept
|
|
||||||
ip saddr 10.0.0.0/8 accept
|
|
||||||
ip saddr 192.168.16.0/20 accept
|
|
||||||
... four more
|
|
||||||
```
|
|
||||||
|
|
||||||
Two of those ranges were constants in the control plane's source. The rest were typed by the operator
|
|
||||||
after [ADR 0137](0137-a-machine-says-which-networks-it-routes.md), which existed to make the typing
|
|
||||||
possible, because converging that workstation had cut every one of its containers off from the
|
|
||||||
internet and nothing reported a fault
|
|
||||||
([issue 137](../04-ISSUES/137-converging-a-machine-cut-off-its-own-guests/00-report.md)).
|
|
||||||
|
|
||||||
**The list is the mistake, not its contents.** Every attempt to make it correct fails the same way.
|
|
||||||
A constant describes one machine. A typed range goes stale, and cannot tell a network the mesh made
|
|
||||||
from one a predecessor left behind — measured on the control-node, where six such ranges fall outside
|
|
||||||
the constants and two of the six belong to services the mesh does not run
|
|
||||||
([issue 141](../04-ISSUES/141-the-forward-chain-does-not-follow-the-modules/00-report.md)).
|
|
||||||
[ADR 0139](0139-a-network-is-forwarded-because-a-module-declared-it.md) tried to generate the same
|
|
||||||
list from the modules and put half the rule set on the machine to do it. Three records, one list, and
|
|
||||||
the list should not exist.
|
|
||||||
|
|
||||||
**Because the mesh has no policy about a container reaching outward.** What the filter is for is
|
|
||||||
stated in [ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md): which port is open,
|
|
||||||
and to whom. That is about what arrives. A container of this machine's own opening a connection to
|
|
||||||
something else is not a port being opened to anybody, and enumerating the addresses it might do so
|
|
||||||
from is bookkeeping about the machine's internal plumbing, which the mesh neither owns nor can know.
|
|
||||||
|
|
||||||
**The system being replaced never had this fault, and its rule says why.** The chain still protecting
|
|
||||||
the control-node applies only to traffic arriving on that machine's outward link, and leaves
|
|
||||||
everything else alone. The mesh's filter dropped that distinction and replaced it with a list of
|
|
||||||
addresses.
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
1. **Keep the list and generate it better** — from the modules' declared networks, or from what the
|
|
||||||
machine reports. Rejected: [ADR 0139](0139-a-network-is-forwarded-because-a-module-declared-it.md)
|
|
||||||
is that, and it puts part of the rule set on the machine, which makes the rule set partly the
|
|
||||||
machine's and the derivation advisory.
|
|
||||||
2. **Name the guest links instead of their addresses, and allow only those.** Rejected as more than is
|
|
||||||
needed: it fails in the safe direction, but it is still a list that has to keep up with the
|
|
||||||
machine, and the thing it protects against — a container reaching outward — is not a thing the mesh
|
|
||||||
has a position on.
|
|
||||||
3. **Do not block traffic passing through at all.** Rejected: that is
|
|
||||||
[issue 047](../04-ISSUES/047-the-firewall-does-not-cover-published-container-ports/00-report.md),
|
|
||||||
where a published port was reachable from anywhere because no rule mentioned it.
|
|
||||||
4. **Constrain what arrives from outside, and nothing else.** Adopted.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**The filter constrains traffic arriving from outside the machine, and says nothing about traffic that
|
|
||||||
did not.** Traffic passing through the machine is allowed unless it arrived on one of the machine's
|
|
||||||
outward links, in which case it is allowed only where a declared endpoint's reach admits it
|
|
||||||
([ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md)). A container of this
|
|
||||||
machine's own reaching anywhere is not filtered, because the mesh has no position on it.
|
|
||||||
|
|
||||||
**A machine says which of its links face outside.** One node-level fact, reported by the machine the
|
|
||||||
way it already reports the kind of firewall it found and the tunnel it carries — not a setting, not a
|
|
||||||
list of addresses, and not something anybody types. It does not change when a module is added or
|
|
||||||
removed, which is what separates it from the list it replaces.
|
|
||||||
|
|
||||||
**A machine that has reported no outward link is sent no filter.** Rendering a rule around a link
|
|
||||||
whose name is not known produces a rule set that does not load, which is a machine filtering nothing
|
|
||||||
while its unit reports success. The refusal happens in the control plane, where a person reads it, and
|
|
||||||
the machine keeps the filter it already has.
|
|
||||||
|
|
||||||
**No addresses of the machine's own networks appear in the filter.** The two constants are removed and
|
|
||||||
`node networks` is removed with them, along with everything any machine was told to say through it.
|
|
||||||
Ports continue to follow the modules exactly as before: a module assigned to a machine opens the port
|
|
||||||
its assignment says it reaches on, and nothing about a network is said anywhere.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- **Three records collapse into one rule.** 0137 and 0139 are superseded. What 0137 was right about —
|
|
||||||
that converging a machine had silently cut off its own containers, and that nothing previewed it — is
|
|
||||||
answered by removing the cause rather than by giving the operator a way to compensate for it.
|
|
||||||
- **Every machine already converged loses its declared ranges and keeps working**, because the traffic
|
|
||||||
those ranges allowed is now allowed by not having arrived from outside. The workstation's five ranges
|
|
||||||
and the laptop's one are deleted rather than migrated.
|
|
||||||
- **A machine's test beds stop being a special case.** A bed's network is created while the machine
|
|
||||||
runs and was the case no list could cover; it is now covered by not being mentioned.
|
|
||||||
- **A new fact travels in the report**, and the control plane refuses to compose a filter without it,
|
|
||||||
so the order of the roll-out matters: the machines report before the control plane depends on it.
|
|
||||||
- **A machine with more than one outward link says so**, and a machine that acquires one while the mesh
|
|
||||||
is not looking is treated as internal until its next report. That window is the cost of this shape;
|
|
||||||
it is bounded by the report interval, and it exists on machines whose outward link changes, which
|
|
||||||
are the machines with nothing published to the outside.
|
|
||||||
- **What got harder:** nothing in the declaration, and one more thing a machine must be able to work
|
|
||||||
out about itself. A machine that cannot say which link faces outside cannot be given a filter.
|
|
||||||
|
|
||||||
## How it is checked
|
|
||||||
|
|
||||||
- **A machine's own container reaches outward with no network named anywhere.** A bed converges a
|
|
||||||
machine carrying containers on several networks, none of them mentioned in any setting, and each
|
|
||||||
reaches out afterwards. This fails against the previous behaviour, where the same flip cut them off,
|
|
||||||
and that is how it is written.
|
|
||||||
- **A port declared reachable from outside is reachable; one that is not, is not.** Probed from off the
|
|
||||||
machine's private network, for a published port and for an undeclared one, before and after the flip.
|
|
||||||
- **A network created after the filter was composed needs no new filter.** A network is made on the
|
|
||||||
machine after its last declaration and a container on it reaches out, with nothing re-sent.
|
|
||||||
- **No address of a machine's own networks appears in a rendered filter**, asserted on the text so a
|
|
||||||
range cannot creep back in.
|
|
||||||
- **A machine that reports no outward link is sent no filter, and the refusal names it** — asserted in
|
|
||||||
the control plane, and that the machine's existing filter is left alone.
|
|
||||||
- **A machine reporting two outward links has both constrained**, asserted per chain body so a rule
|
|
||||||
covering one and not the other cannot pass.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md) — what the filter is for
|
|
||||||
- [ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md) — what admits traffic
|
|
||||||
arriving from outside
|
|
||||||
- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md) — why traffic passing through is
|
|
||||||
filtered at all
|
|
||||||
- [ADR 0137](0137-a-machine-says-which-networks-it-routes.md),
|
|
||||||
[ADR 0139](0139-a-network-is-forwarded-because-a-module-declared-it.md) — superseded here
|
|
||||||
- [issue 137](../04-ISSUES/137-converging-a-machine-cut-off-its-own-guests/00-report.md),
|
|
||||||
[issue 141](../04-ISSUES/141-the-forward-chain-does-not-follow-the-modules/00-report.md)
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
---
|
|
||||||
topic: what runs on it
|
|
||||||
status: accepted
|
|
||||||
date: 2026-09-29
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
extends: 02-DECISIONS/0005-the-node-host.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 141. The host delivers its own successor, and versions live side by side
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
[Issue 142](../04-ISSUES/142-the-host-is-the-one-thing-the-mesh-does-not-deliver/00-report.md). A
|
|
||||||
merge builds every changed module and the control plane — which is itself a module — and the result
|
|
||||||
reaches the machines running it with nobody asking. The host is the exception: it is not a build
|
|
||||||
target, no declaration delivers it, and every machine in this mesh runs a byte-identical binary that
|
|
||||||
somebody built on a workstation and copied out.
|
|
||||||
|
|
||||||
The half that *recovers* from a bad host exists. `internal/upgrade` can tell that the executable this
|
|
||||||
process started from was replaced on disk, and it records which version last completed a reconcile.
|
|
||||||
The launcher counts consecutive failed starts, calls a rollback at the limit, and treats a clean exit
|
|
||||||
as the host standing aside so that the next loop runs whatever is on disk now. That supervision is
|
|
||||||
complete and correct.
|
|
||||||
|
|
||||||
Two things make it dead code:
|
|
||||||
|
|
||||||
- **`Replaced()` is called by nothing but its own tests.** Nothing tells the running host that a
|
|
||||||
successor is waiting.
|
|
||||||
- **The rollback resolves a version through the machine's package manager** — `pacman -U` from the
|
|
||||||
package cache. No machine here has the host installed as a package, so the recovery cannot run on
|
|
||||||
any of them; and being written in one package manager's terms, it cannot run on two of the three
|
|
||||||
operating systems the host is built for — [ADR 0005](0005-the-node-host.md) builds one binary per
|
|
||||||
operating system, pinned at link time.
|
|
||||||
|
|
||||||
**The record already points at the answer.** What is kept is a *version*, not a path. Keeping a
|
|
||||||
version is only useful to something that can choose between versions present on the machine, which is
|
|
||||||
what the package manager was being asked to do. The versions can simply be on disk.
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
1. **Deliver the host as a package, as the rollback assumes.** Rejected: it needs a package built and
|
|
||||||
a repository trusted per operating system, three of each, and the existing `package` resource
|
|
||||||
asserts presence and deliberately never a version — "version is the package manager's business and
|
|
||||||
the mesh does not hold a second opinion about it" — so it cannot ask for a particular host anyway.
|
|
||||||
Heaviest of the three and the only one that is different on every machine.
|
|
||||||
2. **Write the new binary over the running one.** Rejected on a fact: a running executable cannot be
|
|
||||||
truncated, and `archive` opens what it unpacks with `O_TRUNC`. It could be made to write and
|
|
||||||
rename, which is better hygiene and worth doing for its own sake, but it buys nothing here that
|
|
||||||
option 3 does not, and it leaves rollback with nowhere to go back to.
|
|
||||||
3. **Versions side by side; the newest retires the old.** Adopted.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**A host version is delivered as an archive into a directory named for it, and never over a running
|
|
||||||
one.** The declaration names it like any other archive — fetched by digest, the digest checked before
|
|
||||||
anything is unpacked. Nothing new travels, no new resource kind, and no change to how archives are
|
|
||||||
applied, because the path being written is not the path being executed.
|
|
||||||
|
|
||||||
**The launcher starts the most recently delivered version.** That is what "the newest" means: the
|
|
||||||
version whose directory arrived last. It reads no pointer and follows no link — the mesh creates no
|
|
||||||
links ([ADR 0012](0012-the-mesh-creates-no-symlinks.md)) — and the version is in the path, so nothing
|
|
||||||
has to be told what is running.
|
|
||||||
|
|
||||||
**The running host stands aside for a successor, and only between reconciles.** Finding a newer
|
|
||||||
version delivered, it finishes the reconcile it is in and exits cleanly. The launcher already reads a
|
|
||||||
clean exit as exactly this and starts what is on disk now. A host that stood aside mid-apply is the
|
|
||||||
half-configured machine this project exists to prevent, so the check happens at the boundary and
|
|
||||||
nowhere else.
|
|
||||||
|
|
||||||
**A version that completes a reconcile records itself, and retires what came before it.** The
|
|
||||||
known-good record is written as it is today. Then versions older than the one before the running one
|
|
||||||
are removed: the running version and its predecessor are kept, which is exactly what a rollback
|
|
||||||
needs, and nothing else accumulates.
|
|
||||||
|
|
||||||
**Rollback starts the previous version instead of reinstalling a package.** At the failure limit the
|
|
||||||
launcher pins the known-good version and starts that, once. The second failure is still a different
|
|
||||||
diagnosis — the previously working version does not run either, so it is the machine and not the
|
|
||||||
binary — and the halt is unchanged. No package manager, no package cache, and the same script on every
|
|
||||||
operating system.
|
|
||||||
|
|
||||||
**A machine says which host version it is running,** on the report it already sends, beside the other
|
|
||||||
facts it states about itself. Without it nothing can say a machine is behind, so "every machine
|
|
||||||
current with its source" cannot include the host.
|
|
||||||
|
|
||||||
## Progressive insight — 2026-09-29, the same day
|
|
||||||
|
|
||||||
**The delivery is not "nothing new", and this record said it was.** The decision above stands and is
|
|
||||||
built: versions side by side, the newest runs, the running host stands aside between reconciles, a
|
|
||||||
completed reconcile retires what is older than the predecessor, rollback picks a directory. What was
|
|
||||||
wrong was a claim about how a version reaches a machine. The paragraph on delivery said the
|
|
||||||
declaration "names it like any other archive… nothing new travels, no new resource kind"; the second
|
|
||||||
half is true and the first is not, because two things the delivery needs do not exist:
|
|
||||||
|
|
||||||
- **Nothing can compile it.** A `bundle` artifact is compiled by a closed list of toolchains —
|
|
||||||
typescript and python — whose own comment says adding a language is a decision, because a language
|
|
||||||
used by *modules* needs an SDK carrying the broker client, the event envelope and tool serving. The
|
|
||||||
host uses none of that: it is what applies modules, not one of them. So the obligation that list
|
|
||||||
warns about attaches to a module written in a language, not to the language being buildable, and
|
|
||||||
the control plane — also written in Go — is built as an image from a Dockerfile rather than through
|
|
||||||
a toolchain at all.
|
|
||||||
- **A version cannot reach the path.** An `archive` resource names a fixed path in the manifest, and
|
|
||||||
nothing interpolates the built version into it, so nothing can ask for
|
|
||||||
`…/versions/<version>/`.
|
|
||||||
|
|
||||||
Neither changes what was decided, which options were weighed, or any consequence: the shape is
|
|
||||||
unaffected and the host half is merged and tested. What it changes is the cost, which this record
|
|
||||||
understated as none. The remaining work is a way to build the host and a way to name a version in a
|
|
||||||
path, and until both exist nothing delivers a version and every machine takes the fallback — which is
|
|
||||||
what every machine does today.
|
|
||||||
|
|
||||||
> **Progressive insight — 2026-09-30. Both of those exist now.** The paragraph above named two missing
|
|
||||||
> things and they are built: a Go toolchain, based on a new `mesh-tools-go` module so the compiler is
|
|
||||||
> named and not pinned, and `${version}` in any value of a resource that uses an archive or a bundle.
|
|
||||||
> The mesh compiles its own host and publishes it to its own registry, measured — a statically linked
|
|
||||||
> stripped binary, fetched back out and run. **The cost was larger again than this note said**: three
|
|
||||||
> more things in the path assumed one language or one shape, and a fourth was in the base image.
|
|
||||||
> The account is [issue 142](../04-ISSUES/142-the-host-is-the-one-thing-the-mesh-does-not-deliver/01-progress.md).
|
|
||||||
>
|
|
||||||
> The version in a path is the artifact's **digest**, not the commit this note's own wording would
|
|
||||||
> suggest. Two builds of one commit are meant to be the same bytes, so a content-addressed version
|
|
||||||
> means an unchanged build keeps the path it had; a commit-named one would move for an identical binary
|
|
||||||
> and recreate everything reading it.
|
|
||||||
>
|
|
||||||
> **Still nothing delivers a version to a machine.** The host is a module and builds, and declares no
|
|
||||||
> resources, so the bundle sits in the registry and no machine is asked to take it. That is the next
|
|
||||||
> piece, and the decision above is unchanged by any of this.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- **The host becomes a build target and a module** — a module whose resource is the next host, applied
|
|
||||||
by the host that is running. The bootstrap is not circular because the two are different versions in
|
|
||||||
different directories.
|
|
||||||
- **Rollback becomes usable on every machine**, having been usable on none. It also stops being
|
|
||||||
written in one operating system's terms.
|
|
||||||
- **One copy by hand remains, once.** The first host that understands versioned directories cannot be
|
|
||||||
fetched by a host that does not. That copy is the last, and it is the honest cost of the change
|
|
||||||
rather than a step in the design.
|
|
||||||
- **Two versions occupy disk instead of one.** About nine megabytes. The predecessor is the price of a
|
|
||||||
rollback that does not depend on a cache somebody else may clean.
|
|
||||||
- **What got harder:** a host must now be able to find its own successor and to judge when it is safe
|
|
||||||
to stand aside. Both are between reconciles, which is the only moment the host is not mid-change.
|
|
||||||
- **A machine that is never told a newer version keeps running what it has**, indefinitely and
|
|
||||||
visibly, because its report says which version that is.
|
|
||||||
|
|
||||||
## How it is checked
|
|
||||||
|
|
||||||
- **A delivered version is run, and the old one is not.** A bed delivers a second version to a machine
|
|
||||||
running the first; the host exits between reconciles, the launcher starts the new one, and the
|
|
||||||
machine reports the new version. This fails against the previous behaviour, where nothing notices a
|
|
||||||
delivered version at all.
|
|
||||||
- **It stands aside between reconciles and never inside one.** Asserted by delivering a version while
|
|
||||||
an apply is in flight: the apply completes, and the exit follows it.
|
|
||||||
- **A version that will not start is rolled back to its predecessor, once**, and the second failure
|
|
||||||
halts with the machine named rather than the binary — asserted with no package manager involved.
|
|
||||||
- **A completed reconcile retires what is older than the predecessor**, and never the predecessor
|
|
||||||
itself, because that is what a rollback needs. Asserted on the directory afterwards.
|
|
||||||
- **The report names the running version**, asserted end to end rather than on the function that reads
|
|
||||||
it, since the point is that the control plane can tell a machine is behind.
|
|
||||||
- **The launcher picks the newest delivered version** with no pointer file and no link, asserted by
|
|
||||||
delivering two and checking which runs.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0005](0005-the-node-host.md) — the host, and what its supervision is for
|
|
||||||
- [ADR 0010](0010-delivery.md) — a declaration is owned resources; this adds no kind to it
|
|
||||||
- [ADR 0012](0012-the-mesh-creates-no-symlinks.md) — why the version is in the path
|
|
||||||
- [ADR 0005](0005-the-node-host.md), *it is built per operating system* — why a rollback written in
|
|
||||||
one package manager's terms was wrong for two of three
|
|
||||||
- [issue 142](../04-ISSUES/142-the-host-is-the-one-thing-the-mesh-does-not-deliver/00-report.md) — the
|
|
||||||
measurement
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
---
|
|
||||||
topic: the mesh
|
|
||||||
status: accepted
|
|
||||||
date: 2026-09-29
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
extends: 02-DECISIONS/0141-the-host-delivers-its-own-successor.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 142. The mesh delivers its own components as binaries, not as container images
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
Measured on the control-node, 2026-09-29:
|
|
||||||
|
|
||||||
| what | how it runs | publishes |
|
|
||||||
|---|---|---|
|
|
||||||
| host | a binary on the machine | — |
|
|
||||||
| controller, catalogue, builder, vault | containers | nothing |
|
|
||||||
| store, registry, broker | containers | ports |
|
|
||||||
|
|
||||||
**The mesh's own software is delivered two ways, and the difference is not a property of the
|
|
||||||
software.** The host and the controller are both written in the same language, both the mesh's own,
|
|
||||||
both doing the mesh's own work. One is an image fetched from a registry. The other is a file somebody
|
|
||||||
copied to four machines, owned by no package, built by nothing
|
|
||||||
([issue 142](../04-ISSUES/142-the-host-is-the-one-thing-the-mesh-does-not-deliver/00-report.md)).
|
|
||||||
|
|
||||||
**The reason is not a judgement about either, it is that images are the only delivery that works.**
|
|
||||||
There is no way to put a binary on a machine. The host is hand-copied because of that, and the
|
|
||||||
controller is an image because of that. Neither was chosen on its merits.
|
|
||||||
|
|
||||||
What it costs, all of it measured rather than argued:
|
|
||||||
|
|
||||||
- **Genesis must raise a container runtime before the control plane can exist.** The bundle carries
|
|
||||||
three images and one of them is the controller, *"in the bundle for the same reason they are: there
|
|
||||||
is nothing to fetch it with yet"*
|
|
||||||
([design 07](../03-DESIGN/01-to-be/07-the-foundation.md)). So the hardest moment in the mesh's life
|
|
||||||
has a prerequisite that the thing being started does not need.
|
|
||||||
- **Updating the control plane depends on the control plane.** Its image is fetched from the registry,
|
|
||||||
which is a container the controller manages.
|
|
||||||
- **A change to the host cannot be rolled out at all.** Every machine here runs a byte-identical
|
|
||||||
hand-copied binary. A change merged yesterday reached none of them.
|
|
||||||
- **Compiling the language the mesh is written in is not a capability of the builder.** The bundle
|
|
||||||
toolchains are typescript — real, with a registered base module — and python, which is named in the
|
|
||||||
list and absent from the catalogue. The controller is built as an image from a Dockerfile, which is
|
|
||||||
the per-repository incantation the bundle toolchain exists to abolish
|
|
||||||
([design 18](../03-DESIGN/01-to-be/18-building-a-module.md)).
|
|
||||||
|
|
||||||
The half that *receives* a binary safely is already built and tested
|
|
||||||
([ADR 0141](0141-the-host-delivers-its-own-successor.md)): versions side by side in directories named
|
|
||||||
for them, the newest run, the running one standing aside between reconciles, retirement keeping the
|
|
||||||
predecessor, and a rollback that chooses a directory. What is missing is everything that puts one
|
|
||||||
there.
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
1. **Leave it as it is.** Rejected: it is not a design, it is the reach of one mechanism. And it is
|
|
||||||
what makes a host change undeliverable.
|
|
||||||
2. **Containerise the host too**, so everything is delivered one way. Rejected: the host is what
|
|
||||||
starts the container runtime and what applies containers. A host in a container is the bootstrap
|
|
||||||
problem made total, and the machine would have no way back from a bad one.
|
|
||||||
3. **Deliver the mesh's components as operating-system packages.** Rejected for the reason
|
|
||||||
[ADR 0141](0141-the-host-delivers-its-own-successor.md) rejected it for the host: a package and a
|
|
||||||
trusted repository per operating system, three of each, and the `package` resource asserts presence
|
|
||||||
and deliberately never a version.
|
|
||||||
4. **Binaries for the mesh's own components, containers for third-party software.** Adopted.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**The mesh's own components are delivered as binaries on the machine.** The host, the controller, the
|
|
||||||
catalogue, the builder, the vault — the software this project writes. They are delivered by the
|
|
||||||
mechanism [ADR 0141](0141-the-host-delivers-its-own-successor.md) built: an archive, fetched by
|
|
||||||
digest, unpacked into a directory named for its version, with the running one standing aside between
|
|
||||||
reconciles and a rollback that chooses the predecessor.
|
|
||||||
|
|
||||||
**Third-party software stays a container.** The store, the registry, the broker. They are somebody
|
|
||||||
else's build, they are already adopted as modules
|
|
||||||
([ADR 0078](0078-the-store-and-broker-are-modules.md)), and an image is the right way to carry
|
|
||||||
somebody else's software. **The container runtime remains required** — modules use it — so this
|
|
||||||
removes a dependency from the control plane, not from the machine.
|
|
||||||
|
|
||||||
**The builder compiles the languages the mesh is written in.** A toolchain for Go, with a base module
|
|
||||||
providing the compiler, exactly as typescript has. The obligation the toolchain list warns about — an
|
|
||||||
SDK carrying the broker client, the envelope and tool serving — attaches to a *module* written in a
|
|
||||||
language, not to the language being compilable. None of these components is a module in that sense;
|
|
||||||
the host is what applies modules.
|
|
||||||
|
|
||||||
**An artifact says what it targets.** A compiled binary is per operating system, pinned at link time
|
|
||||||
([ADR 0005](0005-the-node-host.md)), and a toolchain deliberately takes nothing from the module,
|
|
||||||
because anything a module could override there it would be writing a Dockerfile to override. So the
|
|
||||||
target is a property of the artifact rather than of the recipe, and one artifact declared per target
|
|
||||||
is one build each.
|
|
||||||
|
|
||||||
**A component's version comes from where it sits, not from its linker.** It is unpacked into a
|
|
||||||
directory named for its version, so it can read its own version from its path. The stamp goes, and
|
|
||||||
with it the need for a build to know what it will be called.
|
|
||||||
|
|
||||||
**Genesis carries a binary reference where it carried an image reference.** The principle does not
|
|
||||||
change — the bundle names a thing by digest and the host fetches it, pinned because nothing can
|
|
||||||
resolve a version when no mesh exists — and the container runtime stops being a prerequisite for the
|
|
||||||
control plane. It stays a prerequisite for the store and the broker, which is where it belongs.
|
|
||||||
|
|
||||||
**The order is staged, and each step stands alone.** Compiling Go; an artifact naming its target;
|
|
||||||
delivering a binary; the host as the first component delivered; the controller, catalogue, builder and
|
|
||||||
vault out of their containers; genesis last. Genesis is last for the reason it is always last: it
|
|
||||||
matters for a machine nobody has yet, and every earlier step is provable on a mesh that exists.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- **One delivery for the mesh's own software**, so a change to the host ships the way a change to the
|
|
||||||
controller does, and neither is copied by hand.
|
|
||||||
- **The control plane stops depending on a container runtime and on its own registry.** Both remain on
|
|
||||||
the machine for other reasons; neither gates the control plane's own life any more.
|
|
||||||
- **`Replaced()`, the known-good record and the launcher's rollback stop being dead code.** They were
|
|
||||||
written for this and have been called by nothing but their tests.
|
|
||||||
- **Four more components gain a rollback they do not have.** Today a bad controller image is recovered
|
|
||||||
by an operator; under this it is recovered the way a bad host is.
|
|
||||||
- **Two versions of each component occupy disk.** Around nine megabytes each. The predecessor is what a
|
|
||||||
rollback needs.
|
|
||||||
- **Genesis gets smaller, not larger.** One fewer image to carry and one fewer runtime to raise before
|
|
||||||
the control plane.
|
|
||||||
- **This does not make the components smaller or simpler.** They are the same programs; what changes is
|
|
||||||
how they arrive. A reader expecting the containers to have been hiding complexity will not find any.
|
|
||||||
- **What got harder:** the builder gains a language, artifacts gain a target, and the mesh gains a
|
|
||||||
second kind of thing it must deliver correctly — one where getting it wrong takes the control plane
|
|
||||||
down rather than a module. That is why the host is first: it is the component whose recovery is
|
|
||||||
already built and tested.
|
|
||||||
|
|
||||||
## How it is checked
|
|
||||||
|
|
||||||
- **A component is delivered and runs, with nothing copied by hand.** A bed builds the host from its
|
|
||||||
repository, delivers it to a machine running an older one, and the machine reports the new version.
|
|
||||||
This fails today at the first step, because nothing builds it.
|
|
||||||
- **Each target is built once and only the matching one is delivered.** Asserted by declaring an
|
|
||||||
artifact per operating system and checking that a machine is offered the one it can run — a host
|
|
||||||
built for another is what ADR 0005's link-time pin exists to refuse.
|
|
||||||
- **A component reads its version from its path**, asserted by unpacking the same bytes into two
|
|
||||||
differently named directories and seeing each report its own.
|
|
||||||
- **A bad component is rolled back without an operator**, for the host first: a version that will not
|
|
||||||
start is replaced by its predecessor once, and the second failure halts naming the machine.
|
|
||||||
- **The control plane comes up with no registry reachable**, which is the dependency this removes —
|
|
||||||
asserted by raising it with the registry stopped.
|
|
||||||
- **Genesis raises a control plane with no container runtime running**, and raises the store and the
|
|
||||||
broker afterwards. Last, and on a machine with nothing on it.
|
|
||||||
- **A published port count that does not change.** The mesh's own components publish nothing today, so
|
|
||||||
moving them out of containers must not open anything — asserted on the machine's reachable set before
|
|
||||||
and after, which the converge preview already reads.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0141](0141-the-host-delivers-its-own-successor.md) — the receiving half, already built
|
|
||||||
- [ADR 0005](0005-the-node-host.md) — the host, its supervision, and one binary per operating system
|
|
||||||
- [ADR 0078](0078-the-store-and-broker-are-modules.md) — why third-party software stays a container
|
|
||||||
- [ADR 0006](0006-the-substrate-and-the-control-plane.md) — what genesis must raise, and in what order
|
|
||||||
- [issue 142](../04-ISSUES/142-the-host-is-the-one-thing-the-mesh-does-not-deliver/00-report.md) — the
|
|
||||||
measurement that started this
|
|
||||||
- [design 07](../03-DESIGN/01-to-be/07-the-foundation.md) — the bundle's three images, one of them the
|
|
||||||
controller
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
---
|
|
||||||
topic: what runs on it
|
|
||||||
status: superseded
|
|
||||||
date: 2026-09-29
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
extends: 02-DECISIONS/0010-delivery.md
|
|
||||||
superseded-by: 02-DECISIONS/0144-anything-on-a-machine-may-call-anything-on-it.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 143. A consumer verifies the grant it is given
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
A **grant** is what the mesh writes on a consumer's machine so it can reach a provider. The real one
|
|
||||||
the forge receives for its database, as it arrives:
|
|
||||||
|
|
||||||
```
|
|
||||||
provision postgres-database
|
|
||||||
at <the provider's machine, by name>
|
|
||||||
port the machine port the provider is published on
|
|
||||||
as the role the provider created for this consumer
|
|
||||||
```
|
|
||||||
|
|
||||||
with the credential sealed in a separate file. Four facts and a password, and they are the whole
|
|
||||||
mechanism by which anything in the mesh reaches anything else.
|
|
||||||
|
|
||||||
**The mesh asserts that claim and never finds out whether it is true.**
|
|
||||||
[Issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md):
|
|
||||||
converging a machine dropped the path from a container to a port on its own machine, and for eleven
|
|
||||||
hours the mesh answered *all doing what they were told, all heard from, every module current with its
|
|
||||||
source* while a web application logged, six thousand times:
|
|
||||||
|
|
||||||
```
|
|
||||||
connection to server at "<the machine>" (10.10.0.1), port 6852 failed: timeout expired
|
|
||||||
```
|
|
||||||
|
|
||||||
Every check the mesh makes passed, because every check it makes is about the relationship between the
|
|
||||||
mesh and a machine: the declaration was applied, the digest matched, every container named was running.
|
|
||||||
None of them asks whether a consumer can reach what it requires — though the mesh composed the grant
|
|
||||||
and therefore knows the consumer, the machine, the address, the port and the credential.
|
|
||||||
|
|
||||||
**And where the check runs decides whether it catches anything.** The rule in force admitted the
|
|
||||||
machines' own addresses on the private network. A dial from the *machine* to its own address carries
|
|
||||||
exactly such a source address, so a check run by the host on its own behalf would have matched that rule
|
|
||||||
and passed — while every container on the machine was refused. This is inference from the rule that was
|
|
||||||
loaded, not a measurement: the fault was found and fixed before anyone thought to dial from the host.
|
|
||||||
It is enough to decide the question, because a check whose position differs from the consumer's is
|
|
||||||
testing something nobody asked about.
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
1. **The control plane dials each provision.** Rejected, and it is the tempting one because the control
|
|
||||||
plane holds every fact. It sits on the provider's machine for most provisions here and reaches the
|
|
||||||
address by a path no consumer uses; in the measured outage it would have passed throughout.
|
|
||||||
2. **The host dials on the consumer's behalf, from the machine.** Rejected for the reason above: the
|
|
||||||
machine's network position is not the consumer's, and the one outage this exists to catch is exactly
|
|
||||||
a difference between them.
|
|
||||||
3. **Ask the module.** Rejected: a module is arbitrary software that the mesh does not write. Some could
|
|
||||||
report on their provisions and most cannot, and a check that covers the modules that opted in tells
|
|
||||||
nobody anything about the rest.
|
|
||||||
4. **Read the module's logs.** Rejected: the failure was in a log the whole time, and reading a module's
|
|
||||||
logs makes the mesh depend on the wording of software it does not control.
|
|
||||||
5. **The consumer verifies it, from its own network position.** Adopted.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**A consumer verifies each grant it is given, from its own network position.** After a reconcile has
|
|
||||||
applied a grant, the machine opens a connection to the address and port that grant names, from inside
|
|
||||||
the consumer's own network namespace — the same position the consumer's software dials from, which is
|
|
||||||
the only position that answers the question the grant asks.
|
|
||||||
|
|
||||||
**It is a connection, not a conversation.** Whether the port accepts a connection is what a grant
|
|
||||||
claims; whether the credential is right, the role exists or the schema is current is the provider's to
|
|
||||||
answer and the consumer's to discover. A check that spoke each provision's protocol would be a second
|
|
||||||
implementation of every provision, and would fail for reasons that are not the mesh's.
|
|
||||||
|
|
||||||
**One failure is not news.** A provider restarting is ordinary, and so is a consumer between containers.
|
|
||||||
A grant is reported unreachable only after it has failed on **consecutive** reconciles, and the count is
|
|
||||||
what the machine reports rather than the last attempt — so a reader can tell "it was briefly away" from
|
|
||||||
"it has never worked".
|
|
||||||
|
|
||||||
**A grant that cannot be checked is said to be unchecked, never assumed good.** A consumer that is not
|
|
||||||
running has no network position to dial from; that is not a broken grant and must not read as one. It is
|
|
||||||
also not a verified grant, and the two are different sentences.
|
|
||||||
|
|
||||||
**What it costs to be wrong is the constraint on all of it.** A check that reports a working provision
|
|
||||||
broken trains a reader to ignore the report, which is worse than having none — the fault this
|
|
||||||
repository keeps finding, one level up. So the threshold is consecutive failures, the check is the
|
|
||||||
cheapest thing that answers the question, and an unknown is reported as unknown.
|
|
||||||
|
|
||||||
**The mesh says it where it says everything else.** A machine's report carries its unreachable grants,
|
|
||||||
and `status` names them beside what is out of date — so "every module current with its source" stops
|
|
||||||
being the whole of what the mesh will tell you about a machine whose modules cannot reach each other.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- **The mesh can be wrong out loud.** It has been able to assert a grant and not check it; now a grant
|
|
||||||
that does not work is a thing the mesh says, and the eleven hours of issue 145 become minutes.
|
|
||||||
- **The host gains the ability to act from a container's network position**, which it has not needed
|
|
||||||
before. That is a real capability and the only one this needs.
|
|
||||||
- **A machine reports something that is not about the declaration.** Everything it reports today is
|
|
||||||
what it applied and what it holds; this is the first thing it says about whether what it applied
|
|
||||||
works.
|
|
||||||
- **A provision with no port is not checked**, because there is nothing to dial. Several are files and
|
|
||||||
secrets, and saying "checked" about those would be the appearance of verification that this record
|
|
||||||
exists to remove.
|
|
||||||
- **What got harder:** a reconcile does more than apply. Every grant adds a connection attempt on a
|
|
||||||
cadence, which is cheap individually and worth naming: a machine with many consumers dials once per
|
|
||||||
grant per reconcile.
|
|
||||||
|
|
||||||
## How it is checked
|
|
||||||
|
|
||||||
- **The outage is caught.** A bed drops the path from a consumer's network position to a provider's
|
|
||||||
port while leaving the machine's own path to it open — the exact shape of issue 145 — and the grant
|
|
||||||
reads unreachable. This fails against the previous behaviour, where nothing reported anything, and
|
|
||||||
against a check run from the machine, which passes while the consumer cannot reach it.
|
|
||||||
- **A restarting provider is not an outage.** One failed reconcile reports nothing; the count rises and
|
|
||||||
falls, and the grant reads reachable again without anybody acting.
|
|
||||||
- **A consumer that is not running reads unchecked, not broken**, asserted separately from the
|
|
||||||
unreachable case because they are different sentences.
|
|
||||||
- **A provision with no port is not claimed to be checked.**
|
|
||||||
- **The report carries the count, not the last attempt**, so "briefly away" and "never worked" are
|
|
||||||
distinguishable by a reader who sees only the report.
|
|
||||||
- **`status` names an unreachable grant**, asserted on the output, since a check nothing surfaces is
|
|
||||||
the same as no check.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0010](0010-delivery.md) — the declaration is owned resources; a grant is one of them
|
|
||||||
- [ADR 0009](0009-modules-and-the-graph.md) — what a provision and a consumer are
|
|
||||||
- [issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md)
|
|
||||||
— the eleven hours
|
|
||||||
- [issue 136](../04-ISSUES/136-a-module-may-name-a-program-the-machine-does-not-have/00-report.md) — the
|
|
||||||
same distance between a declaration and a machine, one level down
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
---
|
|
||||||
topic: what runs on it
|
|
||||||
status: accepted
|
|
||||||
date: 2026-09-29
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
extends: 02-DECISIONS/0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md
|
|
||||||
supersedes: 02-DECISIONS/0143-a-consumer-verifies-the-grant-it-is-given.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 144. Anything on a machine may call anything on it, and that is the whole of "local"
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
Everything in the mesh should be able to call:
|
|
||||||
|
|
||||||
- what runs on the same machine;
|
|
||||||
- another machine's service over the private network, if that service is exposed there;
|
|
||||||
- another machine's service over the public network, if it is exposed there.
|
|
||||||
|
|
||||||
Three cases. The filter had two of them.
|
|
||||||
|
|
||||||
**The first was broken and the break was invisible.** A service exposed to the private network rendered
|
|
||||||
as the machines' own addresses on it. A caller on the machine carries such an address; a caller inside
|
|
||||||
one of that machine's containers carries a bridge address and matched nothing. Measured:
|
|
||||||
|
|
||||||
```
|
|
||||||
the machine: local 10.10.0.1 dev lo src 10.10.0.1
|
|
||||||
a container: 10.10.0.1 via 172.17.0.1 dev eth0 src 172.17.0.8
|
|
||||||
```
|
|
||||||
|
|
||||||
Same destination, same machine, two source addresses. The rule named the first and silently refused the
|
|
||||||
second, so a module reaching its database on its own machine's name timed out for eleven hours
|
|
||||||
([issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md)).
|
|
||||||
|
|
||||||
**The second case works, and by accident.** A caller on another machine reaches the private network over
|
|
||||||
the tunnel, and arrives carrying that machine's own address — so the rule matches. It would not have
|
|
||||||
matched the caller's own address either; the tunnel rewrites it. That two of three cases worked is why
|
|
||||||
this looked correct.
|
|
||||||
|
|
||||||
**[ADR 0143](0143-a-consumer-verifies-the-grant-it-is-given.md) answered the wrong question.** Written
|
|
||||||
hours earlier, it proposed that a consumer verify each grant it is given by opening a connection from
|
|
||||||
its own network position — and it went to some length about *which* position, because whether a caller
|
|
||||||
sat in a container changed the answer. That difference was the bug. A verification mechanism would have
|
|
||||||
reported this outage sooner and would not have prevented it, and the machinery it needed existed only
|
|
||||||
because the rule was wrong. The remedy for a configuration error is the correct configuration.
|
|
||||||
|
|
||||||
**And a module is not a container.** A module is software that delivers one or more services, and it may
|
|
||||||
do that as a container, an installed package with a unit, a binary, or files something else reads. Of 72
|
|
||||||
modules in the catalogue, 61 happen to use a container and 11 do not — among them the resolver, the ssh
|
|
||||||
daemon and the intrusion-prevention module. A rule that reasons about containers describes most of the
|
|
||||||
mesh and not the mesh.
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
1. **A line per service admitting the machine's own callers.** Rejected: it is what was written first,
|
|
||||||
and it only ever covers the services somebody remembered to think about. It also states, service by
|
|
||||||
service, a thing that is true of the machine.
|
|
||||||
2. **Verify each grant from the consumer's position** ([ADR 0143](0143-a-consumer-verifies-the-grant-it-is-given.md)).
|
|
||||||
Rejected as a remedy: it observes the fault rather than removing it, and the question it agonised over
|
|
||||||
— which network position — exists only while the fault does.
|
|
||||||
3. **Enumerate the addresses a machine's callers may have.** Rejected for the reason no address is named
|
|
||||||
anywhere in this filter any more ([ADR 0140](0140-the-filter-constrains-what-arrives-from-outside.md)):
|
|
||||||
a range describes one machine and goes stale in silence.
|
|
||||||
4. **Local is not filtered, stated once.** Adopted.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**Anything on a machine may call anything on that machine, and the filter says so once.** Not per
|
|
||||||
service, not per port, and not by naming who the callers are: traffic that did not arrive from outside
|
|
||||||
the machine and did not arrive over the private network is the machine's own, and is admitted. It is
|
|
||||||
asked by the link the traffic arrived on, because that is a fact about the machine rather than a list
|
|
||||||
that describes one.
|
|
||||||
|
|
||||||
**Local is not a boundary this mesh draws.** Whether a caller is a container, a unit, or the operator's
|
|
||||||
shell changes nothing, because the thing being decided is "is this the same machine" and the answer does
|
|
||||||
not depend on the form the caller takes.
|
|
||||||
|
|
||||||
**The other two cases are unchanged and are now legible beside it.** A service exposed to the private
|
|
||||||
network admits the machines on it; a service exposed publicly admits anything. Three cases, three lines,
|
|
||||||
and a reader can see all three at once.
|
|
||||||
|
|
||||||
**[ADR 0143](0143-a-consumer-verifies-the-grant-it-is-given.md) is superseded and nothing replaces it.**
|
|
||||||
Whether the mesh should check that a grant works is a real question — it reported this machine healthy
|
|
||||||
for eleven hours — but it is a question about what the mesh can say, not about what it should do, and it
|
|
||||||
must stand on its own rather than as the remedy for a rule that was wrong. It is not built.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- **The three things everything should be able to call are three lines**, and the first is one line
|
|
||||||
rather than one per service, so a service added tomorrow is reachable locally without anybody
|
|
||||||
remembering to say so.
|
|
||||||
- **A form of module stops mattering to the filter.** The 11 modules that are not containers were never
|
|
||||||
affected by this bug and were never the reason it was hard to see; they are the reason the rule should
|
|
||||||
never have mentioned containers.
|
|
||||||
- **The mesh still cannot say when a grant stops working.** That is the live gap, recorded in issue 145
|
|
||||||
and no longer pretending to have an answer.
|
|
||||||
- **What got harder:** nothing. This removes a line per service and replaces it with one.
|
|
||||||
|
|
||||||
## How it is checked
|
|
||||||
|
|
||||||
- **A caller on the machine reaches a service on it, in the input chain**, asserted on that chain's own
|
|
||||||
body — because the forward chain carries the same line in the same words, and an assertion on the
|
|
||||||
whole rendered file passed with the input chain's copy deleted. That is what
|
|
||||||
[ADR 0137](0137-a-machine-says-which-networks-it-routes.md)'s tests already say to do.
|
|
||||||
- **It is one rule, not one per service.** Asserted by rendering two services of different reach and
|
|
||||||
refusing a per-port local line.
|
|
||||||
- **The three reaches render as three lines**, asserted together, so the whole of what the filter says
|
|
||||||
about who may call what is one test.
|
|
||||||
- **The measured case:** from a container on the machine, a service exposed to the private network on
|
|
||||||
that machine answers. This is the outage, and it fails against the rule this replaces.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0045](0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md) — the filter is the sum
|
|
||||||
of what its modules listen on
|
|
||||||
- [ADR 0140](0140-the-filter-constrains-what-arrives-from-outside.md) — why no address is named
|
|
||||||
- [ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md) — internal and public,
|
|
||||||
the other two cases
|
|
||||||
- [ADR 0143](0143-a-consumer-verifies-the-grant-it-is-given.md) — superseded here
|
|
||||||
- [issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md)
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
---
|
|
||||||
topic: what runs on it
|
|
||||||
status: superseded
|
|
||||||
date: 2026-09-29
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
extends: 02-DECISIONS/0144-anything-on-a-machine-may-call-anything-on-it.md
|
|
||||||
superseded-by: 02-DECISIONS/0146-connectivity-is-checked-by-name-per-hosting-form.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 145. A module checks what the mesh claims is reachable, and it checks itself
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
The mesh asserts three things are callable ([ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md)):
|
|
||||||
what runs on the same machine, another machine's service exposed to the private network, and another
|
|
||||||
machine's service exposed publicly. It has never checked any of them.
|
|
||||||
|
|
||||||
[Issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md):
|
|
||||||
the first of the three was broken for eleven hours and the mesh answered *all heard from, every module
|
|
||||||
current with its source* throughout. Every check it makes is about the relationship between the mesh and
|
|
||||||
a machine — applied, current, containers running — and none about whether anything can reach anything.
|
|
||||||
|
|
||||||
**A first answer was drafted and withdrawn.** [ADR 0143](0143-a-consumer-verifies-the-grant-it-is-given.md)
|
|
||||||
put the check inside the host, verifying each grant from the consumer's network position. It was
|
|
||||||
superseded because the difference it worked so hard to reproduce — whether a caller sat in a container —
|
|
||||||
was the bug itself. What survives from it is the part that was right: a check run from the wrong place
|
|
||||||
proves nothing, and the mesh's own reports are not evidence about the network.
|
|
||||||
|
|
||||||
**The mesh already has the shape for this and it is a module.** A module can declare a container that
|
|
||||||
runs on a cadence ([ADR 0053](0053-a-step-that-runs-on-a-schedule.md), and three modules already use
|
|
||||||
`*/5 * * * *`), can be given the mesh's roster as a rendered fact — every machine's name, address and
|
|
||||||
this node's own identity, the same mechanism the resolver and the operator's ssh configuration use — and
|
|
||||||
can emit what it found on the bus. Nothing new is needed to build this except the module.
|
|
||||||
|
|
||||||
**What it must not check is the trap.** The obvious probe target is ssh: present on every machine, never
|
|
||||||
closed by design. Dialling it would have passed throughout the outage, because ssh is admitted
|
|
||||||
unconditionally and the thing that broke was a service exposed to the private network. A checker whose
|
|
||||||
probe is unconditionally open measures the one path that cannot fail, which is the failure this whole
|
|
||||||
sequence keeps producing — a check that reads as verification and verifies nothing.
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
1. **The host verifies each grant** ([ADR 0143](0143-a-consumer-verifies-the-grant-it-is-given.md)).
|
|
||||||
Superseded. It needed the host to act from another network position, which is machinery that exists
|
|
||||||
only while local calls are filtered wrongly.
|
|
||||||
2. **The control plane dials every node.** Rejected: it sits on one machine and reaches the others by a
|
|
||||||
path no ordinary caller uses. It would have passed throughout the outage.
|
|
||||||
3. **Probe an existing service.** Rejected for the target problem above: the services guaranteed on every
|
|
||||||
machine are the ones that are never closed, so they cannot fail the way the mesh fails.
|
|
||||||
4. **A module on every machine that serves its own probe and dials the others'.** Adopted.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**A module runs on every machine, serves an endpoint of its own, and dials every other machine's.** The
|
|
||||||
probe is the module's own endpoint, declared reachable over the private network — so the thing being
|
|
||||||
dialled is admitted by exactly the rule that governs every other internally-exposed service, and fails
|
|
||||||
when that rule is wrong. A second endpoint, declared public, does the same for the public path where a
|
|
||||||
machine has one.
|
|
||||||
|
|
||||||
**It checks the three cases the mesh claims, by name:**
|
|
||||||
|
|
||||||
- its **own machine**, by dialling its own machine's address — the case that broke, and the only one that
|
|
||||||
distinguishes a caller on the machine from a caller in one of its containers;
|
|
||||||
- **each other machine over the private network**;
|
|
||||||
- **each machine's public path**, where one is recorded.
|
|
||||||
|
|
||||||
**It resolves before it dials, and says which failed.** A name that does not resolve and a port that does
|
|
||||||
not answer are different faults with different owners, and a checker that reports one sentence for both
|
|
||||||
sends a reader to the wrong place.
|
|
||||||
|
|
||||||
**It runs where the callers run.** The module's own code in its own container, on the cadence the mesh
|
|
||||||
already has, from the same position as every other module on that machine. It is not the host and not the
|
|
||||||
control plane, and that is the whole point.
|
|
||||||
|
|
||||||
**It says what it found and nothing else.** It emits results; it repairs nothing, opens nothing and holds
|
|
||||||
no credential beyond its own. A checker that fixes things is a second control plane.
|
|
||||||
|
|
||||||
**One failure is not a fault.** A machine rebooting is ordinary. A path is reported broken after it has
|
|
||||||
failed on consecutive runs, and the count travels with the result so a reader can tell "briefly away"
|
|
||||||
from "never worked" — the one thing [ADR 0143](0143-a-consumer-verifies-the-grant-it-is-given.md) got
|
|
||||||
right and worth keeping.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- **The mesh gains the ability to be wrong out loud about the network.** Eleven hours becomes two runs.
|
|
||||||
- **It is a module, so it is assigned, built, pushed and reported on like everything else** — no new host
|
|
||||||
capability, no new vocabulary, nothing in the control plane that has to know about checking.
|
|
||||||
- **Its own endpoint is the instrument.** That is what makes it able to fail; it also means the checker
|
|
||||||
must be assigned to a machine before that machine can be checked, and a machine without it is
|
|
||||||
unchecked rather than healthy.
|
|
||||||
- **It cannot check what it cannot be told.** The roster gives it machines; it does not give it every
|
|
||||||
module's endpoints, so this checks the paths the mesh claims and not every grant in the mesh. That is
|
|
||||||
the honest scope of a first one, and the difference is worth saying rather than growing quietly.
|
|
||||||
- **What got harder:** one more module on every machine, and a module whose whole purpose is to fail
|
|
||||||
visibly when something else is wrong. Its own failures will be read as the mesh's, which is the cost of
|
|
||||||
an instrument.
|
|
||||||
|
|
||||||
## How it is checked
|
|
||||||
|
|
||||||
- **It catches the measured outage.** A bed closes the path from a container to a service exposed to the
|
|
||||||
private network on its own machine — issue 145's shape — and the checker reports its own machine
|
|
||||||
unreachable while every other path still reads reachable. This fails against a probe on a port that is
|
|
||||||
never closed, which is the wrong target this record exists to name.
|
|
||||||
- **A machine rebooting is not a fault**: one failed run reports nothing, the count rises and falls.
|
|
||||||
- **A name that does not resolve is reported as that**, not as a port that did not answer.
|
|
||||||
- **It reports and does not act**: asserted by giving it a broken path and checking nothing on the machine
|
|
||||||
changed.
|
|
||||||
- **A machine without the module reads unchecked**, never healthy — asserted on what the mesh says about
|
|
||||||
a machine it is not assigned to.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md) — the three things that must be callable
|
|
||||||
- [ADR 0143](0143-a-consumer-verifies-the-grant-it-is-given.md) — superseded; what survives is that the
|
|
||||||
position matters
|
|
||||||
- [ADR 0053](0053-a-step-that-runs-on-a-schedule.md) — the cadence
|
|
||||||
- [ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md) — internal and public,
|
|
||||||
which the probe endpoints declare
|
|
||||||
- [issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md)
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
---
|
|
||||||
topic: what runs on it
|
|
||||||
status: accepted
|
|
||||||
date: 2026-09-29
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
extends: 02-DECISIONS/0145-a-module-checks-what-the-mesh-claims-is-reachable.md
|
|
||||||
supersedes: 02-DECISIONS/0145-a-module-checks-what-the-mesh-claims-is-reachable.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 146. Connectivity is checked by name, per hosting form, with a valid certificate
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
[ADR 0145](0145-a-module-checks-what-the-mesh-claims-is-reachable.md) decided that a module checks what
|
|
||||||
the mesh claims is reachable, from where the callers are, because the mesh reported four machines healthy
|
|
||||||
for eleven hours while a module could not reach its database
|
|
||||||
([issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md)).
|
|
||||||
That decision stands. What it got wrong is everything about *what* is dialled.
|
|
||||||
|
|
||||||
It dialled a raw port on each machine's address. Three things are wrong with that:
|
|
||||||
|
|
||||||
- **A raw port is not how anything in this mesh is reached.** A real caller resolves a name, the proxy
|
|
||||||
answers it, and the proxy reaches the service. A check that dials a port tests the last hop of a path
|
|
||||||
with four hops in it, and the three it skips — resolution, the proxy, the certificate — are where most
|
|
||||||
of the mesh's connectivity actually lives.
|
|
||||||
- **It tested one hosting form.** A module is software that delivers services, and it may deliver them
|
|
||||||
from a container, from a unit the mesh writes for its own code, or from a unit a package ships. Those
|
|
||||||
are three different paths to the same machine, and the outage that produced this was two of them
|
|
||||||
disagreeing. A probe served one way measures one way.
|
|
||||||
- **It said nothing about certificates.** An internal name that resolves, routes and answers over TLS
|
|
||||||
that nothing can verify is not a working path; it is a working path for whoever holds the proxy's
|
|
||||||
trust and nobody else.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**Each hosting form gets its own endpoint, its own route and therefore its own name.** On every machine:
|
|
||||||
|
|
||||||
| name | what serves it |
|
|
||||||
|---|---|
|
|
||||||
| `connect-docker.<node>.internal` | a container |
|
|
||||||
| `connect-process.<node>.internal` | the mesh's own code, in a unit the mesh writes |
|
|
||||||
| `connect-unit.<node>.internal` | a unit a package ships |
|
|
||||||
|
|
||||||
and the same set under each machine's public domain where it has one — `connect-docker.<domain>` and its
|
|
||||||
siblings. The names are the instrument: a failure reads as *`connect-docker.g14.internal` did not answer*,
|
|
||||||
which says which machine and which hosting form without anybody interpreting anything.
|
|
||||||
|
|
||||||
**Every machine checks every machine, by name, over TLS, verifying the certificate.** Not a port, not an
|
|
||||||
address: resolve the name, connect, complete the handshake, check the certificate against the authority
|
|
||||||
that should have issued it — the mesh's own for an internal name, a public one for a public name. That is
|
|
||||||
the whole path a real caller takes, and each step failing is reported as itself.
|
|
||||||
|
|
||||||
**No name is written anywhere.** The machines come from the roster the mesh already renders as a fact, and
|
|
||||||
the labels are the module's. A machine that joins appears in every other machine's roster on the next
|
|
||||||
push, and they begin checking it without an edit.
|
|
||||||
|
|
||||||
**And the module arrives on a machine because the machine exists, not because somebody assigned it.** A
|
|
||||||
machine that joins and does not have it is worse than unchecked: every other machine is already dialling
|
|
||||||
its names, so it reads as broken everywhere until someone notices. This is the part the mesh cannot
|
|
||||||
currently express — see below — and it is the part that makes the rest safe.
|
|
||||||
|
|
||||||
**What survives from 0145**, unchanged: it reports and repairs nothing; one failure is not a fault and a
|
|
||||||
path is broken after consecutive runs with the count travelling with the result; findings are said on the
|
|
||||||
bus, because a finding in a file on the machine is what this exists to end; and the bus is the one path
|
|
||||||
that cannot report its own failure, so an emit that does not land is written locally and nowhere else.
|
|
||||||
|
|
||||||
## What this needs that the mesh does not have
|
|
||||||
|
|
||||||
Named here rather than assumed, because each is a decision of its own and this record is not the place to
|
|
||||||
make them:
|
|
||||||
|
|
||||||
1. **A module that every machine has.** `ScopeNode` means *at most one holder per node* — an exclusivity
|
|
||||||
rule, not an obligation — and nothing assigns a module at enrolment. Today the resolver, the packet
|
|
||||||
filter, ssh and intrusion prevention are each assigned per machine by hand, which is the same gap
|
|
||||||
wearing different clothes.
|
|
||||||
2. **A container running a module's own bundle.** A `process` runs the mesh's own compiled code with no
|
|
||||||
image; a `container` needs an image of the module's own, which means a Dockerfile — the thing the
|
|
||||||
`bundle` artifact exists to abolish. Nothing in the catalogue runs a bundle in a container, so
|
|
||||||
`connect-docker` has no shape yet.
|
|
||||||
3. **A unit a package ships, for `connect-unit`.** The `service` resource puts an existing unit into a
|
|
||||||
state and deliberately installs none, so this form needs a package that serves a port — and naming a
|
|
||||||
program the machine may not have is
|
|
||||||
[issue 136](../04-ISSUES/136-a-module-may-name-a-program-the-machine-does-not-have/00-report.md).
|
|
||||||
4. **A machine's public domain in the roster fact.** The fact carries each machine's name, mesh name,
|
|
||||||
address and operator account. The public names cannot be composed without the domain.
|
|
||||||
5. **Something that installs the mesh's own root on a machine.** This is
|
|
||||||
[issue 129](../04-ISSUES/129-nothing-makes-a-machine-trust-the-meshs-authority/00-report.md), open
|
|
||||||
since before any of this. Until it is closed, every internal name will fail certificate verification
|
|
||||||
from every machine — correctly, because nothing can verify it. That is the checker working, and it is
|
|
||||||
worth saying in advance so the first run is not read as the checker being broken.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- **A failure names the machine and the hosting form.** That is the whole gain over a port: eleven hours
|
|
||||||
became two runs under 0145, and under this it also becomes one line that says where to look.
|
|
||||||
- **The checker surfaces issue 129 immediately**, and will report every internal name unverifiable until
|
|
||||||
it is fixed. A reader must be told that before the first run rather than after.
|
|
||||||
- **Five things must be built before this is what it says it is**, and until they are, what exists is a
|
|
||||||
port dial from one position — useful, and not this.
|
|
||||||
- **What got harder:** a module with three hosting forms of the same trivial service is a strange thing to
|
|
||||||
read. It is justified only because those three forms are how the mesh actually runs software, and a
|
|
||||||
checker that tested one of them would keep the class of outage it exists to catch.
|
|
||||||
|
|
||||||
## How it is checked
|
|
||||||
|
|
||||||
- **A name per hosting form answers from every machine**, asserted by name and not by port.
|
|
||||||
- **A certificate that does not verify is reported as that**, distinctly from a name that does not resolve
|
|
||||||
and a port that does not answer — three faults, three owners.
|
|
||||||
- **A machine that joins is checked by every other machine without an edit**, asserted by adding one to a
|
|
||||||
bed and looking at what the others dial on their next run.
|
|
||||||
- **A machine that joins has the module**, which is gap 1 above and is the assertion that cannot be
|
|
||||||
written yet.
|
|
||||||
- **The measured outage is still caught**: the path from a container to a service on its own machine is
|
|
||||||
closed and `connect-docker.<that node>.internal` fails from that machine while the others still pass.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0145](0145-a-module-checks-what-the-mesh-claims-is-reachable.md) — superseded; its core stands
|
|
||||||
- [ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md) — the two reaches these
|
|
||||||
names come from
|
|
||||||
- [ADR 0066](0066-public-routing-is-name-agnostic.md) — a label plus a domain, which is why no name is written
|
|
||||||
- [issue 129](../04-ISSUES/129-nothing-makes-a-machine-trust-the-meshs-authority/00-report.md) — what the
|
|
||||||
internal names will fail on until it is closed
|
|
||||||
- [issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md)
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
---
|
|
||||||
topic: what runs on it
|
|
||||||
status: accepted
|
|
||||||
date: 2026-09-29
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
extends: 02-DECISIONS/0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 147. A module anchors the mesh's authority on a machine, and takes it away again
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
The mesh runs its own certificate authority and every internal name is served with a certificate
|
|
||||||
from it. No machine trusts it. On an enrolled, adopted workstation — on the private network,
|
|
||||||
resolving through the mesh's resolver — every internal HTTPS name fails verification with
|
|
||||||
*unable to get local issuer certificate*
|
|
||||||
([issue 129](../04-ISSUES/129-nothing-makes-a-machine-trust-the-meshs-authority/00-report.md)).
|
|
||||||
The certificates are genuine; nothing on the machine has ever been told what issued them.
|
|
||||||
|
|
||||||
The authority's only consumer today is a proxy, which fetches the root into a directory of its own
|
|
||||||
and hands it to one program ([ADR 0098](0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md)).
|
|
||||||
That is enough for the proxy and for nothing else: a browser, `git` over HTTPS, `curl`, a package
|
|
||||||
manager and every module that calls another module by an internal name read the machine's trust
|
|
||||||
store, which holds the predecessor's authority and a developer tool's local root, and nothing of
|
|
||||||
the mesh's.
|
|
||||||
|
|
||||||
The predecessor wrote its root into every machine it set up. Removing it was deliberate — an
|
|
||||||
honest failure beats a name that verifies for the wrong reason — and it leaves the mesh with no
|
|
||||||
answer at all until this one lands. It is also what keeps the predecessor alive on the machines
|
|
||||||
that still speak TLS to a mesh name.
|
|
||||||
|
|
||||||
**What makes this a decision rather than a patch** is where the knowledge goes. Two mechanisms in
|
|
||||||
the mesh already write things onto a machine because it is on the private network: `/etc/hosts`
|
|
||||||
and the registry's plaintext trust ([ADR 0082](0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md)).
|
|
||||||
Following that precedent, the controller would inject an anchor into every such machine's
|
|
||||||
declaration, and issue 129 proposed exactly that. It would work. It would also put *where this
|
|
||||||
operating system keeps trust anchors* and *which command refreshes its bundles* into the control
|
|
||||||
plane, for a fact the control plane does not have (the root does not exist until the authority has
|
|
||||||
run) and a machine that may have no reason to verify a mesh name at all.
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
1. **The controller injects the anchor into every machine on the private network**, the
|
|
||||||
`/etc/hosts` and insecure-registry shape. Rejected: being on the network is what makes the
|
|
||||||
registry reachable, and that is why network presence is the right trigger *there* — the trust
|
|
||||||
and the reachability are the same fact. Trusting an authority is not the same fact as being
|
|
||||||
able to reach it, and the anchor's path and the bundle refresh are a property of the machine's
|
|
||||||
operating system, which is the host's half of the mesh, not the controller's.
|
|
||||||
2. **A new host primitive — a `trust-anchor` resource type.** Rejected for now, not on principle.
|
|
||||||
The host's vocabulary should grow when a shape cannot be said with what exists, and this one
|
|
||||||
can: a file and a service already express it, as the packet filter proves
|
|
||||||
([ADR 0140](0140-the-filter-constrains-what-arrives-from-outside.md), whose module writes a
|
|
||||||
unit file and a service and nothing else). The primitive becomes right the moment a second
|
|
||||||
operating system is in play, because the anchor directory and the refresh command are exactly
|
|
||||||
the difference `internal/system` exists to hold. Until then it would be a vocabulary word with
|
|
||||||
one speaker.
|
|
||||||
3. **A module that requires the authority, fetches its root, installs it as an anchor and
|
|
||||||
refreshes the machine's bundles — and removes both when it is no longer assigned.** Adopted.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**A machine trusts the mesh's authority because a module put its root there, and stops trusting it
|
|
||||||
when that module is taken away.**
|
|
||||||
|
|
||||||
1. **The module requires `internal-acme-ca`** and reads the provider's bound address and the path
|
|
||||||
it serves its root at. It requires nothing else and provides nothing: it is a consumer of the
|
|
||||||
authority like any other.
|
|
||||||
2. **It fetches the root over the mesh's own network, without prior trust**, because there is no
|
|
||||||
prior trust to have — this is the module that establishes it — and the network is what
|
|
||||||
authenticates the fetch ([ADR 0098](0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md),
|
|
||||||
the same reasoning that lets the proxy fetch it). What it accepts is checked: a body that is
|
|
||||||
not a certificate fails, and the failure is the module's, not a later handshake's.
|
|
||||||
3. **It installs the root where this machine's TLS clients look, and refreshes the extracted
|
|
||||||
bundles** — the command that does the refresh is an ordinary part of the unit that places the
|
|
||||||
anchor, not a new thing the mesh can be asked to do.
|
|
||||||
4. **Removal is symmetric and is the same unit's business.** Undeclared, the host stops the unit;
|
|
||||||
stopping it removes the anchor and refreshes the bundles again. A machine that leaves the mesh
|
|
||||||
stops trusting the mesh, without anybody remembering to go and look.
|
|
||||||
5. **It is an ordinary assignment.** No machine is given it automatically. A machine that verifies
|
|
||||||
a mesh name is assigned it, and a machine that does not is not — which is the same statement
|
|
||||||
the mesh already makes about every other module, and is why this is not the controller's
|
|
||||||
business.
|
|
||||||
|
|
||||||
**One operating system, said out loud.** The anchor directory and the refresh command in the
|
|
||||||
module today are Arch's. On a machine that is not Arch the unit fails, visibly, rather than
|
|
||||||
writing a file nothing reads. That is the accurate failure, and it is the signal that option 2
|
|
||||||
above has become right.
|
|
||||||
|
|
||||||
## How this is checked
|
|
||||||
|
|
||||||
- **The verification that could not succeed before.** On a machine holding the module, a plain
|
|
||||||
client fetches an internal HTTPS name with no `-k` and no bundle argument and verifies. On a
|
|
||||||
machine without it, the same fetch fails with *unable to get local issuer certificate*. Both
|
|
||||||
halves, because only the pair distinguishes "the anchor works" from "something else already
|
|
||||||
trusted it".
|
|
||||||
- **The removal half, in the same bed:** unassign the module, refetch, and the failure returns.
|
|
||||||
Checking only the arrival is how a trust store fills up with authorities nobody can account for.
|
|
||||||
- **What is deliberately not checked here:** that the authority issues, that a name resolves, that
|
|
||||||
the proxy serves. Those have their own beds, and this module's bed passing for those reasons is
|
|
||||||
the failure mode this record is most exposed to — which is why the negative half is not optional.
|
|
||||||
|
|
||||||
**What this bed is dialled at, and why it is the authority itself.** The authority serves its own
|
|
||||||
API with a certificate it issued, so the handshake under test needs nothing else in the mesh to be
|
|
||||||
right. A trust bed that reached for a routed name through the proxy would be passing or failing for
|
|
||||||
the proxy's reasons and the resolver's.
|
|
||||||
|
|
||||||
**Written, and not yet run** *(2026-09-29)*. The bed is `trust-anchor` in the lab, and it cannot
|
|
||||||
execute: raising a foundation fails before any module is reached, in both bundles that exist
|
|
||||||
([issue 146](../04-ISSUES/146-the-foundation-cannot-be-raised-on-the-bus-the-mesh-runs-on/00-report.md)).
|
|
||||||
So what stands behind this record today is the rendering — the script the machine would run names
|
|
||||||
the authority it was bound to, checked in the control plane's own test suite — and **not** a machine
|
|
||||||
that verified anything. That is a weaker thing than the paragraph above describes, and it stays
|
|
||||||
written this way until the bed runs.
|
|
||||||
|
|
||||||
> **Progressive insight — 2026-09-30. It has now been run, on the live mesh rather than in the bed.**
|
|
||||||
> The paragraph above said nothing had verified anything, and something has. The module was registered
|
|
||||||
> from the catalogue, assigned to a workstation, and checked in the form this section prescribes — the
|
|
||||||
> authority's own API, so the handshake needs nothing else in the mesh to be right:
|
|
||||||
>
|
|
||||||
> ```
|
|
||||||
> $ curl -sS -o /dev/null -w '%{http_code}' https://<the authority>:9000/health
|
|
||||||
> 200
|
|
||||||
> subject=CN=Step Online CA
|
|
||||||
> issuer=O=Mesh Internal CA, CN=Mesh Internal CA Intermediate CA
|
|
||||||
> Verify return code: 0 (ok)
|
|
||||||
> ```
|
|
||||||
>
|
|
||||||
> **Both halves.** Unassigning and pushing removed the anchor, emptied the trust store of the mesh's
|
|
||||||
> authority, and returned the plain client to *unable to get local issuer certificate* — then assigning
|
|
||||||
> again restored it. The negative half is what distinguishes the anchor working from something else
|
|
||||||
> having trusted it, and it is the half nothing had ever exercised.
|
|
||||||
>
|
|
||||||
> **One thing this found that is not in the module.** The removal only works because the *host* removes
|
|
||||||
> the service before the script: stopping the unit is what deletes the certificate and refreshes the
|
|
||||||
> bundles, and it needs the script it calls to still exist. Nothing in the module states that ordering;
|
|
||||||
> the symmetry this record claims rests on it.
|
|
||||||
>
|
|
||||||
> Run on the live mesh because that is where a change is verified now
|
|
||||||
> ([ADR 0149](0149-the-live-mesh-is-the-test-bed.md)), and the bed still cannot raise a foundation. The
|
|
||||||
> evidence is [issue 129](../04-ISSUES/129-nothing-makes-a-machine-trust-the-meshs-authority/02-resolution.md).
|
|
||||||
> Extended the same day to every converged machine — `novox`, `g14` and `shanks` each hold the anchor
|
|
||||||
> and verify with a plain client. `ace` is excluded on purpose: it is adopted, so a module assigned
|
|
||||||
> there is held rather than run, which is right and is not trust.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
The predecessor's authority can be retired from a machine once this module is assigned to it,
|
|
||||||
which is the first time that has been true. `git` over HTTPS to the mesh's forge starts working,
|
|
||||||
so the ssh-only clone URL stops being a rule. A module on any machine can call another module's
|
|
||||||
internal name and verify it.
|
|
||||||
|
|
||||||
What got harder: one more module to assign to a machine that needs it, and the machine's trust
|
|
||||||
store now changes when an assignment changes — which is the point, and is also a thing an operator
|
|
||||||
can be surprised by. The fetch without prior trust is the same exposure ADR 0098 accepted, now on
|
|
||||||
every machine that holds the module rather than only where a proxy runs: anything that can stand
|
|
||||||
in the middle of the mesh's own network at the moment of the fetch can be believed. The mesh
|
|
||||||
already treats that network as the thing it authenticates.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [issue 129](../04-ISSUES/129-nothing-makes-a-machine-trust-the-meshs-authority/00-report.md) —
|
|
||||||
the symptom and the evidence.
|
|
||||||
- [ADR 0098](0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md) — a fact made at
|
|
||||||
first start is fetched from its provider; this extends it from one program to the machine.
|
|
||||||
- [ADR 0082](0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md) — the precedent
|
|
||||||
this deliberately does not follow, and why it is right where it is.
|
|
||||||
- [ADR 0005](0005-the-node-host.md) — the host is where one operating system's difference lives.
|
|
||||||
- [`03-DESIGN/01-to-be/08-connectivity.md`](../03-DESIGN/01-to-be/08-connectivity.md).
|
|
||||||
@@ -1,176 +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
|
|
||||||
---
|
|
||||||
|
|
||||||
# 148. The mesh's names are resolved, not copied into every container
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
The mesh gives every container it declares the whole roster of mesh names as entries written into
|
|
||||||
the container's own hosts file at creation
|
|
||||||
([design 08 §2](../03-DESIGN/01-to-be/08-connectivity.md),
|
|
||||||
[ADR 0066](0066-public-routing-is-name-agnostic.md)). A container takes those entries once and never
|
|
||||||
looks again.
|
|
||||||
|
|
||||||
Three issues are the same fact arriving three times.
|
|
||||||
|
|
||||||
**A container keeps the address it was made with.** Adopting the predecessor's tunnel moved the hub's
|
|
||||||
private address; the declaration followed it within one push and nothing on the machine did. The
|
|
||||||
forge's container held the old address, lost its database, reported healthy while its existing
|
|
||||||
connections lasted, and then the public name went down
|
|
||||||
([issue 109](../04-ISSUES/109-a-container-keeps-the-address-it-was-made-with/00-report.md)).
|
|
||||||
|
|
||||||
**The same fault, four days later, undetected for five days.** One container had restarted 2286 times
|
|
||||||
against a database it could no longer find, while the mesh reported the machine as doing what it was
|
|
||||||
told. Inside it, `novox.internal` was an address that had not existed for five days
|
|
||||||
([issue 135](../04-ISSUES/135-a-containers-mesh-names-are-not-compared/00-report.md)). Forty-eight
|
|
||||||
other containers were current, none of them corrected — each had been recreated for some other
|
|
||||||
reason and picked up the roster on the way.
|
|
||||||
|
|
||||||
135 was fixed by putting the roster into the digest the host compares a container against, so a
|
|
||||||
container whose names moved is recreated like one whose image moved. **That made the roster part of
|
|
||||||
every container's identity**, which is the third arrival:
|
|
||||||
|
|
||||||
**One name moving replaces every container in the mesh.** Migrating one small module on one machine
|
|
||||||
took four routine actions; each changed the roster, and each replaced every container on the control
|
|
||||||
node — its own store, the registry, the edge proxy, the forge, the directory, mail. The control plane
|
|
||||||
was unreachable twice while its own store came back through crash recovery
|
|
||||||
([issue 151](../04-ISSUES/151-a-new-name-recreates-every-container-in-the-mesh/00-report.md)). None of
|
|
||||||
the replaced containers had anything to do with the module being migrated, or with its machine.
|
|
||||||
|
|
||||||
The blast radius of a name is now every container that carries the list, which is all of them. The
|
|
||||||
node-by-node migration ahead adds names one module at a time — on one machine alone that is around
|
|
||||||
twenty-five — and each would be a full restart of every service on the hub.
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
**1. Keep the roster in every container and accept the churn.** Rejected. It is not a cost that can
|
|
||||||
be paid down: the mesh gets more names as it grows, and every name costs a restart of everything.
|
|
||||||
A rollback costs another.
|
|
||||||
|
|
||||||
**2. Scope each container's entries to the names it actually binds.** A container is given the names
|
|
||||||
of the things it declared a requirement on, so a name's blast radius is its consumers. Tidy, needs no
|
|
||||||
new mechanism, and keeps 135's guarantee exactly.
|
|
||||||
|
|
||||||
Rejected, and this is the close one. It contradicts the standing intent that **anything on the mesh
|
|
||||||
can call anything on it** — three cases, same machine, the private network, the public network, and
|
|
||||||
no fourth. Scoping resolution to declared couplings makes a name reachable only where the mesh was
|
|
||||||
told in advance that it would be wanted, and a person debugging inside a container would find names
|
|
||||||
missing that exist everywhere else on the machine. It also leaves the roster in the digest, so the
|
|
||||||
churn returns the moment a widely-bound name moves — smaller, not gone.
|
|
||||||
|
|
||||||
**3. Resolve at lookup time through the machine's resolver, and copy nothing.** Chosen.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**A container resolves the mesh's names through its machine's resolver, at the moment it asks. No
|
|
||||||
mesh name and no mesh address is written into a container, and none is part of a container's
|
|
||||||
identity.**
|
|
||||||
|
|
||||||
The three consequences that make this worth doing:
|
|
||||||
|
|
||||||
- **Staleness stops being possible**, rather than being detected. 109 and 135 are not bugs that were
|
|
||||||
fixed; they are a shape that no longer exists. A name that moves is answered differently by the next
|
|
||||||
lookup, in every container, with nothing recreated and nothing restarted.
|
|
||||||
- **A name's blast radius becomes nothing.** Assigning a module on one machine does not touch a
|
|
||||||
container on another.
|
|
||||||
- **Anything can still call anything**, which option 2 gave up. The resolver answers every mesh name to
|
|
||||||
every asker on the machine, exactly as it answers the machine itself.
|
|
||||||
|
|
||||||
**The resolver is a machine-level process, not a container** — one of the modules that is not a
|
|
||||||
container at all — so a container depending on it is not the circularity it would be if the mesh's
|
|
||||||
own store had to resolve a name through something the store's own runtime had to start first.
|
|
||||||
|
|
||||||
**What a module declares for itself is untouched.** Entries a manifest asks for are the module's own,
|
|
||||||
stay in the container, and stay in its identity: they are part of what the module *is*, they do not
|
|
||||||
move when the mesh's roster does, and the mesh does not know what they mean.
|
|
||||||
|
|
||||||
**The machine's own roster file is untouched.** It is a file, rewritten in place, read by processes and
|
|
||||||
people; nothing restarts when it changes. It is only the *copy into each container* that this ends.
|
|
||||||
|
|
||||||
### The order this lands in, which is not a preference
|
|
||||||
|
|
||||||
**Nothing may stop copying names until resolution works from a container.** Removing the copy first
|
|
||||||
reintroduces 109 and 135 — silently, and on a live mesh, which is exactly how both were found.
|
|
||||||
|
|
||||||
1. **A container on any network can reach the resolver.** Today a container on the runtime's default
|
|
||||||
network asks from an address the converged filter drops, so it has no DNS at all
|
|
||||||
([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.
|
|
||||||
3. **Then, and only then, the roster leaves the declaration and the digest.**
|
|
||||||
|
|
||||||
Until step 3 the mesh keeps copying, and keeps comparing. 151 stays open until step 3 lands; it is not
|
|
||||||
closed by this record, only answered by it.
|
|
||||||
|
|
||||||
## How this is checked
|
|
||||||
|
|
||||||
- **A container resolves a name that moved, without being recreated.** Move a name the mesh serves;
|
|
||||||
from a container that was running before the move and has not been touched since, the name answers
|
|
||||||
with the new address. This is the one 109 and 135 would both have failed.
|
|
||||||
- **A name's blast radius is nothing.** Add a routed name on one machine; no container on any other
|
|
||||||
machine is recreated. The apply report on each machine says nothing changed. This is 151.
|
|
||||||
- **Anything calls anything.** From a container on any machine, every `<node>.internal` name and every
|
|
||||||
routed name the mesh serves resolves — including names the module never declared a requirement on,
|
|
||||||
which is the guarantee option 2 would have given up.
|
|
||||||
- **On every network the runtime offers.** The first three hold for a container on the runtime's
|
|
||||||
default network as well as one on a declared network, because the default network is the case that
|
|
||||||
has no DNS today.
|
|
||||||
- **No mesh name is in a container's spec.** A test asserts the digest a host computes for a container
|
|
||||||
does not move when the mesh's roster does, and does move when the module's own declared entries do.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- **The resolver becomes load-bearing for every container**, where before it was load-bearing for the
|
|
||||||
machine. This is a real cost and is accepted: a resolver that is down is a machine that cannot
|
|
||||||
resolve, which is already true of the machine itself, and is a smaller event than a roster change
|
|
||||||
destroying and recreating every container on the machine.
|
|
||||||
- **Design 08's "a file rather than a resolver" no longer describes containers.** It was written when
|
|
||||||
the mesh had no resolver and it gave the right answer then. The reasoning it rested on — every Linux
|
|
||||||
has a hosts file, no package needed — was already overtaken by names a hosts file cannot express:
|
|
||||||
service names and wildcards under `<node>.internal`, which is why the resolver was built.
|
|
||||||
- **ADR 0066's mesh-wide propagation is kept and its mechanism changes.** A routed name still reaches
|
|
||||||
every asker in the mesh; it reaches them through the resolver rather than by being written into each
|
|
||||||
container. The consequence 0066 records — that an internal issuer's challenge needs the routed name
|
|
||||||
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
|
|
||||||
person debugging in a hand-started container resolving the same names as everything else is the
|
|
||||||
behaviour worth having, and it is what "anything can call anything" means.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [issue 151](../04-ISSUES/151-a-new-name-recreates-every-container-in-the-mesh/00-report.md) — one name replaces every container; the question this answers
|
|
||||||
- [issue 135](../04-ISSUES/135-a-containers-mesh-names-are-not-compared/00-report.md) — the roster put into the digest
|
|
||||||
- [issue 109](../04-ISSUES/109-a-container-keeps-the-address-it-was-made-with/00-report.md) — the first arrival
|
|
||||||
- [issue 110](../04-ISSUES/110-a-container-on-the-runtimes-own-network-cannot-reach-the-resolver/00-report.md) — the prerequisite
|
|
||||||
- [ADR 0066](0066-public-routing-is-name-agnostic.md) — routed names propagate mesh-wide; extended here
|
|
||||||
- [design 08 §2](../03-DESIGN/01-to-be/08-connectivity.md) — the file-not-resolver reasoning this narrows
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
---
|
|
||||||
topic: building it
|
|
||||||
status: accepted
|
|
||||||
date: 2026-09-30
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
supersedes: 02-DECISIONS/0068-the-lab-takes-requests.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 149. The live mesh is the test bed
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
[ADR 0068](0068-the-lab-takes-requests.md) proposed that the lab accept queued requests
|
|
||||||
— a bed and a commit — answer them one at a time from a copy it owns, and expose that through tools so
|
|
||||||
an agent could start a run and come back to it. It has been `proposed` since 2026-09-12 and nothing was
|
|
||||||
built.
|
|
||||||
|
|
||||||
What happened instead is that the mesh became the thing under test. It runs on four machines; every
|
|
||||||
fault worth finding in the last month was found on them, and none was found in a bed:
|
|
||||||
|
|
||||||
- a container holding an address that had not existed for five days, on the control node
|
|
||||||
([issue 135](../04-ISSUES/135-a-containers-mesh-names-are-not-compared/00-report.md));
|
|
||||||
- a machine reading healthy for eleven hours while no module could reach another
|
|
||||||
([issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md));
|
|
||||||
- one name replacing every container on the hub
|
|
||||||
([issue 151](../04-ISSUES/151-a-new-name-recreates-every-container-in-the-mesh/00-report.md));
|
|
||||||
- a consumer assertion that is correct on a mesh being raised and fatal on one that is running
|
|
||||||
([issue 156](../04-ISSUES/156-moving-a-consumers-delivery-subject-stops-the-control-plane/00-report.md)).
|
|
||||||
|
|
||||||
The last is the one that settles it. That change was exercised on the raise path — which is what a bed
|
|
||||||
*is* — and the raise path is the only path on which the fault cannot appear. A bed raises a mesh; it
|
|
||||||
does not have a mesh that has been running for weeks, with consumers already bound, containers created
|
|
||||||
against an older roster, and an adopted machine carrying a predecessor's configuration. **The faults
|
|
||||||
that cost the most were all faults of a mesh that already exists**, and a bed is by construction a mesh
|
|
||||||
that does not.
|
|
||||||
|
|
||||||
Lab runs are also expensive in a way that changed the behaviour around them: each costs a build and
|
|
||||||
several minutes, so they were batched, and a batched test is one whose result arrives after the next
|
|
||||||
three changes were already written.
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
**1. Build 0068 as proposed.** Rejected. It answers a question nobody is asking: the bottleneck was
|
|
||||||
never that a person had to sit at the lab, it was that a bed cannot hold the state the faults live in.
|
|
||||||
Queueing and tooling a mechanism that finds the wrong class of fault faster is not an improvement.
|
|
||||||
|
|
||||||
**2. Leave 0068 `proposed`.** Rejected, and it is why this record exists rather than nothing. A record
|
|
||||||
that contradicts current practice and sits unresolved is worse than either answer: it reads as intent
|
|
||||||
to anyone who finds it, and the practice it contradicts is written down nowhere but a handoff note.
|
|
||||||
|
|
||||||
**3. Record that the live mesh is the test bed, and supersede 0068.** Chosen.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**A change is verified against the mesh that is running.** Not because a bed would be unwelcome, but
|
|
||||||
because the state that breaks things is state a bed does not have: containers made against an older
|
|
||||||
roster, consumers already bound, an adopted machine, a store with weeks of history.
|
|
||||||
|
|
||||||
**A change that can only be exercised on the raise path is not verified.** If the only test available
|
|
||||||
raises a fresh mesh, the record says so, and says which case was therefore not covered. The words
|
|
||||||
"exercised on a fresh mesh" are a statement about coverage, not a pass.
|
|
||||||
|
|
||||||
**The lab is not retired**, and [ADR 0016](0016-the-lab.md) stands. It remains the place to raise a
|
|
||||||
mesh from bare, which is the one thing the live mesh cannot be asked to do and the one thing a bed does
|
|
||||||
better than anything else. What this record removes is the lab as the *default* answer to "is this
|
|
||||||
change good", and with it 0068's queue, tools and request protocol.
|
|
||||||
|
|
||||||
**Accuracy over a green run.** An honest failure on the live mesh beats a pass in a bed that could not
|
|
||||||
have failed — and a change that is risky on the running mesh is a reason to make the change smaller,
|
|
||||||
not a reason to test it somewhere it cannot break.
|
|
||||||
|
|
||||||
**What 0068 got right is kept as a rule, not a mechanism:** a run reads a copy that is not anybody's
|
|
||||||
working tree. Every run of the lab that mattered was pinned to a checkout rather than a worktree, and
|
|
||||||
the ones that were not produced results about code nobody had written down.
|
|
||||||
|
|
||||||
## How this is checked
|
|
||||||
|
|
||||||
- **A record that says a change was verified says on what.** Where it was a fresh mesh, it says which
|
|
||||||
case is uncovered. This is the clause that would have caught 156: its change was verified, honestly,
|
|
||||||
on the only path where it works.
|
|
||||||
- **The lab is not in the path of a merge.** No check, playbook or handoff requires a bed to have run.
|
|
||||||
- **0068 is unreachable as intent.** Its status is `superseded` and it names this record, so a reader
|
|
||||||
arriving at the queue design finds out immediately that it was not built and why.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- **A fault can be introduced on the machines that serve.** This is the cost, it is real, and it was
|
|
||||||
paid twice in one evening — a control plane crash-looping for half an hour, and every container on the
|
|
||||||
hub recreated five times. Both were found in minutes because they were live, and both would have
|
|
||||||
passed a bed.
|
|
||||||
- **There is no pre-merge gate beyond the repositories' own suites.** `make check` and the three hq
|
|
||||||
checks are what stands between a change and the machines, which raises what those suites are worth
|
|
||||||
and makes a test that cannot fail a genuine defect rather than an untidiness.
|
|
||||||
- **Raising a mesh from bare is now the lab's whole job**, and is exercised deliberately rather than
|
|
||||||
as a side effect of testing something else. The foundation work
|
|
||||||
([issue 146](../04-ISSUES/146-the-foundation-cannot-be-raised-on-the-bus-the-mesh-runs-on/00-report.md))
|
|
||||||
is that job, and it is also the proof that the mesh can make another of itself.
|
|
||||||
- **An agent cannot hand a run to a queue and come back**, which 0068 would have given. In practice it
|
|
||||||
watches a push and reads the machines, which is what happened anyway.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0068](0068-the-lab-takes-requests.md) — superseded by this
|
|
||||||
- [ADR 0016](0016-the-lab.md) — the lab, which stands
|
|
||||||
- [issue 156](../04-ISSUES/156-moving-a-consumers-delivery-subject-stops-the-control-plane/00-report.md) — correct on the raise path, fatal on a running mesh
|
|
||||||
-113
@@ -1,113 +0,0 @@
|
|||||||
---
|
|
||||||
topic: what runs on it
|
|
||||||
status: accepted
|
|
||||||
date: 2026-09-30
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
extends: 02-DECISIONS/0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 150. A module's own code runs as supervised processes under the module's one account
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
The repository answers "what runs a module's own code" two ways and reconciles them nowhere
|
|
||||||
([issue 117](../04-ISSUES/117-a-modules-own-code-is-a-container-and-a-process/00-report.md)).
|
|
||||||
|
|
||||||
[ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md) is accepted and
|
|
||||||
says **a container** — "the tool runtime carrying that module's compiled code" — and "one module, one
|
|
||||||
process, one account". Two `proposed` design documents say a **`process`** resource running an argv,
|
|
||||||
supervised by the machine, and one of them declares *four* of them for a single module and presents
|
|
||||||
four as the point. Neither design document names 0047 in its `decisions:`, and the string `process`
|
|
||||||
as a resource type appears in no decision record at all. The thing as built is the container.
|
|
||||||
|
|
||||||
Two things have happened since 0047 was written that bear on it directly.
|
|
||||||
|
|
||||||
[**ADR 0142**](0142-the-mesh-delivers-its-own-components-as-binaries.md) decided that the mesh's own
|
|
||||||
components are binaries on the machine rather than container images, and
|
|
||||||
[issue 114](../04-ISSUES/114-should-the-controller-be-a-container-or-a-process/00-report.md) was closed
|
|
||||||
by it. That settled the mesh's components and deliberately said nothing about a module's.
|
|
||||||
|
|
||||||
And the standing definition of a module hardened: **a module is software that delivers one or more
|
|
||||||
services, and a module is not a container.** It may deliver them as a container, an installed package
|
|
||||||
with a unit, a binary, or configuration files; 61 of 73 happen to use a container and 11 do not,
|
|
||||||
including the resolver, sshd and fail2ban. A rule that a module's *own code* must be a container makes
|
|
||||||
the one kind of module the mesh writes itself the only kind that has no choice.
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
**1. Hold 0047 as written: a container.** Rejected. Its own reasoning does not require one. What 0047
|
|
||||||
argued for was a runtime **per module** rather than one for the whole node, because a node-wide runtime
|
|
||||||
could not hold a per-module broker account and per-module runtimes competing on one tool key would each
|
|
||||||
be handed calls for tools they do not have. A supervised unit per module satisfies that argument
|
|
||||||
exactly — it is per module, and a unit runs as an account. The container was the mechanism to hand, not
|
|
||||||
the conclusion.
|
|
||||||
|
|
||||||
**2. Let each design document choose.** Rejected; that is the present state and it is what issue 117
|
|
||||||
reports. A module author reading the guide writes four processes; a module author reading the record
|
|
||||||
writes a container; nothing tells either that the other exists.
|
|
||||||
|
|
||||||
**3. Settle the hosting form as a supervised process, and settle the count separately.** Chosen.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**A module's own code runs as one or more supervised processes on the machine, under the module's single
|
|
||||||
account.** Where [ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md) says
|
|
||||||
"a container, the tool runtime carrying that module's compiled code", read this record. Everything else
|
|
||||||
0047 decided stands untouched: a tool is served on its own key, only the module that serves it answers,
|
|
||||||
and the module's account is scoped to exactly its tool keys.
|
|
||||||
|
|
||||||
**The invariant is the account, not the process count.** 0047's "one module, one process, one account"
|
|
||||||
carried its weight in the last clause. Its stated worry about a second process was "not a second one to
|
|
||||||
scope and seal" — a second *identity* to grant, seal a secret to, and scope on the bus. Several
|
|
||||||
processes sharing the module's one account create no second identity, so nothing further is scoped or
|
|
||||||
sealed, and a module may therefore declare as many as its work has shapes: events, tools, a
|
|
||||||
provisioner, a scheduled ingest. **What a module may not have is two accounts.**
|
|
||||||
|
|
||||||
**A module that delivers its service as a container still does.** This record is about the code the
|
|
||||||
module itself carries — its tools, its events, its provisioner — and not about the software it delivers.
|
|
||||||
A module wrapping a third-party image wraps a third-party image.
|
|
||||||
|
|
||||||
**Why supervised by the machine rather than by the mesh:** it is the same answer ADR 0142 gave for the
|
|
||||||
mesh's own components, for the same reason. A unit the machine restarts needs no image, no registry
|
|
||||||
pull and no runtime to be up before the mesh's own code can run — which matters most for exactly the
|
|
||||||
modules whose code the mesh cannot start any other way.
|
|
||||||
|
|
||||||
## How this is checked
|
|
||||||
|
|
||||||
- **No design document describes a hosting form for a module's own code without citing this record.**
|
|
||||||
Designs 18 and 20 name it in `decisions:`; this is the gap issue 117's third point reports, and
|
|
||||||
`cycle.py` already enforces that a to-be design names its decisions.
|
|
||||||
- **A module declaring several processes resolves to one account.** A test composes a module with more
|
|
||||||
than one process resource and asserts the mesh mints exactly one broker account for it, scoped to that
|
|
||||||
module's tool keys and nothing else — which is 0047's invariant stated as an assertion rather than a
|
|
||||||
sentence.
|
|
||||||
- **A module's own code does not require the container runtime.** A machine with no container runtime
|
|
||||||
can still run a module whose code is its own, which is the claim that separates this from option 1 and
|
|
||||||
is checkable on a machine that has one by asserting the declaration names no image for it.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- **The sidecar port stops being needed.** [ADR 0029](0029-a-network-is-a-shape-because-an-action-cannot-be-undone.md)
|
|
||||||
records that "anything that is a service plus a sidecar currently has to publish a port to talk to
|
|
||||||
itself", and the host's `network` shape exists partly for it. A process beside the service on the same
|
|
||||||
machine reaches it without publishing anything, so that pressure goes.
|
|
||||||
- **Something must supervise, and it is the machine.** This adds a unit per module's code to what the
|
|
||||||
host writes and owns. The mesh already writes and owns units — `nftables` proves a module can write one
|
|
||||||
and run it — so the mechanism exists; the count grows.
|
|
||||||
- **A module's code is delivered, not pulled**, which puts it behind the same gap as the host's own
|
|
||||||
delivery ([ADR 0141](0141-the-host-delivers-its-own-successor.md), not built): nothing yet delivers a
|
|
||||||
version of a module's binary to a machine. A container's code arrives by `docker pull`, and this does
|
|
||||||
not. **This is the cost of the decision and it is not paid**; until delivery exists, a module whose code
|
|
||||||
is its own is a module somebody places by hand.
|
|
||||||
- **Issue 117 is answered and its three disagreements close differently:** container-or-unit is decided
|
|
||||||
here; one-process-or-several is decided here as several under one account; and whether the record was
|
|
||||||
consulted is fixed by designs 18 and 20 naming this one.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [issue 117](../04-ISSUES/117-a-modules-own-code-is-a-container-and-a-process/00-report.md) — the contradiction this answers
|
|
||||||
- [ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md) — extended; its "a container" clause is settled here
|
|
||||||
- [ADR 0142](0142-the-mesh-delivers-its-own-components-as-binaries.md) — the same answer for the mesh's own components
|
|
||||||
- [ADR 0141](0141-the-host-delivers-its-own-successor.md) — the delivery this depends on and which is not built
|
|
||||||
- [ADR 0029](0029-a-network-is-a-shape-because-an-action-cannot-be-undone.md) — the sidecar port this relieves
|
|
||||||
@@ -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
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
---
|
|
||||||
topic: how we work
|
|
||||||
status: accepted
|
|
||||||
date: 2026-09-30
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
extends: 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 153. The record is read by a module the mesh assigns, and the console lists it
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
[ADR 0025](0025-the-design-record-is-read-not-copied.md) decided that this repository is **read where
|
|
||||||
it is written, never copied to be found**: an agent reads it directly, and a search of the mesh's
|
|
||||||
memory consults that agent so its answers appear beside ordinary results. It named the check that
|
|
||||||
closes [issue 006](../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md): search
|
|
||||||
for a phrase that appears only in a design document here, and get it back. It gated the build on an
|
|
||||||
agent that did not exist — the mesh session of
|
|
||||||
[15 — The agent session](../03-DESIGN/01-to-be/15-the-agent-session.md) — and on a search that
|
|
||||||
does not exist either, now: the knowledge base 0025 meant was the predecessor's, and since the
|
|
||||||
cut-over nothing reaches it ([issue 147](../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md)).
|
|
||||||
|
|
||||||
**So the two halves of 0025 have no home.** There is no store to be "beside", and no session to be
|
|
||||||
the reader. What the mesh has instead, since today: a tool model in which every module answers what
|
|
||||||
it serves, and a console on the machine a person sits at that lists every tool the running modules
|
|
||||||
answer ([ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)). An agent holding the
|
|
||||||
console does not search a store; it reads a tool list and calls what fits the question.
|
|
||||||
|
|
||||||
**What 0025 could not tolerate was a derived copy** — the enforced copy winning while the reasoned one
|
|
||||||
quietly stops being true. It rejected a sync for that reason and for no other. A git checkout is not a
|
|
||||||
derived copy: it is the same bytes at a commit the answer names, and the only way it can differ from
|
|
||||||
the source is by lagging behind it, which is measurable and stated. 0025's own words allow it —
|
|
||||||
*retrieval is an agent reading this repository, not a copy living in a second store* — and the
|
|
||||||
transformation that makes a copy dangerous is exactly what a checkout does not do.
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
**1. Wait for the mesh session.** Rejected. Design 15 is `designed` with nothing built, its model
|
|
||||||
access is a provisions question with no consumer identity yet, and 006 has waited since 2026-08-23.
|
|
||||||
A record whose check cannot run is a rule enforced by nothing.
|
|
||||||
|
|
||||||
**2. The console reads the repository itself.** Rejected. The console holds nothing and decides
|
|
||||||
nothing (ADR 0152, ADR 0035); a reader inside it would be a second implementation of a thing that
|
|
||||||
should be one module, unavailable to a person's client and to any other module.
|
|
||||||
|
|
||||||
**3. A module that keeps a checkout of the repository and answers questions about it, listed by the
|
|
||||||
console like any tool.** Chosen.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**The reader is a module: `records`.** It requires the `git` provision — the forge — clones the
|
|
||||||
repository its settings name, keeps the checkout current on every merge the forge announces and on a
|
|
||||||
timer, and answers over the bus: where a phrase appears as written (document, line, nearest heading),
|
|
||||||
one document whole, what a folder holds, and where the checkout stands — always with the commit it
|
|
||||||
read. Nothing is indexed, ranked or summarised: a design record is found by its own words, and a
|
|
||||||
reader deciding which words matter would be a second opinion about somebody else's document.
|
|
||||||
|
|
||||||
**The repository is a setting, not a manifest field.** The module names no mesh
|
|
||||||
([ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md)): which repository it reads is
|
|
||||||
the assignment's business, and an installation that keeps its record elsewhere sets that. Until a
|
|
||||||
repository is set it serves no tools and says why. Public repositories only; it asks for no
|
|
||||||
credential, because a secret it did not need would be one more thing to seal.
|
|
||||||
|
|
||||||
**"Beside everything else" is the console's tool list.** 0025's second half — the search consults the
|
|
||||||
agent — has no store to consult and needs none: the console lists `records_search` beside the forge's
|
|
||||||
tools and the mesh's own verbs, with a description that says when to call it, and an agent choosing
|
|
||||||
tools for a symptom is the search. That is surfacing, not merely reaching: nobody has to know this
|
|
||||||
repository exists to be offered it.
|
|
||||||
|
|
||||||
**Reading stays one-way.** The module reads the forge and answers; nothing flows back into the
|
|
||||||
repository. It holds no credential that could write.
|
|
||||||
|
|
||||||
**The mesh session, when it exists, is a caller of this module, not a replacement for it.** Design 15's
|
|
||||||
*it holds the design record by reading it* is satisfied by asking `records`; the session brings
|
|
||||||
judgement, this brings the text.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- **Issue 006 closes on 0025's own check**, run through the console: `records_search` for a phrase
|
|
||||||
that appears in one design document here returns that document. The module's test does the same
|
|
||||||
against a repository it makes.
|
|
||||||
- **The as-is knowledge document is rewritten.** [`07-knowledge.md`](../03-DESIGN/00-as-is/07-knowledge.md)
|
|
||||||
described the predecessor's two stores; neither is reachable from the mesh, and what the mesh knows
|
|
||||||
is now what its modules answer. Saying otherwise is the failure this repository exists to name.
|
|
||||||
- **A checkout lags.** Between a merge and the next sync — seconds when the forge announces it,
|
|
||||||
minutes when it does not — an answer is the previous commit's, and says which. That is the cost of
|
|
||||||
no copy, and it is a number rather than a silence.
|
|
||||||
- **The reader depends on the forge module's event, by name.** `consumes: gitea.pull.merged` names a
|
|
||||||
module rather than the `git` seat, because the seat declares no events. A forge that is not gitea
|
|
||||||
leaves the timer as the only refresh, which still works.
|
|
||||||
- **What got harder:** the record is now reachable from every machine holding a console, which is what
|
|
||||||
was wanted, and a reader must remember that this repository is public and the mesh is not — the
|
|
||||||
module reads the public repository and nothing about the installation.
|
|
||||||
|
|
||||||
## How this is checked
|
|
||||||
|
|
||||||
| Rule | Checked by |
|
|
||||||
|---|---|
|
|
||||||
| A phrase in one document comes back from where it is written, with the commit | the module's test against a repository it makes; and live, through the console |
|
|
||||||
| A merge on the origin is pulled and the next answer names the new commit | the same test |
|
|
||||||
| A path outside the checkout is refused, not resolved | a test per shape |
|
|
||||||
| A failed sync leaves the checkout standing and is said | a test against an unreachable origin |
|
|
||||||
| Without a repository set, no tools are served and the log says why | the module's own start |
|
|
||||||
| The console lists `records_search` beside every other tool | the console's listing, live |
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0025](0025-the-design-record-is-read-not-copied.md) — extended: the reader is a module, the search is the console's list
|
|
||||||
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — what lists it
|
|
||||||
- [issue 006](../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md) — what closes
|
|
||||||
- [35 — Reading the record](../03-DESIGN/01-to-be/35-reading-the-record.md) — the design
|
|
||||||
- mesh-catalog `modules/records` — the module (PR 183)
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
---
|
|
||||||
topic: the mesh
|
|
||||||
status: accepted
|
|
||||||
date: 2026-09-30
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
extends: 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 154. The mesh's own verbs are the mesh-controller seat's tools, and which verbs those are
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
[ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) decided that a seat's protocol
|
|
||||||
carries its tools in full, that holding a seat means serving them, and that the mesh's own verbs are
|
|
||||||
the `mesh-controller` seat's. It named three prerequisites, none in place: the protocol in the store
|
|
||||||
rather than in compiled defaults; a protocol richer than a list of verbs; a node-scoped seat's subject
|
|
||||||
carrying the node. And it left one thing to a decision per seat: **which verbs each seat serves**,
|
|
||||||
because a seat's tools bind every future holder.
|
|
||||||
|
|
||||||
The console shipped the same day ([ADR 0152](0152-the-operators-surface-is-a-module-the-console.md))
|
|
||||||
and made the gap visible from the operator's chair: a person on a workstation could call every tool a
|
|
||||||
*module* serves and none of the mesh's own. What a node runs, what is assigned, whether a push
|
|
||||||
applied — the questions issue 147 opened with — still meant a shell on the control node. The console's
|
|
||||||
own handshake said so.
|
|
||||||
|
|
||||||
The control plane already answers every one of those questions, as commands: `status --json`,
|
|
||||||
`node show`, `plan --json`, `assign`, `push`. [ADR 0035](0035-one-implementation-several-surfaces.md)
|
|
||||||
says a surface is an adapter over those with no decisions in it, and the `api` verb proves the shape:
|
|
||||||
every route calls the function the command line calls.
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
**1. Leave the mesh's verbs to the shell until an identity provider authenticates the HTTP API.**
|
|
||||||
Rejected. The authenticated network surface is for a browser on another machine; the console is
|
|
||||||
already behind the machine's login (0152), and the bus already carries every other tool call under an
|
|
||||||
account whose permission list says what it may ask. Waiting would keep the one surface the mesh has
|
|
||||||
from answering the mesh's own questions, for a reason that does not apply to it.
|
|
||||||
|
|
||||||
**2. Serve the verbs as the mesh-controller *module's* tools, `mesh.mod.mesh-controller.tool.<verb>`.**
|
|
||||||
Rejected; 0132 rejected it already. The controller holds a seat, and the verbs must keep their address
|
|
||||||
while the control plane is being replaced, which is the moment they are most needed. A module's name
|
|
||||||
would change with the implementation; the seat's does not.
|
|
||||||
|
|
||||||
**3. Call each command's function inside the serving process.** Rejected on two facts: the commands
|
|
||||||
print, to the process's standard output, and two calls answered at once would read each other's
|
|
||||||
words; and each command opens and closes its own stores, which the serving process holds open. Making
|
|
||||||
every command return a value is the larger refactor, and it would give the tools a second code path to
|
|
||||||
keep in step with the command line — the thing ADR 0035 forbids.
|
|
||||||
|
|
||||||
**4. The holder of the seat runs the command it names, in its own binary, and answers what it
|
|
||||||
printed.** Chosen.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**The `mesh-controller` seat serves twelve verbs**, and these are its interface, additive within a
|
|
||||||
version ([33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §7):
|
|
||||||
|
|
||||||
| verb | answers with | takes |
|
|
||||||
|---|---|---|
|
|
||||||
| `tools` | every seat's tools, from the mesh's records | nothing |
|
|
||||||
| `status` | what is wrong, quiet, behind, waiting — `status --json` | nothing |
|
|
||||||
| `nodes` | every machine and its mode | nothing |
|
|
||||||
| `node` | what one machine reported, what it is assigned, why | `node` |
|
|
||||||
| `modules` | every module, its version, commit and machines | nothing |
|
|
||||||
| `seats` | every seat, what it delivers, who holds it — `seats --json` | nothing |
|
|
||||||
| `builds` | what was built lately and what came of it | `module` (optional) |
|
|
||||||
| `plan` | the declaration a machine would be sent — `plan --json` | `node` |
|
|
||||||
| `assign`, `unassign` | the mesh's own words, refusal included | `node`, `module` |
|
|
||||||
| `push` | that it was sent; `status` says what the machine did | `node` (optional: every machine behind) |
|
|
||||||
| `build` | that the build machine was asked; `builds` says what came of it | `repository`, `path`, `ref` |
|
|
||||||
|
|
||||||
**Each verb runs the command it names, in the controller's own binary, and answers what the command
|
|
||||||
printed** — the output, whether it succeeded, and, where the command speaks JSON, the same as data. A
|
|
||||||
refusal is the command's refusal in the command's words, because it is the same output. A verb takes
|
|
||||||
only the arguments its schema names; nothing reaches a flag the schema did not declare. `push` and
|
|
||||||
`build` are sent and not waited for: a call that blocked for a whole apply would time out on every
|
|
||||||
machine that takes a minute and say nothing about the others.
|
|
||||||
|
|
||||||
**The three prerequisites are built.** A seat's protocol is three columns on its row, seeded from the
|
|
||||||
compiled defaults where a row had none and additively thereafter, so a verb a release adds joins the
|
|
||||||
row and nothing an operator wrote is taken away. A served verb is its name, what it does, and the
|
|
||||||
schema of its arguments and answer; a manifest may still write a bare name. A node-scoped seat's tool
|
|
||||||
carries the node it is asked of, as the last token of its subject; a mesh-scoped seat's stays flat.
|
|
||||||
|
|
||||||
**Holding a mesh seat requires serving its verbs**, judged where the store's set is loaded, and the
|
|
||||||
refusal names the missing verbs. The controller's own manifest lists the twelve under `tools`.
|
|
||||||
|
|
||||||
**Discovery reads the records, through the seat.** `tools` is one of the twelve because the console
|
|
||||||
cannot read the store and should not: the mesh answers for its own records through the role that owns
|
|
||||||
them, and the answer is true while any *other* holder restarts. It is not true while the control plane
|
|
||||||
itself restarts, and the console says so rather than hiding the modules' tools with it.
|
|
||||||
|
|
||||||
**A grant of `*` reaches a role's tools; `seat:<seat>.<verb>` grants one.** The console's `*` needed no
|
|
||||||
change to reach the mesh's verbs, which is what a grant meaning *every tool* should mean.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- **The console answers the mesh's own questions.** Issue 147's first paragraph closes: what a node
|
|
||||||
runs, what is assigned, whether a push applied, from the machine the person sits at, over the bus,
|
|
||||||
under an account whose permission list says so.
|
|
||||||
- **Whoever may call `mesh-controller.push` may change the mesh.** That is the console's `*` on a
|
|
||||||
machine whose login owns the mesh (0152), and a person's account only if `operator issue` says so.
|
|
||||||
A grant reviewer reads `*` and `seat:mesh-controller.` with the same care.
|
|
||||||
- **A verb here binds every future controller.** Twelve is deliberate: what an operator asks weekly,
|
|
||||||
and nothing that is still finding its shape (`take`, `converge`, `settings`, `secret` stay commands).
|
|
||||||
- **A command's text is the answer**, and text changes. The three verbs that speak JSON carry it as
|
|
||||||
data; the rest are read by a person or an agent, not parsed. Anything that needs a shape asks for
|
|
||||||
`--json` to be added to the command first, which is the right order.
|
|
||||||
- **What got harder:** the mesh-controller seat's row now carries a protocol an operator could edit, and
|
|
||||||
a verb removed from the row is a verb the controller stops serving without a build. That is
|
|
||||||
ADR 0122's arrangement applied to tools, and `seats` shows the row.
|
|
||||||
|
|
||||||
## How this is checked
|
|
||||||
|
|
||||||
| Rule | Checked by |
|
|
||||||
|---|---|
|
|
||||||
| Every declared verb is one the binary can run, with the arguments its schema names | a test walks the table and derives a command line for each |
|
|
||||||
| A verb missing a required argument is refused in its own words, before anything runs | a test per shape |
|
|
||||||
| A holder that does not serve a mesh seat's verbs cannot hold it, and the refusal names them | a catalogue test against a seat with two verbs and a holder with one |
|
|
||||||
| A node-scoped seat's tool carries the node; a mesh seat's does not | the bus composition test: two nodes derive two addresses |
|
|
||||||
| The controller subscribes its seat's tools and may answer | the composition test, and the golden user list |
|
|
||||||
| `*` reaches a role's tools; `seat:` grants one and refuses a name with no verb | the composition test |
|
|
||||||
| The protocol is seeded into the row and widened additively | the store-backed seat test |
|
|
||||||
| Live: the console lists `mesh-controller.status` and a call answers what `status --json` prints | the rollout of this record |
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) — extended: the prerequisites built, the verbs decided
|
|
||||||
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — the surface that lists them
|
|
||||||
- [ADR 0035](0035-one-implementation-several-surfaces.md) — a surface is an adapter with no decisions in it
|
|
||||||
- [ADR 0122](0122-a-seat-is-data-a-rename-is-a-database-update.md) — the row is the mesh's, and now carries the protocol
|
|
||||||
- [33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) — the design this completes
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
---
|
|
||||||
topic: what runs on it
|
|
||||||
status: accepted
|
|
||||||
date: 2026-09-30
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
extends: 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 155. A definition names no installation: how that is checked, and the three ways a value that did gets out
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
[ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) decided that a module definition
|
|
||||||
names no node, no mesh and no host path, and said how the name half is checked: *a catalogue test
|
|
||||||
finds no domain name in any definition value*. No such test existed
|
|
||||||
([issue 134](../04-ISSUES/134-a-definition-may-still-name-the-mesh/00-report.md)). Written and run
|
|
||||||
over the 77 definitions on 2026-09-30, the check it describes finds **42 values**, in 15 definitions,
|
|
||||||
and they are of four kinds that want four different answers:
|
|
||||||
|
|
||||||
| kind | count | example |
|
|
||||||
|---|---|---|
|
|
||||||
| a service told its own public name as a literal | 5 | an identity provider's `KC_HOSTNAME`, an object store's console redirect, an automation tool's webhook URL |
|
|
||||||
| an operator's value written into the definition | 10 | a mail server's domain, site name, website, and the address it trusts a real-IP header from |
|
|
||||||
| this mesh's forge, by URL, as a recipe's build context | 2 | the builder and the proxy, which package the controller's source |
|
|
||||||
| an application built outside the mesh, pulled from this mesh's registry | 7 | four sites and tools whose repositories are the operator's own |
|
|
||||||
| the world's servers, named by upstream defaults | 10 | a Matrix homeserver's trusted key server, Element's integration manager |
|
|
||||||
| a module named after the domain it serves | 8 | one site module, with its paths and network named after it |
|
|
||||||
|
|
||||||
Not one was careless. Each was the value the software needs, and until today there was nowhere else
|
|
||||||
to put it ([issue 122](../04-ISSUES/122-a-module-cannot-ask-for-its-own-public-name/00-report.md)).
|
|
||||||
Two of the answers were built before this record: a module is told the name its route composes
|
|
||||||
(`${bound:<route>:name}`, controller PR 149, 2026-09-30), and a source may be a path on the git seat
|
|
||||||
([ADR 0111](0111-a-build-source-is-on-the-git-seat-or-external.md)). What was missing: an operator's
|
|
||||||
value in a file the software reads, the same for a build *context*, a way to say a name is meant, and
|
|
||||||
the check.
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
**1. A string search for the installation's own names.** Rejected. The controller is as
|
|
||||||
mesh-agnostic as the definitions; it does not know which names are "this mesh's", and a check that had
|
|
||||||
to be told would be configured per installation and pass everywhere else. What it can know is the
|
|
||||||
*shape*: a name under a public top-level domain, a public address.
|
|
||||||
|
|
||||||
**2. Report every such shape.** Rejected. Eight of the 42 were `why` strings — prose the mesh never
|
|
||||||
reads, explaining what a port is for — and a check that reports those beside `KC_HOSTNAME` teaches
|
|
||||||
people to ignore the report. And a Matrix homeserver *must* name the federation's public key server;
|
|
||||||
a check with no way to say so would be a check people argue with rather than obey.
|
|
||||||
|
|
||||||
**3. Judge what the mesh acts on; let a definition say which names it means, one by one, with a
|
|
||||||
reason; exempt the world's services that a definition may name as a policy default.** Chosen.
|
|
||||||
|
|
||||||
**For an operator's value**, one option was to wait for design 27's requirement form in full. Rejected
|
|
||||||
for the reason 0112 gave against a slow operator provider: if asking a person for a value takes more
|
|
||||||
than a setting, module authors route around it and the literals come back. `${setting:<key>}` is the
|
|
||||||
operator provider in its first form, on the settings a module already has.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**The check.** Every string value of a definition that the mesh acts on is judged for a hostname under
|
|
||||||
a public top-level domain and for a public address. Not judged: `why` and `description`, which are
|
|
||||||
prose. Allowed where they can only mean the world: the public registries an `image` may be pulled
|
|
||||||
from, the public resolvers a machine may forward to, and the public certificate authorities' ACME
|
|
||||||
directories. The container runtime's alias for its own host is the runtime's. A module's own name is a
|
|
||||||
value too. The check runs in `module check` and as a catalogue-wide test; **it does not yet refuse at
|
|
||||||
registration**, because the list it prints is the list that shrinks, and a registration that refused a
|
|
||||||
manifest whose only remedy is a merge elsewhere would refuse the mesh's own catalogue on the day the
|
|
||||||
check landed. It moves to registration when the list has been empty for a release.
|
|
||||||
|
|
||||||
**A name a definition means is declared with its reason.** `names-on-purpose` on a resource maps each
|
|
||||||
such name to why: *the federation's public key server, the world's*; *built outside the mesh, from the
|
|
||||||
application's own repository, until that repository is a build source on the git seat*. A name the map
|
|
||||||
does not cover is still reported. The host never sees the word.
|
|
||||||
|
|
||||||
**An operator's value reaches a file as `${setting:<key>}`**, filled from the module's settings
|
|
||||||
layers — the mesh's, then the node's — the same layers a mergeable file and a contribution take, so
|
|
||||||
`settings set <module>` stays the one place a person's values go. Refused, naming the key and the
|
|
||||||
command, when nothing set it: a default for a mail domain would be the literal this removes, and a
|
|
||||||
blank written silently would be a service that comes up wrong somewhere that names nothing.
|
|
||||||
|
|
||||||
**A build context may live on the git seat.** `context: {"seat": "git", "repository": "<owner>/<name>"}`
|
|
||||||
is composed by the mesh that builds it: the request carries each seat's clone base, and a builder told
|
|
||||||
no base for a seat a context names refuses the build by the seat's name rather than guessing a forge.
|
|
||||||
|
|
||||||
**A module is named for what it is.** The site module named after its domain is `website`.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- **The catalogue names no installation, and a test says so.** The forty-two became zero the same day,
|
|
||||||
by the four answers above; seven of them are declared on purpose and stay visible as the list to
|
|
||||||
shrink — four applications the mesh does not build yet.
|
|
||||||
- **An operator's values are the assignment's.** The mail module takes its domain, its site name, its
|
|
||||||
website and the address it trusts a real-IP header from as settings; a mesh that installs it without
|
|
||||||
them is refused at composition, by name, which is the right moment. The module's own README says
|
|
||||||
which.
|
|
||||||
- **What got harder:** a manifest reviewer has one more word to read, and `names-on-purpose` on an
|
|
||||||
application's image is a debt visible in the definition until the application is built here. A
|
|
||||||
reader of `settings set` output sees more keys than files, because a key a file asks for is a
|
|
||||||
destination too.
|
|
||||||
- **Not decided here:** ADR 0112's requirement form (design 27) still replaces `${setting:…}` and the
|
|
||||||
other placeholders when it lands; this is its first case, the way [ADR 0038](0038-the-mesh-assigns-the-port.md)
|
|
||||||
was for ports. Host paths ([issue 119](../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md))
|
|
||||||
are the next step of the same group, and the registry's name ([issue 123](../04-ISSUES/123-the-image-registry-is-named-after-a-role/00-report.md))
|
|
||||||
the one after.
|
|
||||||
|
|
||||||
## How this is checked
|
|
||||||
|
|
||||||
| Rule | Checked by |
|
|
||||||
|---|---|
|
|
||||||
| A value naming an installation is reported at its path, in the definition's words | a catalogue unit test over a definition with a hostname in an env value and a public address in a file |
|
|
||||||
| Prose, the world's registries in an image, public resolvers, ACME directories and the runtime's own alias are not reported | the same tests |
|
|
||||||
| A name declared on purpose is not reported; a name beside it that is not declared is | a test with a homeserver's config |
|
|
||||||
| An image from an installation's registry needs a reason | a test without and with the word |
|
|
||||||
| A module named after a domain is reported | a test |
|
|
||||||
| No definition in the catalogue names an installation | `TestNoCatalogueManifestNamesAnInstallation` over the checkout, and `module check modules/` |
|
|
||||||
| `${setting:key}` fills from the layers, node over mesh; refused by name when unset; not stray when set | three tests |
|
|
||||||
| A context on a seat is cloned from the base the mesh sent; a seat with no base is refused by name | the builder's test |
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) — extended: the check it promised, and the operator provider's first form
|
|
||||||
- [ADR 0111](0111-a-build-source-is-on-the-git-seat-or-external.md) — a source on the seat; now a context too
|
|
||||||
- [issue 122](../04-ISSUES/122-a-module-cannot-ask-for-its-own-public-name/00-report.md), [issue 134](../04-ISSUES/134-a-definition-may-still-name-the-mesh/00-report.md) — what this closes
|
|
||||||
- [27 — A module requires, the mesh resolves](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md) — where this sits in the larger design
|
|
||||||
- mesh-controller PR 169, mesh-catalog PR 188 — the check, the words, and the catalogue that passes it
|
|
||||||
-83
@@ -1,83 +0,0 @@
|
|||||||
---
|
|
||||||
topic: the mesh
|
|
||||||
status: accepted
|
|
||||||
date: 2026-09-30
|
|
||||||
deciders: jochen
|
|
||||||
reconstructed: false
|
|
||||||
extends: 02-DECISIONS/0075-two-stores-and-which-provides-what.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 156. An artifact is what a build produces, the artifact store serves every kind, and its seat is named for its scope
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
[Issue 123](../04-ISSUES/123-the-image-registry-is-named-after-a-role/00-report.md) found three
|
|
||||||
wordings disagreeing about the mesh's registry. The glossary defined *artifact* as "an OCI image, by
|
|
||||||
digest"; the manifest's build vocabulary names four kinds — `image`, `upstream`, `bundle`, `archive` —
|
|
||||||
and the catalogue builds all four; the seat was `the-artifact-store`, the last of the mesh's own seats
|
|
||||||
named after the job it does rather than for the mesh
|
|
||||||
([ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) decided the
|
|
||||||
rename and deferred it). The issue asked whether the seat and provision should be renamed after
|
|
||||||
images, and whether the mesh needs two registry implementations at all.
|
|
||||||
|
|
||||||
Reading what the store actually serves settles the first question the other way. A kept reference
|
|
||||||
has two shapes — `artifact-store://<module>/<artifact>@sha256:…` for an image and
|
|
||||||
`artifact-store://<module>/<artifact>/blobs/sha256:…` for an archive — and both are served by the
|
|
||||||
same OCI registry, by digest. [ADR 0075](0075-two-stores-and-which-provides-what.md) already
|
|
||||||
defined the provision that way: *content-addressed blobs, pinned by digest, no versions, no ranges;
|
|
||||||
what the mesh delivers to machines*. The provision was never an image registry. Only the glossary said
|
|
||||||
so, and only the seat's name was odd.
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
**1. Rename the seat and the provision after images.** Rejected. The store serves archives too, by the
|
|
||||||
same protocol; naming it for one kind would be the glossary's mistake made permanent, in the name
|
|
||||||
every manifest uses.
|
|
||||||
|
|
||||||
**2. Fix the word, rename the seat for its scope, keep the provision.** Chosen. The rename ADR 0121
|
|
||||||
deferred as a delivering-seat migration is, since [ADR 0122](0122-a-seat-is-data-a-rename-is-a-database-update.md),
|
|
||||||
one update and one alias: the former name resolves forever, a held record follows by cascade, a claim
|
|
||||||
written with the old name still holds.
|
|
||||||
|
|
||||||
**On two implementations:** left as 0075 decided. Two provisions because two protocols; the OCI
|
|
||||||
registry the genesis installs because something must serve images before the mesh can build; the
|
|
||||||
forge may provide `artifact-store` too and a mesh may choose it. The bootstrap argument is weaker than
|
|
||||||
it reads, as 123 says, and the day the forge is raised at genesis and adopted in place is the day to
|
|
||||||
retire the second server — a migration a mesh performs, not a decision to take here.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
- **An artifact is anything a build produces** — an image, a mirrored upstream image, a bundle, an
|
|
||||||
archive — and the glossary says so. *Image* is one kind. A module is not an image; a module may
|
|
||||||
build several artifacts and install none.
|
|
||||||
- **The artifact store serves artifacts of every kind a machine fetches**, images and archives, by
|
|
||||||
digest, over the OCI registry protocol. The provision keeps its name.
|
|
||||||
- **The seat is `mesh-artifact-store`.** `the-artifact-store` is its alias. The catalogue's registry
|
|
||||||
module claims the new name; a definition elsewhere claiming the old one still holds.
|
|
||||||
- The two other deferred renames — `npm-package-registry` and `git` — stay deferred, and for the
|
|
||||||
same reason no longer. They are one migration each when wanted; nothing here needs them.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- The glossary stops contradicting the manifest vocabulary, and a reader of `artifact-store` reads
|
|
||||||
it as what it is: where the mesh's built things are kept.
|
|
||||||
- One migration on the seat table; no manifest but the registry's changes; no consumer of the
|
|
||||||
provision changes, because the provision did not.
|
|
||||||
- **What got harder:** nothing measurable. A record that says `the-artifact-store` is read through the
|
|
||||||
alias; design 26's table already carried the new name as intent.
|
|
||||||
|
|
||||||
## How this is checked
|
|
||||||
|
|
||||||
| Rule | Checked by |
|
|
||||||
|---|---|
|
|
||||||
| The former name resolves to the seat once the store's aliases are loaded | a catalogue test |
|
|
||||||
| The seat is in the compiled set under its new name, delivering `artifact-store` | the seat tests, updated |
|
|
||||||
| The registry module holds the seat under the new name on the live mesh | `seats` after the rollout |
|
|
||||||
| The glossary's *artifact* matches the build kinds a manifest may declare | design 18's table and the showcase module list the kinds; the glossary names the same four |
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [issue 123](../04-ISSUES/123-the-image-registry-is-named-after-a-role/00-report.md)
|
|
||||||
- [ADR 0075](0075-two-stores-and-which-provides-what.md) — extended: the provision as defined stands, the word is corrected
|
|
||||||
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0122](0122-a-seat-is-data-a-rename-is-a-database-update.md) — the rename, decided and made cheap
|
|
||||||
- mesh-controller migration 0048; mesh-catalog `modules/distribution`
|
|
||||||
+7
-58
@@ -48,31 +48,6 @@ form above and dated no earlier than the record's own `date:` — an unmarked ed
|
|||||||
violation the reviewer looks for in the diff, and a marked one is legible in the record itself.
|
violation the reviewer looks for in the diff, and a marked one is legible in the record itself.
|
||||||
The git history is the backstop, not the record of intent; the note is the record of intent.
|
The git history is the backstop, not the record of intent; the note is the record of intent.
|
||||||
|
|
||||||
## A pointer back from what a record changes
|
|
||||||
|
|
||||||
A new record naming an old one is not enough. **Where a record changes a mechanism an older record
|
|
||||||
states — without reversing the decision, so no supersession — the older record gets a dated note
|
|
||||||
saying where its mechanism now lives.** A reader arrives at the old record by following a citation,
|
|
||||||
and finds text that is still the decision and no longer the method; nothing in it says a later record
|
|
||||||
moved the method, and the new record is not in their hands.
|
|
||||||
|
|
||||||
> **The mechanism changed — YYYY-MM-DD, by ADR NNNN.** What still stands, what moved,
|
|
||||||
> and why.
|
|
||||||
|
|
||||||
Three examples of the shape, all found by being missed: ADR 0066 still described a routed name being
|
|
||||||
written into every container after 0148 replaced that with resolution; ADR 0047 still said a module's
|
|
||||||
code runs in a container after 0150 made it a supervised process; and ADR 0016 still read as though the
|
|
||||||
lab were the test bed after 0149 said the live mesh is. Each was a citation leading to the wrong
|
|
||||||
answer, in a record that was not wrong about anything it decided.
|
|
||||||
|
|
||||||
**This is not machine-checked, and it cannot be from `extends:` alone.** 102 records extend another and
|
|
||||||
87 name a parent that does not mention them, which is correct: extending usually means building on a
|
|
||||||
context, and a one-directional pointer is the right shape for that. What needs a note is the narrower
|
|
||||||
case where the parent's own text has gone stale, and which case that is, is a judgement — so it is a
|
|
||||||
rule for the author and the reviewer, and the diff is where it is caught. Making it mechanical would
|
|
||||||
mean a record declaring the relationship in its frontmatter, which is a change to the record schema and
|
|
||||||
has not been decided.
|
|
||||||
|
|
||||||
The records run in the order the decisions were taken, oldest first.
|
The records run in the order the decisions were taken, oldest first.
|
||||||
|
|
||||||
**Every decision is a record.** There is no ledger and no index file — if a decision is worth
|
**Every decision is a record.** There is no ledger and no index file — if a decision is worth
|
||||||
@@ -160,16 +135,10 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0116** — [The bus is built in five steps, and the protocol moves with it](0116-the-bus-is-built-in-five-steps.md)
|
- **0116** — [The bus is built in five steps, and the protocol moves with it](0116-the-bus-is-built-in-five-steps.md)
|
||||||
- **0119** — [A taken tunnel's predecessor is retired once the take is proven](0119-a-taken-tunnels-predecessor-is-retired.md)
|
- **0119** — [A taken tunnel's predecessor is retired once the take is proven](0119-a-taken-tunnels-predecessor-is-retired.md)
|
||||||
- **0125** — [The bus is the only broker](0125-the-bus-is-the-only-broker.md) *(superseded)*
|
- **0125** — [The bus is the only broker](0125-the-bus-is-the-only-broker.md) *(superseded)*
|
||||||
- **0127** — [AMQP is a provision, not the bus](0127-amqp-is-a-provision-not-the-bus.md) *(superseded)*
|
- **0127** — [AMQP is a provision, not the bus](0127-amqp-is-a-provision-not-the-bus.md)
|
||||||
- **0128** — [The mesh bus is required, not ambient](0128-the-mesh-bus-is-required-not-ambient.md)
|
- **0128** — [The mesh bus is required, not ambient](0128-the-mesh-bus-is-required-not-ambient.md)
|
||||||
- **0129** — [A seat carries the protocol of its role](0129-a-seat-carries-the-protocol-of-its-role.md)
|
- **0129** — [A seat carries the protocol of its role](0129-a-seat-carries-the-protocol-of-its-role.md)
|
||||||
- **0130** — [The predecessor is ending, and its broker goes with it](0130-the-predecessor-is-ending-and-its-broker-goes-with-it.md)
|
- **0130** — [The predecessor is ending, and its broker goes with it](0130-the-predecessor-is-ending-and-its-broker-goes-with-it.md)
|
||||||
- **0131** — [Everything on the mesh speaks to the broker seat, and AMQP is not a provision](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)
|
|
||||||
- **0132** — [A seat carries the tools its holder must serve](0132-a-seat-carries-the-tools-its-holder-must-serve.md)
|
|
||||||
- **0134** — [The mesh says what it applied](0134-the-mesh-says-what-it-applied.md)
|
|
||||||
- **0142** — [The mesh delivers its own components as binaries, not as container images](0142-the-mesh-delivers-its-own-components-as-binaries.md)
|
|
||||||
- **0154** — [The mesh's own verbs are the mesh-controller seat's tools, and which verbs those are](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)
|
|
||||||
- **0156** — [An artifact is what a build produces, the artifact store serves every kind, and its seat is named for its scope](0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)
|
|
||||||
|
|
||||||
### Its tiers, from the bottom up
|
### Its tiers, from the bottom up
|
||||||
|
|
||||||
@@ -201,8 +170,6 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0108** — [A route carries the policy applied to a request, and names a secret rather than holding one](0108-a-route-carries-the-policy-applied-to-a-request.md)
|
- **0108** — [A route carries the policy applied to a request, and names a secret rather than holding one](0108-a-route-carries-the-policy-applied-to-a-request.md)
|
||||||
- **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)
|
- **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)
|
- **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
|
### What runs on them, and how it gets there
|
||||||
|
|
||||||
@@ -235,31 +202,15 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0091** — [A mount is declared, and there are three things it can be](0091-a-mount-is-declared-three-ways.md)
|
- **0091** — [A mount is declared, and there are three things it can be](0091-a-mount-is-declared-three-ways.md)
|
||||||
- **0099** — [A step that runs once names what it reads, and runs again when it changed](0099-a-step-that-runs-once-names-what-it-reads.md)
|
- **0099** — [A step that runs once names what it reads, and runs again when it changed](0099-a-step-that-runs-once-names-what-it-reads.md)
|
||||||
- **0110** — [A seat is held by one assignment, from a closed set, and it may deliver a provision](0110-a-seat-is-a-module-assignment-from-a-closed-set.md)
|
- **0110** — [A seat is held by one assignment, from a closed set, and it may deliver a provision](0110-a-seat-is-a-module-assignment-from-a-closed-set.md)
|
||||||
- **0112** — [A module definition names no node, no mesh and no path: everything it needs is a requirement the mesh resolves](0112-a-module-definition-names-no-node-mesh-or-path.md)
|
- **0112** — [A module definition names no node, no mesh and no path: everything it needs is a requirement the mesh resolves](0112-a-module-definition-names-no-node-mesh-or-path.md) *(proposed)*
|
||||||
- **0113** — [The vault makes every shared secret, a provider makes resources and data, and the mesh carries both](0113-the-vault-makes-every-secret.md)
|
- **0113** — [The vault makes every shared secret, a provider makes resources and data, and the mesh carries both](0113-the-vault-makes-every-secret.md) *(proposed)*
|
||||||
- **0114** — [A credential two parties hold rotates over two credentials; one a single party holds rotates in place, staged; and retiring a credential never removes what it reached](0114-a-shared-credential-rotates-over-two-credentials.md)
|
- **0114** — [A credential two parties hold rotates over two credentials; one a single party holds rotates in place, staged; and retiring a credential never removes what it reached](0114-a-shared-credential-rotates-over-two-credentials.md) *(proposed)*
|
||||||
- **0115** — [One assignment of a module per node: the module's name is the assignment's identity](0115-one-assignment-of-a-module-per-node.md)
|
- **0115** — [One assignment of a module per node: the module's name is the assignment's identity](0115-one-assignment-of-a-module-per-node.md) *(proposed)*
|
||||||
- **0117** — [A machine's uplink is a seat: the mesh configures the manager, never the link](0117-a-machines-uplink-is-a-seat.md)
|
- **0117** — [A machine's uplink is a seat: the mesh configures the manager, never the link](0117-a-machines-uplink-is-a-seat.md)
|
||||||
- **0118** — [Undeclaring removes what the mesh made, and gives a unit back the state it was found in](0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md)
|
- **0118** — [Undeclaring removes what the mesh made, and gives a unit back the state it was found in](0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md)
|
||||||
- **0120** — [A roster fact carries its format as a template: the mesh owns the data, the module owns the format](0120-a-roster-fact-carries-its-format-as-a-template.md)
|
- **0120** — [A roster fact carries its format as a template: the mesh owns the data, the module owns the format](0120-a-roster-fact-carries-its-format-as-a-template.md)
|
||||||
- **0121** — [A system seat is named for its scope, and a module may define its own](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)
|
- **0121** — [A system seat is named for its scope, and a module may define its own](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)
|
||||||
- **0122** — [A seat is data the controller owns, and a rename is a database update](0122-a-seat-is-data-a-rename-is-a-database-update.md)
|
- **0122** — [A seat is data the controller owns, and a rename is a database update](0122-a-seat-is-data-a-rename-is-a-database-update.md)
|
||||||
- **0133** — [A module owns its migrations, and the mesh owns when they run](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md) *(superseded)*
|
|
||||||
- **0135** — [A module version prepares its state before it runs](0135-a-module-version-prepares-its-state-before-it-runs.md)
|
|
||||||
- **0136** — [A step gates its module, not the machine](0136-a-step-gates-its-module-not-the-machine.md)
|
|
||||||
- **0137** — [A machine says which networks it routes](0137-a-machine-says-which-networks-it-routes.md) *(superseded)*
|
|
||||||
- **0138** — [An assignment binds an endpoint and says how far it reaches](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md)
|
|
||||||
- **0139** — [A network is forwarded because a module declared it](0139-a-network-is-forwarded-because-a-module-declared-it.md) *(superseded)*
|
|
||||||
- **0140** — [The filter constrains what arrives from outside, and says nothing about a machine's own guests](0140-the-filter-constrains-what-arrives-from-outside.md)
|
|
||||||
- **0141** — [The host delivers its own successor, and versions live side by side](0141-the-host-delivers-its-own-successor.md)
|
|
||||||
- **0143** — [A consumer verifies the grant it is given](0143-a-consumer-verifies-the-grant-it-is-given.md) *(superseded)*
|
|
||||||
- **0144** — [Anything on a machine may call anything on it, and that is the whole of "local"](0144-anything-on-a-machine-may-call-anything-on-it.md)
|
|
||||||
- **0145** — [A module checks what the mesh claims is reachable, and it checks itself](0145-a-module-checks-what-the-mesh-claims-is-reachable.md) *(superseded)*
|
|
||||||
- **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)
|
|
||||||
- **0155** — [A definition names no installation: how that is checked, and the three ways a value that did gets out](0155-a-definition-names-no-installation-and-how-that-is-checked.md)
|
|
||||||
|
|
||||||
### How it is built
|
### How it is built
|
||||||
|
|
||||||
@@ -269,9 +220,9 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0014** — [No workspace — each module is a standalone package consuming published dependencies](0014-no-npm-workspace.md)
|
- **0014** — [No workspace — each module is a standalone package consuming published dependencies](0014-no-npm-workspace.md)
|
||||||
- **0015** — [Applications live in their own repository; the monorepo is for the mesh](0015-applications-live-in-their-own-repository.md)
|
- **0015** — [Applications live in their own repository; the monorepo is for the mesh](0015-applications-live-in-their-own-repository.md)
|
||||||
- **0016** — [The lab](0016-the-lab.md)
|
- **0016** — [The lab](0016-the-lab.md)
|
||||||
- **0037** — [Where a module lives](0037-where-a-module-lives.md)
|
- **0037** — [Where a module lives](0037-where-a-module-lives.md) *(proposed)*
|
||||||
- **0039** — [What the SDK holds, and what it refuses](0039-what-the-sdk-holds-and-refuses.md)
|
- **0039** — [What the SDK holds, and what it refuses](0039-what-the-sdk-holds-and-refuses.md)
|
||||||
- **0068** — [The lab takes requests, one at a time, and runs each from its own copy](0068-the-lab-takes-requests.md) *(superseded)*
|
- **0068** — [The lab takes requests, one at a time, and runs each from its own copy](0068-the-lab-takes-requests.md) *(proposed)*
|
||||||
- **0069** — [A module is a repository and a path within it](0069-a-module-is-a-repository-and-a-path.md)
|
- **0069** — [A module is a repository and a path within it](0069-a-module-is-a-repository-and-a-path.md)
|
||||||
- **0076** — [The SDK is a published package, and the toolchain resolves it by version](0076-the-sdk-is-a-published-package.md)
|
- **0076** — [The SDK is a published package, and the toolchain resolves it by version](0076-the-sdk-is-a-published-package.md)
|
||||||
- **0082** — [The registry is reached by name, and the overlay is its security](0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md)
|
- **0082** — [The registry is reached by name, and the overlay is its security](0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md)
|
||||||
@@ -280,7 +231,6 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0097** — [A vendor image is a declared build input, and a recipe fetches nothing undeclared](0097-a-vendor-image-is-a-declared-build-input.md)
|
- **0097** — [A vendor image is a declared build input, and a recipe fetches nothing undeclared](0097-a-vendor-image-is-a-declared-build-input.md)
|
||||||
- **0107** — [Persistent data is a directory bind, never a named volume](0107-persistent-data-is-a-directory-bind-never-a-named-volume.md)
|
- **0107** — [Persistent data is a directory bind, never a named volume](0107-persistent-data-is-a-directory-bind-never-a-named-volume.md)
|
||||||
- **0111** — [A build source is on the mesh's git seat, or it is an external repository](0111-a-build-source-is-on-the-git-seat-or-external.md)
|
- **0111** — [A build source is on the mesh's git seat, or it is an external repository](0111-a-build-source-is-on-the-git-seat-or-external.md)
|
||||||
- **0149** — [The live mesh is the test bed](0149-the-live-mesh-is-the-test-bed.md)
|
|
||||||
|
|
||||||
### How it is checked
|
### How it is checked
|
||||||
|
|
||||||
@@ -301,6 +251,5 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0034** — [The local account owns the mesh, and a web application's login is not that](0034-the-local-account-owns-the-mesh.md)
|
- **0034** — [The local account owns the mesh, and a web application's login is not that](0034-the-local-account-owns-the-mesh.md)
|
||||||
- **0080** — [The development cycle is checked, not trusted](0080-the-development-cycle-is-checked.md)
|
- **0080** — [The development cycle is checked, not trusted](0080-the-development-cycle-is-checked.md)
|
||||||
- **0081** — [A decision nothing cites is not yet in the chain](0081-a-decision-nothing-cites-is-not-yet-in-the-chain.md)
|
- **0081** — [A decision nothing cites is not yet in the chain](0081-a-decision-nothing-cites-is-not-yet-in-the-chain.md)
|
||||||
- **0153** — [The record is read by a module the mesh assigns, and the console lists it](0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)
|
|
||||||
|
|
||||||
<!-- index:end -->
|
<!-- index:end -->
|
||||||
|
|||||||
@@ -1,58 +1,78 @@
|
|||||||
---
|
---
|
||||||
layer: as-is
|
layer: as-is
|
||||||
status: implemented
|
status: implemented
|
||||||
code: [mesh-catalog modules/records, mesh-catalog modules/mesh-console]
|
code: [hal]
|
||||||
updated: 2026-09-30
|
updated: 2026-08-23
|
||||||
decisions:
|
decisions: []
|
||||||
- 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
|
|
||||||
- 02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md
|
|
||||||
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Knowledge
|
# Knowledge
|
||||||
|
|
||||||
**The mesh keeps no knowledge store.** What it knows is what its modules answer, and the way a person
|
The mesh keeps two knowledge stores. They are not redundant, and knowing which is which is the
|
||||||
or an agent asks is the console's tool list on the machine they sit at
|
difference between finding an answer in one search and rediscovering it over several hours.
|
||||||
([13 — The console](13-the-console.md)). This document used to describe two stores; it is rewritten
|
|
||||||
because neither exists from the mesh's side, and an as-is document that describes what is gone is a
|
|
||||||
brochure.
|
|
||||||
|
|
||||||
## What was here, and where it went
|
## The operational memory
|
||||||
|
|
||||||
Until the cut-over of 2026-09-28 the predecessor ran two stores: an operational memory of notes
|
A store of operational notes, written and read by whoever — human or agent — is working. Each
|
||||||
indexed on symptoms, and a structured archive of governed documents with a librarian approving
|
note is a slug and a body: how something works, what went wrong, what the fix was, what
|
||||||
promotion. Both were reached through the predecessor's tool server over the bus the mesh removed
|
assumption turned out to be false.
|
||||||
([issue 147](../../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md)).
|
|
||||||
Nothing in the mesh reaches them now, and nothing in the mesh has replaced them: there is no note
|
|
||||||
store, no archive, no librarian, and the lessons of the last days were written into this repository by
|
|
||||||
hand. That is a gap, and it is stated here rather than papered over. What replaces a symptom-indexed
|
|
||||||
memory, if anything does, is undecided.
|
|
||||||
|
|
||||||
## The record
|
It is indexed on **symptoms**. The entry someone needs is usually titled after the error they
|
||||||
|
are staring at, which is why the standing instruction is to search the literal error text
|
||||||
|
before forming a hypothesis rather than after one fails.
|
||||||
|
|
||||||
**The design record is read where it is written.** Since 2026-09-30 a module, `records`, keeps a
|
Its content is overwhelmingly the record of previous debugging: a large body of
|
||||||
checkout of this repository from the forge — cloned from the `git` seat, reset to the origin on every
|
troubleshooting entries, module conventions, and standing notes about work that is open. It is
|
||||||
merge the forge announces and every ten minutes — and answers over the bus: where a phrase appears as
|
the mesh's institutional memory of *what has already gone wrong*.
|
||||||
written, one document whole, what a folder holds, and where the checkout stands, each naming the
|
|
||||||
commit it read. Which repository it reads is a setting on its assignment; the module names no mesh.
|
|
||||||
|
|
||||||
It is listed by the console beside every other tool, with a description that says to search the
|
The cost of skipping it is documented in the mesh's own record: entries have been rediscovered
|
||||||
literal words of a symptom before forming a hypothesis. That is what
|
from scratch, over hours, in sessions where the search was skipped because the trail felt
|
||||||
[ADR 0025](../../02-DECISIONS/0025-the-design-record-is-read-not-copied.md) meant by *beside
|
confident. It fires hardest on familiar ground, not unfamiliar ground.
|
||||||
everything else*, in a mesh with no store to be beside
|
|
||||||
([ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)).
|
|
||||||
|
|
||||||
Nothing is copied. A checkout lags the source by seconds after an announced merge and by minutes
|
## The structured archive
|
||||||
otherwise, and says so.
|
|
||||||
|
|
||||||
## The constitution
|
A second store, structured rather than flat: spaces, pages, revisions, tiers, and full-text
|
||||||
|
search. Where the operational memory is a note, this is a document with an owner and a
|
||||||
|
lifecycle.
|
||||||
|
|
||||||
[`00-META/how-we-build.md`](../../00-META/how-we-build.md) is the source of the mesh constitution
|
Content is promoted through tiers — private, then team, then platform — with a librarian agent
|
||||||
([ADR 0021](../../02-DECISIONS/0021-hq-is-the-source-of-the-constitution.md)). The page it used to be
|
owning approval and promotion at the boundary. Proposals to edit are reviewed rather than
|
||||||
synchronised into lived in the predecessor's archive and is unreachable; the constitution today is
|
applied.
|
||||||
read from this repository, through the same module, and playbook 05's sync has nothing to write to.
|
|
||||||
|
This is where the mesh's **governed** documents live, including the constitution injected into
|
||||||
|
design sessions ([ADR 0020](../../02-DECISIONS/0020-the-mesh-is-governed-by-a-constitution.md)).
|
||||||
|
|
||||||
|
## Why both
|
||||||
|
|
||||||
|
The distinction is by lifecycle, not by subject.
|
||||||
|
|
||||||
|
| Operational memory | Structured archive |
|
||||||
|
|---|---|
|
||||||
|
| Written the moment something is learned | Written deliberately, reviewed |
|
||||||
|
| Flat, symptom-indexed | Structured, tiered, owned |
|
||||||
|
| Anyone writes; nothing approves | Promotion is approved |
|
||||||
|
| Truth is "this happened" | Truth is "this is agreed" |
|
||||||
|
|
||||||
|
Collapsing them would cost one of the two properties: either every hard-won note waits for
|
||||||
|
review, or governed documents can be changed by anyone mid-incident.
|
||||||
|
|
||||||
## Where this repository sits
|
## Where this repository sits
|
||||||
|
|
||||||
A third thing beside two that are gone, which makes it the first: the one governed record the mesh
|
This repository is a third thing, and the objection was raised when it was created: a fourth
|
||||||
has, public, read by a module the mesh assigns, and edited nowhere else.
|
knowledge system repeats the mistake the split was made to fix.
|
||||||
|
|
||||||
|
The answer given was **indexing, not location** — that these documents are indexed into the
|
||||||
|
knowledge base so that a symptom search returns them alongside everything else. One source,
|
||||||
|
many surfaces.
|
||||||
|
|
||||||
|
**That indexing does not currently exist.** A search for this repository's content returns
|
||||||
|
nothing. The claim is load-bearing for the decision to separate the repository at all, and
|
||||||
|
until it is true, this repository is exactly the fourth knowledge system the objection
|
||||||
|
described. Recorded here because it is a statement about how the mesh's knowledge actually
|
||||||
|
works today, and as [`04-ISSUES/006`](../../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md).
|
||||||
|
|
||||||
|
## The librarian
|
||||||
|
|
||||||
|
A single agent owns the archive's approvals and promotions. Its approval capabilities have at
|
||||||
|
times not been reachable as tools, which does not affect the operational memory but does mean
|
||||||
|
promotion stops silently — the store keeps accepting proposals that nothing can approve.
|
||||||
|
|||||||
@@ -82,22 +82,6 @@ to clone.
|
|||||||
The schema column added for this defaults to empty rather than null, because "not on a seat" is a
|
The schema column added for this defaults to empty rather than null, because "not on a seat" is a
|
||||||
real answer, so every row recorded before the change keeps exactly the meaning it had.
|
real answer, so every row recorded before the change keeps exactly the meaning it had.
|
||||||
|
|
||||||
## A seat's protocol is on its row, and the controller serves its own
|
|
||||||
|
|
||||||
*Since 2026-09-30 ([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)).*
|
|
||||||
The `seat` table carries `accepts`, `emits` and `serves`; `serves` holds each verb with its description
|
|
||||||
and input schema. The rows were seeded from the compiled defaults the first time a controller with the
|
|
||||||
columns migrated, and each later migration adds any verb the defaults name that a row lacks, never
|
|
||||||
removing one. `UseSeats` still falls back to the compiled protocol for a row with none, which after the
|
|
||||||
first seeding is no row.
|
|
||||||
|
|
||||||
The `mesh-controller` seat serves twelve verbs — `tools`, `status`, `nodes`, `node`, `modules`, `seats`,
|
|
||||||
`builds`, `plan`, `assign`, `unassign`, `push`, `build` — on `mesh.seat.mesh-controller.tool.<verb>`,
|
|
||||||
each answered by the controller running that command in its own binary and returning what it printed.
|
|
||||||
A module claiming a mesh seat with verbs must list them under `tools` or registration refuses it by
|
|
||||||
name. A node-scoped seat's tool is `mesh.seat.<seat>.tool.<verb>.<node>`; no node-scoped seat declares
|
|
||||||
one yet.
|
|
||||||
|
|
||||||
## Where this differs from the design
|
## Where this differs from the design
|
||||||
|
|
||||||
**Capacity is not implemented.** The design's vocabulary has a seat with a capacity, and a
|
**Capacity is not implemented.** The design's vocabulary has a seat with a capacity, and a
|
||||||
|
|||||||
@@ -1,64 +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, mesh-controller cmd/mesh-controller/seatverbs.go]
|
|
||||||
updated: 2026-09-30
|
|
||||||
decisions:
|
|
||||||
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
|
|
||||||
- 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
|
|
||||||
- 02-DECISIONS/0037-where-a-module-lives.md
|
|
||||||
- 02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# The console, as it runs
|
|
||||||
|
|
||||||
**The mesh's tools reach a person through a module the mesh assigned to their machine.** Since
|
|
||||||
2026-09-30 a workstation that is a node can be assigned `mesh-console`; the mesh mints a bus account
|
|
||||||
`<node>.mesh-console`, seals its credential to the machine, and the container binds
|
|
||||||
`127.0.0.1:<port>` with the port the mesh assigned for the manifest's declared one. An agent on the
|
|
||||||
machine is pointed at `http://127.0.0.1:<port>/mcp` and sees the mesh's tools; a person uses the same
|
|
||||||
endpoint. Nothing on the machine holds a credential a person had to carry.
|
|
||||||
|
|
||||||
## What it answers
|
|
||||||
|
|
||||||
`initialize`, `tools/list`, `tools/call`, over HTTP, one JSON body per request, no session and no event
|
|
||||||
stream. `tools/list` is what the running modules answered: every tool runtime built on or after that day
|
|
||||||
serves a `tools` verb for its module, and the console asks the catalogue for the roster and each module
|
|
||||||
for its tools. A module that did not answer is named in the list's `_meta.notAnswering`. On the day it
|
|
||||||
shipped that was 36 of 51 modules — those that serve no tools at all, and those whose rebuilt runtime the
|
|
||||||
mesh records rather than rolls out — and 62 tools from the rest.
|
|
||||||
|
|
||||||
`tools/call` reaches any tool by `<module>.<tool>`, listed or not. The console's grant is `*`, so what it
|
|
||||||
may call is every tool on the mesh; its account may publish nothing else and subscribes nothing.
|
|
||||||
|
|
||||||
## The mesh's own verbs
|
|
||||||
|
|
||||||
*Since 2026-09-30 evening ([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)).*
|
|
||||||
The console asks the `mesh-controller` seat's `tools` verb beside the modules and lists every role's
|
|
||||||
tools as `<seat>.<verb>` — `mesh-controller.status`, `mesh-controller.push` and the other ten. A call
|
|
||||||
to `<prefix>.<name>` reaches the seat when the prefix is a seat declaring that verb, and the module
|
|
||||||
otherwise; `seat:<seat>.<verb>` says so outright. When the control plane does not answer, the list
|
|
||||||
names `mesh-controller (seat)` as not answering and carries the modules' tools regardless. The
|
|
||||||
`mesh-controller` *module* is always named as not answering: it serves no module tools, only its seat's.
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
@@ -15,13 +15,12 @@ Where the two disagree, the implementation wins and the disagreement is stated.
|
|||||||
| [`04-delivery.md`](04-delivery.md) | Push to running: the three silos, levels, and what a green pipeline proves |
|
| [`04-delivery.md`](04-delivery.md) | Push to running: the three silos, levels, and what a green pipeline proves |
|
||||||
| [`05-runtime-and-installation.md`](05-runtime-and-installation.md) | The node runtime, its modes, and how a node comes into being |
|
| [`05-runtime-and-installation.md`](05-runtime-and-installation.md) | The node runtime, its modes, and how a node comes into being |
|
||||||
| [`06-configuration-and-secrets.md`](06-configuration-and-secrets.md) | Managed files, value resolution, and where secrets live |
|
| [`06-configuration-and-secrets.md`](06-configuration-and-secrets.md) | Managed files, value resolution, and where secrets live |
|
||||||
| [`07-knowledge.md`](07-knowledge.md) | The mesh keeps no store: what it knows is what modules answer, and the record is read by one |
|
| [`07-knowledge.md`](07-knowledge.md) | The two knowledge stores, and what each is for |
|
||||||
| [`08-agents-and-work.md`](08-agents-and-work.md) | Agents as employees, tasks, workflows, and the meeting model |
|
| [`08-agents-and-work.md`](08-agents-and-work.md) | Agents as employees, tasks, workflows, and the meeting model |
|
||||||
| [`09-interfaces-and-observability.md`](09-interfaces-and-observability.md) | How the mesh is reached and watched — tools, board, proxy, health, thoughts |
|
| [`09-interfaces-and-observability.md`](09-interfaces-and-observability.md) | How the mesh is reached and watched — tools, board, proxy, health, thoughts |
|
||||||
| [`10-module-catalogue.md`](10-module-catalogue.md) | The catalogue's shape, and what its shape says |
|
| [`10-module-catalogue.md`](10-module-catalogue.md) | The catalogue's shape, and what its shape says |
|
||||||
| [`11-the-lab.md`](11-the-lab.md) | The lab — the first piece of the new shape that exists, and what it does not yet do |
|
| [`11-the-lab.md`](11-the-lab.md) | The lab — the first piece of the new shape that exists, and what it does not yet do |
|
||||||
| [`12-the-seats.md`](12-the-seats.md) | The seats the mesh defines, who holds one, and where a seat changes resolution |
|
| [`12-the-seats.md`](12-the-seats.md) | The seats the mesh defines, who holds one, and where a seat changes resolution |
|
||||||
| [`13-the-console.md`](13-the-console.md) | The mesh's tools on the machine a person sits at, served by a module the mesh assigned there |
|
|
||||||
|
|
||||||
## What these documents are not
|
## What these documents are not
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,8 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: in-progress
|
status: in-progress
|
||||||
code: [mesh-host]
|
code: [mesh-host]
|
||||||
updated: 2026-09-29
|
updated: 2026-09-22
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0141-the-host-delivers-its-own-successor.md
|
|
||||||
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
|
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
|
||||||
- 02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md
|
- 02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md
|
||||||
- 02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md
|
- 02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md
|
||||||
@@ -424,45 +423,3 @@ ignored an instruction and "applied" would be a lie. Applying stays one at a tim
|
|||||||
is not. **Checked** by the link's unit tests on the drain, and by the genesis bed's settle wait,
|
is not. **Checked** by the link's unit tests on the drain, and by the genesis bed's settle wait,
|
||||||
which counts on a node catching up to the newest declaration rather than the oldest.
|
which counts on a node catching up to the newest declaration rather than the oldest.
|
||||||
|
|
||||||
## The host delivers its own successor
|
|
||||||
|
|
||||||
*2026-09-29, from a change to the host that could reach no machine —
|
|
||||||
[issue 142](../../04-ISSUES/142-the-host-is-the-one-thing-the-mesh-does-not-deliver/00-report.md),
|
|
||||||
settled by [ADR 0141](../../02-DECISIONS/0141-the-host-delivers-its-own-successor.md).*
|
|
||||||
|
|
||||||
A merge builds every changed module and the control plane, and the result reaches the machines running
|
|
||||||
it with nobody asking. The host was the exception: not a build target, named by no declaration, and
|
|
||||||
identical on every machine because somebody had copied it there.
|
|
||||||
|
|
||||||
The supervision needed for this was already right. A clean exit from the host means it has stood aside,
|
|
||||||
and the launcher's next turn runs whatever is on disk. Consecutive failed starts are counted, a
|
|
||||||
rollback happens at the limit, and a second failure halts with the machine named rather than the binary.
|
|
||||||
What was missing was smaller than it looked: nothing told the running host a successor was waiting, and
|
|
||||||
the rollback resolved its known-good *version* through one operating system's package manager, which no
|
|
||||||
machine here used.
|
|
||||||
|
|
||||||
Keeping a version rather than a path was the clue. That is only useful to something that can choose
|
|
||||||
between versions present on the machine — so the versions live side by side:
|
|
||||||
|
|
||||||
- **A version arrives as an archive, in a directory named for it.** The ordinary resource, fetched by
|
|
||||||
digest and checked before anything is unpacked. The path written is never the path being executed, so
|
|
||||||
replacing a running binary — which the kernel refuses — never comes up.
|
|
||||||
- **The launcher starts the most recently delivered version**, reading no pointer and following no
|
|
||||||
link, because the version is in the path.
|
|
||||||
- **The running host stands aside between reconciles and never inside one.** Standing aside mid-apply is
|
|
||||||
the half-configured machine this document exists to prevent.
|
|
||||||
- **A version that completes a reconcile records itself and retires what is older than its
|
|
||||||
predecessor.** The predecessor stays, because that is what a rollback needs.
|
|
||||||
- **Rollback starts that predecessor** instead of reinstalling a package: no package manager, no cache
|
|
||||||
somebody else may clean, and the same script on every operating system.
|
|
||||||
- **A machine says which host version it runs**, on the report it already sends, so being behind is
|
|
||||||
answerable at all.
|
|
||||||
|
|
||||||
One copy by hand remains, once: the first host that understands versioned directories cannot be fetched
|
|
||||||
by a host that does not.
|
|
||||||
|
|
||||||
*How it is checked* is stated with the decision — a second version delivered to a running machine is
|
|
||||||
run and reported; the exit follows an in-flight apply rather than interrupting it; a version that will
|
|
||||||
not start is rolled back once and the second failure halts; a completed reconcile retires what is older
|
|
||||||
than the predecessor and never the predecessor; and the newest of two delivered versions is the one
|
|
||||||
that runs.
|
|
||||||
|
|||||||
@@ -11,9 +11,8 @@ code:
|
|||||||
- mesh-catalog modules/postgres
|
- mesh-catalog modules/postgres
|
||||||
- mesh-catalog modules/lavinmq
|
- mesh-catalog modules/lavinmq
|
||||||
- mesh-lab test/integration/mesh.test.ts (a bare machine becomes a mesh)
|
- mesh-lab test/integration/mesh.test.ts (a bare machine becomes a mesh)
|
||||||
updated: 2026-09-29
|
updated: 2026-09-22
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md
|
|
||||||
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
|
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
|
||||||
- 02-DECISIONS/0088-the-foundation-filters-before-anything-listens.md
|
- 02-DECISIONS/0088-the-foundation-filters-before-anything-listens.md
|
||||||
- 02-DECISIONS/0004-a-node-and-how-it-joins.md
|
- 02-DECISIONS/0004-a-node-and-how-it-joins.md
|
||||||
@@ -316,23 +315,3 @@ of a database and pushed to over the broker. What arrived and what did not is th
|
|||||||
**One fault, and it was in the joining.** The token did not say what the mesh calls the machine,
|
**One fault, and it was in the joining.** The token did not say what the mesh calls the machine,
|
||||||
so enrolment needed a flag its own help said it did not — and failed at the broker with an empty
|
so enrolment needed a flag its own help said it did not — and failed at the broker with an empty
|
||||||
username. Recorded in ADR 0004 as the fifth thing a token carries.
|
username. Recorded in ADR 0004 as the fifth thing a token carries.
|
||||||
|
|
||||||
## The mesh's own components arrive as binaries
|
|
||||||
|
|
||||||
*2026-09-29 —
|
|
||||||
[ADR 0142](../../02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md).*
|
|
||||||
|
|
||||||
The bundle carries three images and one of them is the controller, *because there is nothing to fetch
|
|
||||||
it with yet*. That reasoning holds and its conclusion changes: the controller is carried as a **binary**
|
|
||||||
reference rather than an image reference, pinned by digest exactly as before. Nothing about the bundle's
|
|
||||||
shape moves — it names a thing and the host fetches it — and the container runtime stops being something
|
|
||||||
genesis must raise before the control plane can exist. It still raises one, for the store and the broker,
|
|
||||||
which is where somebody else's software belongs.
|
|
||||||
|
|
||||||
The mesh's own components — the host, the controller, the catalogue, the builder, the vault — are
|
|
||||||
delivered as binaries into directories named for their versions, by the mechanism
|
|
||||||
[ADR 0141](../../02-DECISIONS/0141-the-host-delivers-its-own-successor.md) describes. Third-party
|
|
||||||
software stays a container. The split is not about isolation; it is about who built the thing.
|
|
||||||
|
|
||||||
Measured before deciding it: the mesh's own components publish no ports at all, so this opens nothing.
|
|
||||||
Only the store, the registry and the broker publish, and they are staying as they are.
|
|
||||||
|
|||||||
@@ -7,13 +7,8 @@ code:
|
|||||||
- mesh-controller internal/identity/authority.go
|
- mesh-controller internal/identity/authority.go
|
||||||
- mesh-host internal/identity/serving.go
|
- mesh-host internal/identity/serving.go
|
||||||
- mesh-host internal/apply (the service that reflects a rule set)
|
- mesh-host internal/apply (the service that reflects a rule set)
|
||||||
updated: 2026-09-30
|
updated: 2026-09-27
|
||||||
decisions:
|
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
|
|
||||||
- 02-DECISIONS/0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md
|
- 02-DECISIONS/0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md
|
||||||
- 02-DECISIONS/0106-the-bus-is-nats.md
|
- 02-DECISIONS/0106-the-bus-is-nats.md
|
||||||
- 02-DECISIONS/0105-the-mesh-adopts-the-predecessors-tunnel-in-place.md
|
- 02-DECISIONS/0105-the-mesh-adopts-the-predecessors-tunnel-in-place.md
|
||||||
@@ -291,31 +286,6 @@ hosts file by the runtime. That extends the file decision rather than overturnin
|
|||||||
mesh and not chosen by a module: a module that listed the machines would go stale the day one
|
mesh and not chosen by a module: a module that listed the machines would go stale the day one
|
||||||
joins, and a module that did not would be one whose containers cannot reach anything by name.
|
joins, and a module that did not would be one whose containers cannot reach anything by name.
|
||||||
|
|
||||||
**Superseded for containers by [ADR 0148](../../02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md)
|
|
||||||
(2026-09-30).** Everything above still describes the machine's own roster file, which is how it works
|
|
||||||
and how it will keep working. It no longer describes containers.
|
|
||||||
|
|
||||||
Copying the roster into each container made the roster part of each container's identity, so one name
|
|
||||||
moving replaced every container in the mesh — a module assigned on one machine restarted the store, the
|
|
||||||
registry, the edge and mail on another
|
|
||||||
([issue 151](../../04-ISSUES/151-a-new-name-recreates-every-container-in-the-mesh/00-report.md)). And it
|
|
||||||
did not stop the staleness it was meant to: a copy taken at creation is stale the moment the roster
|
|
||||||
moves, twice found as a container holding an address that had not existed for days
|
|
||||||
([issues 109](../../04-ISSUES/109-a-container-keeps-the-address-it-was-made-with/00-report.md)
|
|
||||||
and [135](../../04-ISSUES/135-a-containers-mesh-names-are-not-compared/00-report.md)).
|
|
||||||
|
|
||||||
**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.
|
|
||||||
|
|
||||||
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,
|
|
||||||
not a list of containers.
|
|
||||||
|
|
||||||
**The boundary, which is deliberate and worth stating:** *declared* containers. A container
|
**The boundary, which is deliberate and worth stating:** *declared* containers. A container
|
||||||
somebody starts by hand is not the mesh's to configure, and reaching into every container on a
|
somebody starts by hand is not the mesh's to configure, and reaching into every container on a
|
||||||
machine — declared or not — is what a nameserver in `resolv.conf` would be for.
|
machine — declared or not — is what a nameserver in `resolv.conf` would be for.
|
||||||
@@ -326,14 +296,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
|
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.
|
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
|
**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
|
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
|
*of* it: the mesh has no business shipping one, choosing which one, or knowing its configuration
|
||||||
@@ -409,9 +371,7 @@ can reach from the outside but cannot resolve from the inside is a name it canno
|
|||||||
authority of its own.
|
authority of its own.
|
||||||
|
|
||||||
**So a granted route is published into internal resolution as well** — the routed name to the node
|
**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
|
that serves it, mesh-wide, by the same mechanism that writes the node names. It is *given by the
|
||||||
name has no mesh form, and the suffixed alias the roster once added beside it resolved to a refusal
|
|
||||||
([issue 157](../../04-ISSUES/157-a-routed-names-internal-alias-is-served-by-nothing/00-report.md)). It is *given by the
|
|
||||||
mesh, not chosen by a module*, for the same reason the node names are: a module listing the routes
|
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
|
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
|
knows nothing about what they mean
|
||||||
@@ -665,50 +625,6 @@ the found firewall reloads and reachable from a container on the node, that a ma
|
|||||||
enrols through the openings before and after a reload and a reboot, and that after the flip the
|
enrols through the openings before and after a reload and a reboot, and that after the flip the
|
||||||
declared port is open and the undeclared one closed.
|
declared port is open and the undeclared one closed.
|
||||||
|
|
||||||
### It filters what arrives from outside, and not what the machine's own guests send
|
|
||||||
|
|
||||||
*2026-09-28, preparing the control-node's convergence —
|
|
||||||
[issue 141](../../04-ISSUES/141-the-forward-chain-does-not-follow-the-modules/00-report.md), settled by
|
|
||||||
[ADR 0140](../../02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md), which replaces
|
|
||||||
[ADR 0137](../../02-DECISIONS/0137-a-machine-says-which-networks-it-routes.md) and
|
|
||||||
[ADR 0139](../../02-DECISIONS/0139-a-network-is-forwarded-because-a-module-declared-it.md).*
|
|
||||||
|
|
||||||
Traffic passing through a machine is filtered, because a container's published port is traffic passing
|
|
||||||
through rather than traffic arriving at the machine itself. Having blocked it, the filter then had to
|
|
||||||
let the machine's own containers reach outward again — and it did that by listing the address ranges
|
|
||||||
they sit on. Two of those ranges were constants in this repository's code, and the rest were typed by an
|
|
||||||
operator after the flip had already cut a workstation's containers off from everything.
|
|
||||||
|
|
||||||
**The list was the mistake, not its contents.** A constant describes one machine. A typed range goes
|
|
||||||
stale and cannot tell a network the mesh made from one a predecessor left behind — on the control-node,
|
|
||||||
six ranges fall outside the constants and two of the six belong to services the mesh does not run. The
|
|
||||||
attempt to generate the list from the modules put half the rule set on the machine and made the
|
|
||||||
derivation advisory. Three records, one list.
|
|
||||||
|
|
||||||
**And the mesh has no position on a container reaching outward.** §4 exists to say which port is open
|
|
||||||
and to whom, which is about what arrives. A container of this machine's own opening a connection
|
|
||||||
somewhere is not a port opened to anybody, and the addresses it might do that from are the machine's
|
|
||||||
internal plumbing, which the mesh neither owns nor can know.
|
|
||||||
|
|
||||||
So the filter constrains what arrives from **outside** the machine and says nothing about what did not.
|
|
||||||
Traffic passing through is allowed unless it came in on one of the machine's outward links, and then
|
|
||||||
only where a declared endpoint's reach admits it (§6). The machine says which of its links face
|
|
||||||
outside — one fact it reports, like the kind of firewall it found and the tunnel it carries, not a
|
|
||||||
setting and not a list of addresses. It does not change when a module is added or removed, which is the
|
|
||||||
whole difference from what it replaces. A machine that has reported no outward link is sent no filter
|
|
||||||
at all, and keeps the one it has, because a rule written around a link with no name is a rule set that
|
|
||||||
does not load — a machine filtering nothing while its unit reports success.
|
|
||||||
|
|
||||||
Ports go on following the modules exactly as before: assign a module to a machine and the port its
|
|
||||||
assignment says it reaches on opens. Nothing about a network is said anywhere, by anybody.
|
|
||||||
|
|
||||||
*How it is checked:* a bed converges a machine carrying containers on several networks, none of them
|
|
||||||
named in any setting, and each reaches outward afterwards — which fails against the previous behaviour,
|
|
||||||
where the same flip cut them off, and is how it was written; a network made *after* the last declaration
|
|
||||||
needs no new filter; a declared port is reachable from off the private network and an undeclared one is
|
|
||||||
not; no address of a machine's own networks appears in a rendered filter, asserted on the text; and a
|
|
||||||
machine reporting no outward link is refused in the control plane with its existing filter left alone.
|
|
||||||
|
|
||||||
## 5 — Certificates
|
## 5 — Certificates
|
||||||
|
|
||||||
**Two authorities, kept separate on purpose.**
|
**Two authorities, kept separate on purpose.**
|
||||||
@@ -748,22 +664,6 @@ step, so when the authority moves the root is fetched again and the proxy is rec
|
|||||||
*How it is checked:* the route-forwarding bed installs the authority, the proxy and a consumer
|
*How it is checked:* the route-forwarding bed installs the authority, the proxy and a consumer
|
||||||
from the catalogue and asserts the routed name is served.
|
from the catalogue and asserts the routed name is served.
|
||||||
|
|
||||||
**And a machine trusts that authority because a module put its root in its trust store**
|
|
||||||
([ADR 0147](../../02-DECISIONS/0147-a-module-anchors-the-meshs-authority.md)). The proxy's fetch
|
|
||||||
answers for the proxy and for nothing else: a browser, `git` over HTTPS, a package manager and
|
|
||||||
every module calling another by an internal name read the machine's own trust store, and the mesh
|
|
||||||
had never written anything there. A module requiring the authority does the whole of it — fetch
|
|
||||||
the root over the mesh network, place it where this machine's TLS clients look, refresh the
|
|
||||||
extracted bundles — and stopping it, which is what being unassigned does, takes the anchor away
|
|
||||||
and refreshes them again. Not the controller's business, because being on the private network is
|
|
||||||
what makes the authority *reachable* and is not the same fact as having a reason to *verify* a
|
|
||||||
mesh name; and because where anchors live and which command refreshes them is one operating
|
|
||||||
system's difference, which is the host's half of the mesh
|
|
||||||
([ADR 0005](../../02-DECISIONS/0005-the-node-host.md)).
|
|
||||||
*How it is checked:* on a machine holding the module a plain client verifies an internal HTTPS
|
|
||||||
name with no bundle argument, and on one without it the same fetch fails to find an issuer — both
|
|
||||||
halves, because only the pair tells the anchor apart from something that already trusted it.
|
|
||||||
|
|
||||||
### What was built
|
### What was built
|
||||||
|
|
||||||
*2026-08-31.*
|
*2026-08-31.*
|
||||||
@@ -810,60 +710,6 @@ that verifies against the internal root and nothing else — which cannot succee
|
|||||||
first reached the name to certify it*
|
first reached the name to certify it*
|
||||||
([ADR 0066](../../02-DECISIONS/0066-public-routing-is-name-agnostic.md)).
|
([ADR 0066](../../02-DECISIONS/0066-public-routing-is-name-agnostic.md)).
|
||||||
|
|
||||||
## 6 — One statement behind exposure, filtering and certificates
|
|
||||||
|
|
||||||
*2026-09-28, preparing the control-node's convergence —
|
|
||||||
[issue 140](../../04-ISSUES/140-an-endpoints-reach-is-not-declared/00-report.md), settled by
|
|
||||||
[ADR 0138](../../02-DECISIONS/0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md).*
|
|
||||||
|
|
||||||
The three sections above each decide, independently, how far a service reaches. §3 composes a name
|
|
||||||
from a label and the node's domain. §4 opens a port to the source a listen named. §5 certifies the
|
|
||||||
names that exist, from whichever authority the proxy holds. Each is coherent on its own, and together
|
|
||||||
they mean **reachability is never stated anywhere** — it is the sum of three derivations, and a sum is
|
|
||||||
not something anyone can review or refuse.
|
|
||||||
|
|
||||||
What that costs, measured: an identity provider holding a public certificate valid 90 days and an
|
|
||||||
internal one valid 24 hours, neither asked for by any assignment, because both names existed and a
|
|
||||||
proxy certifies what it serves. And an endpoint that is not routed — git over ssh — which can be
|
|
||||||
spoken about only in the filter's vocabulary, so *this must be reachable from outside* is a setting
|
|
||||||
exactly one mechanism reads.
|
|
||||||
|
|
||||||
**An endpoint is the thing that was missing.** A module declares named endpoints: one port it serves,
|
|
||||||
what it is for, and what it would serve that to absent any instruction. A route contribution names an
|
|
||||||
endpoint rather than repeating a port number. An assignment — which is where a module's configuration
|
|
||||||
lives ([ADR 0046](../../02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md))
|
|
||||||
— then binds each endpoint to a machine port and says how far it reaches.
|
|
||||||
|
|
||||||
One value, three readers:
|
|
||||||
|
|
||||||
| reach | the filter opens | the proxy serves | the certificate comes from |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `internal` | the machine port, to the private network | the internal name | the mesh's own authority |
|
|
||||||
| `public` | the machine port, to anywhere | the public name | the public authority |
|
|
||||||
| `both` | the machine port, to anywhere | both names | each name's own authority |
|
|
||||||
|
|
||||||
**An endpoint that is not routed is reached and never named.** No route contribution means no name is
|
|
||||||
composed and no certificate requested, while the filter still acts on it. That is the case the model
|
|
||||||
could not express at all, and it is the ordinary case for anything that is not HTTP.
|
|
||||||
|
|
||||||
**Nothing moves until an assignment says so.** An endpoint whose assignment is silent keeps the
|
|
||||||
default its manifest states, so every machine already converged renders exactly as it does today —
|
|
||||||
the same property §4 needed when a machine gained a way to say which networks it routes.
|
|
||||||
|
|
||||||
This is what the certificate questions were waiting for. Which authority signs a name, whether a name
|
|
||||||
may appear in a public issuance log, and what must be trusted where are all answerable once an
|
|
||||||
endpoint says whether it is internal — and unanswerable while the proxy decides by composing every
|
|
||||||
name it can. It is also the fact
|
|
||||||
[issue 139](../../04-ISSUES/139-an-internal-route-name-resolves-to-the-consumers-node/00-report.md)
|
|
||||||
needs: an internal name should be composed from the machine serving the endpoint, which is the
|
|
||||||
assignment that bound it.
|
|
||||||
|
|
||||||
**How it is checked** is stated with the decision: one module with two endpoints of differing reach
|
|
||||||
asserted per chain body; the names and the certificate requests following the reach and failing
|
|
||||||
against today's behaviour, where both are always composed; an unrouted endpoint filtered and never
|
|
||||||
named; an assignment naming an endpoint the module does not declare refused where it is said; and a
|
|
||||||
silent assignment rendering byte-identically to today.
|
|
||||||
|
|
||||||
## What this removes
|
## What this removes
|
||||||
|
|
||||||
The list is worth having in one place, because it is most of the argument:
|
The list is worth having in one place, because it is most of the argument:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ code:
|
|||||||
- mesh-controller internal/builder
|
- mesh-controller internal/builder
|
||||||
- mesh-controller cmd/mesh-controller (build, build --behind, push, status)
|
- mesh-controller cmd/mesh-controller (build, build --behind, push, status)
|
||||||
- mesh-controller internal/inventory/builds.go
|
- mesh-controller internal/inventory/builds.go
|
||||||
updated: 2026-09-29
|
updated: 2026-09-21
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0090-a-failure-that-repeats-is-said-to-be-stuck.md
|
- 02-DECISIONS/0090-a-failure-that-repeats-is-said-to-be-stuck.md
|
||||||
- 02-DECISIONS/0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md
|
- 02-DECISIONS/0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md
|
||||||
@@ -226,34 +226,3 @@ when the current failure began and how many reports in a row have said it — th
|
|||||||
id, whatever the words; three make the machine stuck, and `status` says so beside the failure. The host keeps trying — stuck is what the mesh
|
id, whatever the words; three make the machine stuck, and `status` says so beside the failure. The host keeps trying — stuck is what the mesh
|
||||||
knows, not what the machine is told. *How it is checked:* an inventory test counts three identical
|
knows, not what the machine is told. *How it is checked:* an inventory test counts three identical
|
||||||
reports, a different one, and a clean apply; the status test asserts the word appears.
|
reports, a different one, and a clean apply; the status test asserts the word appears.
|
||||||
|
|
||||||
## Everything may call what is exposed to it, and local is not a boundary
|
|
||||||
|
|
||||||
*2026-09-29, from an outage that ran eleven hours —
|
|
||||||
[issue 145](../../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md),
|
|
||||||
settled by [ADR 0144](../../02-DECISIONS/0144-anything-on-a-machine-may-call-anything-on-it.md).*
|
|
||||||
|
|
||||||
A grant is four facts and a credential: the provision, the machine, the port, and who the consumer is
|
|
||||||
when it connects. It is the whole mechanism by which anything in the mesh reaches anything else, and it
|
|
||||||
rests on three things being callable — what runs on the same machine, another machine's service over the
|
|
||||||
private network where it is exposed there, and another machine's service over the public network where it
|
|
||||||
is exposed there.
|
|
||||||
|
|
||||||
The filter had two of those. A service exposed to the private network admitted the machines' own addresses
|
|
||||||
on it; a caller on the machine carries such an address, and a caller inside one of that machine's
|
|
||||||
containers carries a bridge address and matched nothing. Measured, same destination and same machine:
|
|
||||||
`src 10.10.0.1` from the machine, `src 172.17.0.8` from a container on it. So a module reaching its
|
|
||||||
database on its own machine's name timed out for eleven hours while the mesh called the machine healthy.
|
|
||||||
|
|
||||||
The second case worked by accident: a caller on another machine arrives over the tunnel carrying that
|
|
||||||
machine's address, which the rule matched. Two of three working is why this read as correct.
|
|
||||||
|
|
||||||
**So local is not a boundary this mesh draws, and the filter says so once.** Traffic that did not arrive
|
|
||||||
from outside the machine and did not arrive over the private network is the machine's own, and is
|
|
||||||
admitted — for every service there, not per service. Whether the caller is a container, a unit or a shell
|
|
||||||
decides nothing, because the question is "is this the same machine".
|
|
||||||
|
|
||||||
A verification mechanism was drafted for this and withdrawn. It would have reported the outage sooner and
|
|
||||||
would not have prevented it, and the part of it that was hard — deciding which network position to check
|
|
||||||
from — existed only because the rule was wrong. Whether the mesh should check that a grant works is still
|
|
||||||
open, in issue 145; it is not the remedy for a configuration error.
|
|
||||||
|
|||||||
@@ -7,10 +7,9 @@ code:
|
|||||||
- mesh-controller internal/catalogue/build.go
|
- mesh-controller internal/catalogue/build.go
|
||||||
- mesh-controller internal/inventory/secrets.go
|
- mesh-controller internal/inventory/secrets.go
|
||||||
- mesh-controller cmd/mesh-builder
|
- mesh-controller cmd/mesh-builder
|
||||||
updated: 2026-09-30
|
updated: 2026-09-12
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md
|
- 02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md
|
||||||
- 02-DECISIONS/0037-where-a-module-lives.md
|
|
||||||
- 02-DECISIONS/0009-modules-and-the-graph.md
|
- 02-DECISIONS/0009-modules-and-the-graph.md
|
||||||
- 02-DECISIONS/0010-delivery.md
|
- 02-DECISIONS/0010-delivery.md
|
||||||
- 02-DECISIONS/0005-the-node-host.md
|
- 02-DECISIONS/0005-the-node-host.md
|
||||||
@@ -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
|
unbuildable — pointed at the catalogue the builder finds no manifest, pointed at a module's source
|
||||||
it finds no manifest either.*
|
it finds no manifest either.*
|
||||||
|
|
||||||
## A manifest is checked where it is written
|
|
||||||
|
|
||||||
*Added 2026-09-30, from [issue 148](../../04-ISSUES/148-a-manifest-outside-this-catalogue-has-no-check/00-report.md).*
|
|
||||||
|
|
||||||
The check the mesh applies at registration — the manifest parses strictly, every name in it is a
|
|
||||||
usable one, its routes and events and seats are well formed, and no two manifests given together
|
|
||||||
declare one seat — is a verb on the controller's binary, `module check <manifest>…`, and it needs no
|
|
||||||
mesh. It reads the files it is given, runs the same functions registration runs, prints every problem
|
|
||||||
in the manifest's own words, and exits non-zero if there was one. Somebody describing their own
|
|
||||||
application in their own repository — the case [ADR 0037](../../02-DECISIONS/0037-where-a-module-lives.md)
|
|
||||||
calls the one that matters most — runs it before pushing, and finds out there rather than when a
|
|
||||||
running mesh refuses the registration, or later, when a machine applies something that resolved and
|
|
||||||
should not have.
|
|
||||||
|
|
||||||
**What it cannot know, it says.** A seat another module declares elsewhere is unknown to a check that
|
|
||||||
was not handed that module's manifest, and the output says so rather than refusing: pass the other
|
|
||||||
manifest too. The mesh's own seats it knows from the binary, which is the one place that set may be
|
|
||||||
read without a store ([ADR 0122](../../02-DECISIONS/0122-a-seat-is-data-a-rename-is-a-database-update.md)
|
|
||||||
keeps the store authoritative, so a claim on a mesh seat is judged fully only at registration, and the
|
|
||||||
check says that too).
|
|
||||||
|
|
||||||
*How it is checked:* the controller's test runs the check over the catalogue checkout beside it and
|
|
||||||
over a manifest with a known fault, and asserts the first passes and the second names the fault; the
|
|
||||||
test that used to be the only check, `TestEveryCatalogueManifestParses`, now stands beside a command
|
|
||||||
anybody can run.
|
|
||||||
|
|
||||||
## The manifest in the repository is not the manifest the mesh holds
|
## The manifest in the repository is not the manifest the mesh holds
|
||||||
|
|
||||||
A resource names an artifact:
|
A resource names an artifact:
|
||||||
|
|||||||
@@ -148,10 +148,6 @@ than reproduced from a declaration — because there is nothing to reproduce it
|
|||||||
([ADR 0025](../../02-DECISIONS/0025-the-design-record-is-read-not-copied.md)), not by holding a
|
([ADR 0025](../../02-DECISIONS/0025-the-design-record-is-read-not-copied.md)), not by holding a
|
||||||
copy. It is that reader; there is not a second agent for it.
|
copy. It is that reader; there is not a second agent for it.
|
||||||
|
|
||||||
*2026-09-30:* the reading is a module's — `records`, [35 — Reading the record](35-reading-the-record.md),
|
|
||||||
[ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md). The
|
|
||||||
session, when built, asks it rather than reading for itself; what it adds is judgement, not text.
|
|
||||||
|
|
||||||
**It answers into a symptom search**, so what it knows appears beside ordinary results rather than
|
**It answers into a symptom search**, so what it knows appears beside ordinary results rather than
|
||||||
only when it is asked. **And when it cannot be reached, the search says so.** A result set that
|
only when it is asked. **And when it cannot be reached, the search says so.** A result set that
|
||||||
silently omits this material looks identical to one where nothing matched — the same rule as the
|
silently omits this material looks identical to one where nothing matched — the same rule as the
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ code:
|
|||||||
- mesh-controller cmd/mesh-builder
|
- mesh-controller cmd/mesh-builder
|
||||||
- mesh-controller internal/builder
|
- mesh-controller internal/builder
|
||||||
- mesh-catalog modules/builder
|
- mesh-catalog modules/builder
|
||||||
updated: 2026-09-30
|
updated: 2026-09-25
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
|
|
||||||
- 02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md
|
|
||||||
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
|
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
|
||||||
- 02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md
|
- 02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md
|
||||||
- 02-DECISIONS/0096-an-upstream-image-is-copied-between-registries.md
|
- 02-DECISIONS/0096-an-upstream-image-is-copied-between-registries.md
|
||||||
@@ -186,8 +184,7 @@ disagrees with it.
|
|||||||
| `grants` | credentials it must create for its consumers |
|
| `grants` | credentials it must create for its consumers |
|
||||||
| `filtering` | rules beyond its own ports |
|
| `filtering` | rules beyond its own ports |
|
||||||
| `computed` | marks a module the controller generates rather than an author writing |
|
| `computed` | marks a module the controller generates rather than an author writing |
|
||||||
| `build.artifacts` | what it produces; an artifact's `context` may be a URL or a path on the git seat (`seat: git`), composed by the mesh that builds it |
|
| `build.artifacts` | what it produces |
|
||||||
| `names-on-purpose` | on a resource: each name it means to name — the world's federation server, a registry that built an application the mesh does not — with its reason. A definition names no installation, and a test says so ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)) |
|
|
||||||
|
|
||||||
**A container mounts only what the manifest declares**
|
**A container mounts only what the manifest declares**
|
||||||
([ADR 0091](../../02-DECISIONS/0091-a-mount-is-declared-three-ways.md)). A bind mount the module
|
([ADR 0091](../../02-DECISIONS/0091-a-mount-is-declared-three-ways.md)). A bind mount the module
|
||||||
@@ -235,7 +232,7 @@ counts as a copy and what as a base.
|
|||||||
| resource | is | a module may |
|
| resource | is | a module may |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `directory` | a directory with a mode and an owner | ✅ |
|
| `directory` | a directory with a mode and an owner | ✅ |
|
||||||
| `file` | literal content, with `${bound:…}`, `${secret:…}`, `${dir:…}`, `${port:…}`, `${machine:…}` and `${setting:…}` filled in — the last an operator's value from the assignment's settings, refused by name when unset ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)) | ✅ |
|
| `file` | literal content, with `${bound:…}` and `${secret:…}` filled in | ✅ |
|
||||||
| `user` | a login | ✅ |
|
| `user` | a login | ✅ |
|
||||||
| `access` | a pre-existing path it may use and must not own | ✅ |
|
| `access` | a pre-existing path it may use and must not own | ✅ |
|
||||||
| `archive` | files fetched by digest and unpacked | ✅ |
|
| `archive` | files fetched by digest and unpacked | ✅ |
|
||||||
@@ -266,27 +263,3 @@ ships one and wrong for code the mesh built, which has no unit until the mesh wr
|
|||||||
|
|
||||||
**Tools, hooks and consumers are not further modes**, which is the test of whether three is the
|
**Tools, hooks and consumers are not further modes**, which is the test of whether three is the
|
||||||
right number: they are loaded by a tool host, and a tool host is a process that stays up.
|
right number: they are loaded by a tool host, and a tool host is a process that stays up.
|
||||||
|
|
||||||
## The builder compiles the languages the mesh is written in
|
|
||||||
|
|
||||||
*2026-09-29 —
|
|
||||||
[ADR 0142](../../02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md).*
|
|
||||||
|
|
||||||
The toolchain list was typescript and python, and only typescript had a base module in the catalogue.
|
|
||||||
Meanwhile the control plane — written in the language this project is mostly written in — was built as
|
|
||||||
an image from a hand-written Dockerfile, which is the per-repository incantation this whole mechanism
|
|
||||||
exists to abolish.
|
|
||||||
|
|
||||||
So the list gains Go, with a base module providing the compiler exactly as typescript has one. The
|
|
||||||
obligation the list's own comment warns about — an SDK carrying the broker client, the event envelope
|
|
||||||
and tool serving — attaches to a **module** written in a language, not to the language being
|
|
||||||
compilable. The mesh's own components are not modules in that sense; the host is what applies modules.
|
|
||||||
|
|
||||||
**And an artifact says what it targets.** A compiled binary is per operating system, pinned at link
|
|
||||||
time, and a toolchain deliberately accepts nothing from the module — anything a module could override
|
|
||||||
there it would be writing a Dockerfile to override. The target is therefore a property of the artifact,
|
|
||||||
not of the recipe: one artifact declared per target, one build each.
|
|
||||||
|
|
||||||
A component's version stops being stamped in at link time. It is unpacked into a directory named for
|
|
||||||
its version, so it reads its version from its own path, and a build no longer has to know what it will
|
|
||||||
be called.
|
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ code:
|
|||||||
- mesh-catalog modules/showcase
|
- mesh-catalog modules/showcase
|
||||||
- mesh-controller internal/builder
|
- mesh-controller internal/builder
|
||||||
- mesh-sdk src
|
- mesh-sdk src
|
||||||
updated: 2026-09-30
|
updated: 2026-09-21
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
|
|
||||||
- 02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md
|
- 02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md
|
||||||
- 02-DECISIONS/0053-a-step-that-runs-on-a-schedule.md
|
- 02-DECISIONS/0053-a-step-that-runs-on-a-schedule.md
|
||||||
- 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
|
- 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ code:
|
|||||||
updated: 2026-09-27
|
updated: 2026-09-27
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0106-the-bus-is-nats.md
|
- 02-DECISIONS/0106-the-bus-is-nats.md
|
||||||
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
- 02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md
|
||||||
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
|
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
|
||||||
- 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
|
- 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
|
||||||
- 02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md
|
- 02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md
|
||||||
@@ -303,7 +303,7 @@ the private network. It is raised at genesis like the store, adopted as a module
|
|||||||
phase.
|
phase.
|
||||||
|
|
||||||
**The deprecated broker is an ordinary module, not a compatibility layer.** Revision, second review
|
**The deprecated broker is an ordinary module, not a compatibility layer.** Revision, second review
|
||||||
([ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md): AMQP is not a provision, and everything speaks to the `mesh-broker` seat)): earlier text here, and
|
([ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md)): earlier text here, and
|
||||||
ADR 0106 before it, called it `lavinmq-compat` — one purpose, the predecessor's clients, and a
|
ADR 0106 before it, called it `lavinmq-compat` — one purpose, the predecessor's clients, and a
|
||||||
retirement condition of no client connected for a period the operator sets. It is none of those.
|
retirement condition of no client connected for a period the operator sets. It is none of those.
|
||||||
A module may legitimately need an AMQP broker as a **backing service**, the way it needs a
|
A module may legitimately need an AMQP broker as a **backing service**, the way it needs a
|
||||||
@@ -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).
|
Nothing is built of this before §10's bed passes; the MCP surface is a thin adapter over (2).
|
||||||
|
|
||||||
*Built, and then made a module — 2026-09-30.* (1) and (2) exist: `operator issue` and the `mesh`
|
|
||||||
client. What (2) describes as a program on the workstation is now the recovery path; the surface an
|
|
||||||
operator uses is a module the mesh assigns to the machine, holding a credential the mesh minted —
|
|
||||||
[ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md),
|
|
||||||
[34 — The console](34-the-console.md). The tool list it asks for is no longer
|
|
||||||
`catalog_tools`, which nothing served: each runtime answers `tools` for its own module.
|
|
||||||
|
|
||||||
## 8. What a module sees, and what the wire does
|
## 8. What a module sees, and what the wire does
|
||||||
|
|
||||||
**The contract a module is written against does not change.** `publish` on an envelope becomes a
|
**The contract a module is written against does not change.** `publish` on an envelope becomes a
|
||||||
@@ -541,7 +534,7 @@ find what changed and why.
|
|||||||
**Still open:**
|
**Still open:**
|
||||||
|
|
||||||
- ~~Whether EVENTS should be one stream or one per emitting module.~~ **Closed**
|
- ~~Whether EVENTS should be one stream or one per emitting module.~~ **Closed**
|
||||||
([ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md): AMQP is not a provision, and everything speaks to the `mesh-broker` seat) (superseding [ADR 0125](../../02-DECISIONS/0125-the-bus-is-the-only-broker.md))): one stream, and not as a
|
([ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) (superseding [ADR 0125](../../02-DECISIONS/0125-the-bus-is-the-only-broker.md))): one stream, and not as a
|
||||||
preference — the streams the bus is made of are composed as configuration before any module
|
preference — the streams the bus is made of are composed as configuration before any module
|
||||||
runs, because a provisioner is itself a module that needs a bus account to start. Bootstrapping
|
runs, because a provisioner is itself a module that needs a bus account to start. Bootstrapping
|
||||||
decides it.
|
decides it.
|
||||||
|
|||||||
@@ -4,15 +4,12 @@ status: implemented
|
|||||||
code:
|
code:
|
||||||
- mesh-controller internal/catalogue/seats.go
|
- mesh-controller internal/catalogue/seats.go
|
||||||
- mesh-controller internal/catalogue/resolve.go
|
- mesh-controller internal/catalogue/resolve.go
|
||||||
- mesh-controller internal/inventory/seats.go
|
|
||||||
- mesh-controller internal/inventory/migrations/0039-a-seat-is-held-by-one-assignment-on-record.sql
|
|
||||||
- mesh-controller cmd/mesh-controller/seats.go
|
- mesh-controller cmd/mesh-controller/seats.go
|
||||||
- mesh-controller cmd/mesh-controller/source.go
|
- mesh-controller cmd/mesh-controller/source.go
|
||||||
- mesh-controller internal/inventory/migrations/0032-a-source-may-live-on-a-seat.sql
|
- mesh-controller internal/inventory/migrations/0032-a-source-may-live-on-a-seat.sql
|
||||||
- mesh-catalog modules/gitea/module.json
|
- mesh-catalog modules/gitea/module.json
|
||||||
updated: 2026-09-27
|
updated: 2026-09-27
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
|
||||||
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
|
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
|
||||||
- 02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md
|
- 02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md
|
||||||
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
|
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
|
||||||
@@ -45,31 +42,6 @@ is refused. A seat makes a role singular, never a module.
|
|||||||
**The seat points at the assignment.** Everything the mesh knows about the holder is what it knows
|
**The seat points at the assignment.** Everything the mesh knows about the holder is what it knows
|
||||||
about that assignment: the node, the node's settings for the module, and what the module serves.
|
about that assignment: the node, the node's settings for the module, and what the module serves.
|
||||||
|
|
||||||
**Which assignment holds a seat is a fact on record, and changes as one act.** Revision, 2026-09-27
|
|
||||||
([ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)). Until
|
|
||||||
then the holder was derived — the module that is assigned and claims the seat holds it, and a second
|
|
||||||
eligible assignment was refused. That has no way to pass a seat from one holder to the next without a
|
|
||||||
moment in which nobody holds it, and the controller finds its own bus through one of these seats: that
|
|
||||||
moment took the control plane down for an evening. So the holder is now one row the controller keeps,
|
|
||||||
written by a handover — `seat <name> --to <node>/<module>` — that names the seat and the assignment
|
|
||||||
taking it over and replaces the previous holder in the same write. Between two handovers the seat has
|
|
||||||
exactly one holder, and it is never none.
|
|
||||||
|
|
||||||
Three consequences follow. **A seat with no row is held as it always was**: the sole eligible
|
|
||||||
assignment holds it, and two eligible ones are refused — so a mesh that has never handed a seat over
|
|
||||||
behaves exactly as before, and the row appears the first time somebody does. **With a row, any other
|
|
||||||
assignment whose module could hold the seat is eligible and silent**: neither refused nor holding.
|
|
||||||
That is what lets the next holder run beside the current one until the handover, which the bus's move
|
|
||||||
needs ([28](28-building-the-bus.md), task 5.3). **And a holding is the assignment's**: unassigning the
|
|
||||||
holder takes the row with it, so a seat never points at something that is not running anywhere, and
|
|
||||||
the seat falls back to derivation rather than to nothing.
|
|
||||||
|
|
||||||
The handover refuses what would make the new holder wrong before anything is written: the seat must
|
|
||||||
exist, the assignment must exist, and the module must be able to hold the seat — claim it at its scope
|
|
||||||
and provide what it delivers, judged against the store's row and not against anything compiled into a
|
|
||||||
binary. It does not check that the module is running yet; `push` confirms that afterwards, and a
|
|
||||||
handover that could only be recorded after the new holder was up could not be the switch.
|
|
||||||
|
|
||||||
**The set is closed.** A seat the mesh does not define is refused wherever it is named, and so is one
|
**The set is closed.** A seat the mesh does not define is refused wherever it is named, and so is one
|
||||||
named at the wrong scope. Adding a seat is a decision, recorded, for the reason every addition to the
|
named at the wrong scope. Adding a seat is a decision, recorded, for the reason every addition to the
|
||||||
host's vocabulary is one: the set is what a person reads to learn what a mesh can have, and an entry
|
host's vocabulary is one: the set is what a person reads to learn what a mesh can have, and an entry
|
||||||
@@ -109,7 +81,7 @@ convention, which later seats departed from.
|
|||||||
| `mesh-store` | — | mesh | — | the store the mesh's own records live in |
|
| `mesh-store` | — | mesh | — | the store the mesh's own records live in |
|
||||||
| `mesh-broker` | — | mesh | `mesh-bus` | the broker carrying the mesh's own bus |
|
| `mesh-broker` | — | mesh | `mesh-bus` | the broker carrying the mesh's own bus |
|
||||||
| `mesh-vault` | — | mesh | `secret`, reserved | the vault |
|
| `mesh-vault` | — | mesh | `secret`, reserved | the vault |
|
||||||
| `mesh-artifact-store` | `the-artifact-store` (renamed 2026-09-30, [ADR 0156](../../02-DECISIONS/0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)) | mesh | `artifact-store` | the artifact registry |
|
| `mesh-artifact-store` | `the-artifact-store` | mesh | `artifact-store` | the artifact registry |
|
||||||
| `mesh-catalog` | `the-catalogue` | mesh | — | the catalogue |
|
| `mesh-catalog` | `the-catalogue` | mesh | — | the catalogue |
|
||||||
| `mesh-npm-package-registry` | `npm-package-registry` | mesh | `npm-package-registry` | the forge |
|
| `mesh-npm-package-registry` | `npm-package-registry` | mesh | `npm-package-registry` | the forge |
|
||||||
| `mesh-git` | `git` | mesh | `git` | the forge |
|
| `mesh-git` | `git` | mesh | `git` | the forge |
|
||||||
@@ -235,9 +207,7 @@ checked as their tables say:
|
|||||||
| `mesh-*` is the mesh's, and a module may not declare one | 0118: a registration test refusing a manifest that declares any `mesh-*` seat, naming the prefix. |
|
| `mesh-*` is the mesh's, and a module may not declare one | 0118: a registration test refusing a manifest that declares any `mesh-*` seat, naming the prefix. |
|
||||||
| Two modules cannot declare the same seat | 0118: a registration test; the second is refused and the first untouched. |
|
| Two modules cannot declare the same seat | 0118: a registration test; the second is refused and the first untouched. |
|
||||||
| A holder satisfies the seat's protocol | 0118: a claim whose module does not serve what the seat declares is refused at assignment. |
|
| A holder satisfies the seat's protocol | 0118: a claim whose module does not serve what the seat declares is refused at assignment. |
|
||||||
| A seat is held by one assignment, and only by one whose module can hold it | 0118: resolution tests for a second holder and for a seat the definition does not name. 0131: `CanHold` is the one judgement, shared by registration and the handover, and its test follows the store's row. |
|
| A seat is held by one assignment, and only by one whose module can hold it | 0118: resolution tests for a second holder and for a seat the definition does not name. |
|
||||||
| A holder on record settles the seat; another eligible assignment is silent, not refused | 0131: resolution tests with a recorded holder on the same machine, on another machine, and under a seat's former name; without a record, the old rule's tests still pass unchanged. |
|
|
||||||
| A handover replaces the holder as one write, needs an assignment to point at, and goes with it | 0131: store tests — a second handover leaves one row; a handover to a module not assigned where named is refused; unassigning the holder removes the row. |
|
|
||||||
| A requirement naming a seat is answered by its holder; a foundation seat cannot be named | 0118: resolution tests with a second provider on the consumer's node, with the seat unheld, and naming `mesh-store`. |
|
| A requirement naming a seat is answered by its holder; a foundation seat cannot be named | 0118: resolution tests with a second provider on the consumer's node, with the seat unheld, and naming `mesh-store`. |
|
||||||
| Several providers and none local is a person's choice | 0118: an assignment test listing candidates with the seat's holder first and recording the pin. |
|
| Several providers and none local is a person's choice | 0118: an assignment test listing candidates with the seat's holder first and recording the pin. |
|
||||||
| `secret` is reserved | 0118: the parser and resolution refusals for another provider and a pin. |
|
| `secret` is reserved | 0118: the parser and resolution refusals for another provider and a pin. |
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
---
|
---
|
||||||
layer: to-be
|
layer: to-be
|
||||||
status: in-progress
|
status: proposed
|
||||||
code: [mesh-controller internal/catalogue]
|
code: []
|
||||||
updated: 2026-09-30
|
updated: 2026-09-26
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
||||||
- 02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md
|
|
||||||
- 02-DECISIONS/0115-one-assignment-of-a-module-per-node.md
|
- 02-DECISIONS/0115-one-assignment-of-a-module-per-node.md
|
||||||
- 02-DECISIONS/0113-the-vault-makes-every-secret.md
|
- 02-DECISIONS/0113-the-vault-makes-every-secret.md
|
||||||
- 02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md
|
- 02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md
|
||||||
@@ -172,22 +171,6 @@ make a new external key, so rotating one means an operator handing over a new va
|
|||||||
assignment, and the route provider answers. A public name already held by another assignment is
|
assignment, and the route provider answers. A public name already held by another assignment is
|
||||||
refused, like any other singular thing.
|
refused, like any other singular thing.
|
||||||
|
|
||||||
**Built so far, 2026-09-30** ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)):
|
|
||||||
the operator's value in its first form — `${setting:<key>}` in a file's content, from the assignment's
|
|
||||||
settings layers, refused by name when nothing set it; a module told the name its route composes
|
|
||||||
(`${bound:<route>:name}`); a build context on the git seat; and the check that no definition names an
|
|
||||||
installation, with `names-on-purpose` for the names a definition means. The host's directory in its
|
|
||||||
first form is [`${dir:<id>}`](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md),
|
|
||||||
a placed directory under the node's root. Each is this design's provider in the shape the existing
|
|
||||||
placeholders have, not yet the one requirement form below; they are phase 1's first cases.
|
|
||||||
|
|
||||||
*Phase 3, in part (2026-09-30):* every definition's **own** data directory is placed; the conversion
|
|
||||||
moved no data, proven by resolving both catalogues with the controller's rule and comparing
|
|
||||||
([issue 119](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md)).
|
|
||||||
What the mesh writes *for* a module is still placed by the definition
|
|
||||||
([issue 174](../../04-ISSUES/174-the-meshs-own-files-for-a-module-are-placed-by-the-definition/00-report.md)),
|
|
||||||
which is the gap this design answered on 2026-09-26 and has not built.
|
|
||||||
|
|
||||||
## How a definition reads what was resolved
|
## How a definition reads what was resolved
|
||||||
|
|
||||||
**One form, naming a requirement and a field of its contract.** A definition that needs the database's
|
**One form, naming a requirement and a field of its contract.** A definition that needs the database's
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ code:
|
|||||||
- mesh-catalog modules/nats
|
- mesh-catalog modules/nats
|
||||||
- mesh-controller internal/catalogue
|
- mesh-controller internal/catalogue
|
||||||
- mesh-lab scenarios
|
- mesh-lab scenarios
|
||||||
updated: 2026-09-28
|
updated: 2026-09-27
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
|
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
|
||||||
- 02-DECISIONS/0106-the-bus-is-nats.md
|
- 02-DECISIONS/0106-the-bus-is-nats.md
|
||||||
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
- 02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md
|
||||||
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
|
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
|
||||||
- 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
|
- 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
|
||||||
- 02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md
|
- 02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md
|
||||||
@@ -535,7 +535,7 @@ it, and the beds that need a mesh living on NATS can finally run.
|
|||||||
The outcome carries the module name, because only the manifest says what was built and one
|
The outcome carries the module name, because only the manifest says what was built and one
|
||||||
message now has three readers. A failed build names none: it produced no module version, and
|
message now has three readers. A failed build names none: it produced no module version, and
|
||||||
the catalogue would otherwise place something that was never made.
|
the catalogue would otherwise place something that was never made.
|
||||||
- [x] 4.3 an installation completes over the bus, with the same outcome as the path it replaces —
|
- [~] 4.3 an installation completes over the bus, with the same outcome as the path it replaces —
|
||||||
**the installer can raise it**: a foundation template that stands up the server, writes the
|
**the installer can raise it**: a foundation template that stands up the server, writes the
|
||||||
server's own settings and the mesh's first user list beside them, and starts a controller
|
server's own settings and the mesh's first user list beside them, and starts a controller
|
||||||
reaching the new bus. What remains is running it, which is 4.1's bed.
|
reaching the new bus. What remains is running it, which is 4.1's bed.
|
||||||
@@ -655,72 +655,55 @@ healthy while reacting to nothing.
|
|||||||
- [ ] 5.1 the cutover bed: a mesh on AMQP with a predecessor stand-in on the deprecated broker
|
- [ ] 5.1 the cutover bed: a mesh on AMQP with a predecessor stand-in on the deprecated broker
|
||||||
moves its bus in one rollout, every node reporting on NATS afterwards, the stand-in's own
|
moves its bus in one rollout, every node reporting on NATS afterwards, the stand-in's own
|
||||||
client still connected throughout
|
client still connected throughout
|
||||||
- [x] 5.2 the rollout: accounts composed, then the controller, every host and every runtime
|
- [~] 5.2 the rollout: accounts composed, then the controller, every host and every runtime
|
||||||
together; every node confirmed heard before AMQP stops.
|
together; every node confirmed heard before AMQP stops.
|
||||||
|
|
||||||
**Done 2026-09-28, 02:25.** Every machine reports on the new bus, the seat is held by the
|
**The readiness half is in and is the half worth having.** The move takes every node at once, so
|
||||||
module that provides it, the old broker is unassigned and forgotten, and every credential was
|
there is nothing to inspect afterwards and no half to roll back — either the mesh was ready or it
|
||||||
minted afresh at the end because two had been printed on the way. What it took, in the order
|
was not. `rollout check` answers that from records, with one dial: is a bus answering, does a
|
||||||
it was found, each fixed on the trunk before the next step: the control plane's `serve` and
|
machine hold the seat, has it been sent the composed user list, does every machine and every
|
||||||
`push` never selected the new transport (task 4.3, open until then); a machine's user was
|
module that speaks have a credential. Each missing thing names its own next step, because "not
|
||||||
granted neither the asking nor the delivery of its own consumer; the account had no JetStream
|
ready" that cannot be acted on is not an answer at the point where the next step is irreversible.
|
||||||
of its own; the control plane's client verified the bus's certificate by name instead of
|
|
||||||
pinning it; the seat table's rows carried no protocol, so no role's work queue was raised; the
|
**A machine with no credential is what must stop it.** It keeps running, cannot come back, and
|
||||||
build machine decided its bus from a variable its container never received; and a rotation
|
afterwards there is no bus to tell it anything over.
|
||||||
put new hashes on the bus before three machines had received their new memberships — which
|
|
||||||
is why there is now `rollout hand <node>` and a host adopts a delivered membership at start.
|
The move itself is deliberately not written yet, and the command says so rather than pretending:
|
||||||
The bootstrap loop — a bus that can only be raised by a declaration that can only arrive
|
it waits on the check having been run against a real mesh. Writing the irreversible half before
|
||||||
over that bus — was broken once, by hand: the mesh's own composed configuration started the
|
the question it depends on has ever been asked of something real is how the plan's own rule about
|
||||||
server, and the controller binary was run on the node directly until the managed container
|
beds gets broken by another route.
|
||||||
could be rebuilt over the bus it was on.
|
|
||||||
|
> **What this costs if it goes wrong, measured rather than assumed.** On the installation this is
|
||||||
|
> for, the old broker is also what a whole automation layer outside the mesh connects to — so it
|
||||||
|
> stays, as an ordinary provider of `amqp` ([ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md)),
|
||||||
|
> and this step is not its retirement. Nothing in a served request's path goes over the mesh's own
|
||||||
|
> bus: modules serve from their own containers. What a failed move costs is the mesh's ability to
|
||||||
|
> *change* anything — pushes, tool calls, new provisioning — until it is finished or undone. That
|
||||||
|
> is worth knowing before rather than after, and it is why the operator's "as long as my services
|
||||||
|
> keep running" is a reasonable position rather than a gamble.
|
||||||
|
- [ ] 5.3 the mesh's own accounts removed from the deprecated broker, and then the broker itself:
|
||||||
|
after the rollout nothing of the mesh speaks to it, and an account nothing uses is one nobody
|
||||||
|
rotates. **It finishes now** ([ADR 0130](../../02-DECISIONS/0130-the-predecessor-is-ending-and-its-broker-goes-with-it.md)):
|
||||||
|
the predecessor is deprecated rather than kept, so once its remnants have stopped the module is
|
||||||
|
unassigned and the port is free. No retirement machinery — a provision with no consumers has its
|
||||||
|
provider unassigned, which is ADR 0127 being paid off rather than revised.
|
||||||
|
|
||||||
- [x] 5.3 **the seat changes hands as one act.** A command takes a seat and the assignment taking it
|
|
||||||
over, and the seat is never empty in between — the emptiness is the outage of 2026-09-27, when
|
|
||||||
the control plane, which finds its own bus through this seat, lost the address and looped.
|
|
||||||
**Built 2026-09-27** (`seat_holding`, migration 0039; design 26 says how it is checked), and used
|
|
||||||
live the next night to hand `mesh-broker` from the old broker's assignment to the new one's. This
|
|
||||||
is what 5.2 uses to move `mesh-broker` from the old
|
|
||||||
broker's assignment to the new one's, and it is built first ([ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)).
|
|
||||||
- [x] 5.4 **the old broker and everything that named AMQP leave the mesh** ([ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md),
|
|
||||||
superseding [ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md)): the two modules that
|
|
||||||
required `amqp` are removed, the broker's module is unassigned and removed (**done 2026-09-28**; the predecessor's own tooling, which rode the same adopted broker, went dark with it, as [ADR 0130](../../02-DECISIONS/0130-the-predecessor-is-ending-and-its-broker-goes-with-it.md) accepted), registration refuses
|
|
||||||
a manifest that provides or requires `amqp`, and a whole-catalogue check asserts none does. Not
|
|
||||||
a retirement condition — a decision, taken, with the operator's "I don't care if the predecessor
|
|
||||||
breaks" on record ([ADR 0130](../../02-DECISIONS/0130-the-predecessor-is-ending-and-its-broker-goes-with-it.md)).
|
|
||||||
Retiring with it: the build outcome's second announcement under the module's own name, which
|
Retiring with it: the build outcome's second announcement under the module's own name, which
|
||||||
existed only so a catalogue deployed before the rename and one after both heard it.
|
exists only so a catalogue deployed before the rename and one deployed after both hear it.
|
||||||
|
|
||||||
> **The remote tooling goes with it too.** The predecessor's own mesh talks over that broker, so
|
> **The remote tooling goes with it too.** The predecessor's own mesh talks over that broker, so
|
||||||
> shutting it down ends the path that reaches this installation's machines from a workstation.
|
> shutting it down ends the path that reaches this installation's machines from a workstation.
|
||||||
> The rollout is driven from the node, or before the broker stops — a sequencing constraint on
|
> The rollout has to be driven from the node, or driven before the broker stops — which is a
|
||||||
> 5.2, not an afterthought.
|
> sequencing constraint on 5.2 and not an afterthought.
|
||||||
- [x] 5.5 **the AMQP transport is deleted from the control plane and the hosts**. One bus, nothing
|
|
||||||
to select ([ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)).
|
|
||||||
|
|
||||||
**Done 2026-09-28.** The control plane's old consume loop, build request, tool call, management
|
> **5.4 is gone, and was wrong from ADR 0127 onward.** It read "the deprecated broker retires
|
||||||
API and account scoping went, and the host's old dialling and enrolment paths with them; a
|
> when its condition holds — no client connected for the period the operator sets", which is
|
||||||
membership or token naming any other bus is refused before anything is sent. Nothing selects a
|
> ADR 0106's framing of it as a compatibility module with an end date.
|
||||||
transport any more: the variable that once did (`MESH_BUS_NATS`) now only names where the
|
> [ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) settled that it is an
|
||||||
control plane reads its own bus credential, the way any module reads a secret. **Checked by the
|
> **ordinary provider** of the `amqp` provision, like any module answering a backing service:
|
||||||
build**: neither repository's module file names the AMQP client library, so a line that still
|
> no seat, not foundation, and **no retirement condition**, because the day its last client
|
||||||
used it would not compile. The store-window guarantee ([issue 083](../../04-ISSUES/083-other-control-messages-are-lost-while-the-store-restarts/00-report.md))
|
> disappears is not a day anything is waiting for. Removed rather than reworded — a step that
|
||||||
is tested against a bus-less fake rather than the old transport's memory, which is what let
|
> waits for a condition nobody set would sit open forever.
|
||||||
that memory go — the one thing it did that the stream does not (superseding a held report) is
|
|
||||||
the staleness check on the message itself (design 25 §3).
|
|
||||||
|
|
||||||
Found on the way: **no build had ever recorded what it stood on.** A recipe reads its base from
|
|
||||||
a build argument, so the digest was never in the file the builder derived edges from, and every
|
|
||||||
order that says *bases first* — `build --on`, `build --behind`, the merge follow-up of
|
|
||||||
[issue 131](../../04-ISSUES/131-nothing-tells-the-mesh-a-source-moved/00-report.md) — walked a
|
|
||||||
graph with no edges. The builder now reports the bases it was handed, the control plane records
|
|
||||||
them by artifact path, and the graph is read from the newest build of each module — a recorded
|
|
||||||
manifest carries no `build.on`, so the edge is derived from the build or it does not exist.
|
|
||||||
|
|
||||||
> **The old 5.4 note is history.** It recorded that a retirement *condition* was wrong from
|
|
||||||
> [ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) onward, which framed the old broker
|
|
||||||
> as an ordinary provider with no end. [ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md) ends that
|
|
||||||
> framing in turn: the broker is not kept as a provider either, because AMQP is not a provision. Both
|
|
||||||
> readings are kept here so the two reversals can be read in order.
|
|
||||||
|
|
||||||
**Done when.** Every node reports on NATS, and nothing of the mesh's own is left connected to the
|
**Done when.** Every node reports on NATS, and nothing of the mesh's own is left connected to the
|
||||||
deprecated broker.
|
deprecated broker.
|
||||||
|
|||||||
@@ -1,29 +1,17 @@
|
|||||||
---
|
---
|
||||||
layer: to-be
|
layer: to-be
|
||||||
status: in-progress
|
status: proposed
|
||||||
code:
|
code: []
|
||||||
- mesh-controller internal/catalogue/declaration.go
|
updated: 2026-09-27
|
||||||
- mesh-controller internal/catalogue/manifest.go
|
|
||||||
- mesh-controller internal/link/serve.go
|
|
||||||
- mesh-controller internal/link/bus.go
|
|
||||||
- mesh-controller internal/broker/nats.go
|
|
||||||
- mesh-controller internal/inventory/nodes.go
|
|
||||||
- mesh-host internal/apply/apply.go
|
|
||||||
- mesh-tools src/main.ts
|
|
||||||
- mesh-catalog modules/mesh-catalog
|
|
||||||
updated: 2026-09-28
|
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
|
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
|
||||||
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
- 02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md
|
||||||
- 02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md
|
- 02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md
|
||||||
- 02-DECISIONS/0106-the-bus-is-nats.md
|
- 02-DECISIONS/0106-the-bus-is-nats.md
|
||||||
- 02-DECISIONS/0041-events-are-a-relationship.md
|
- 02-DECISIONS/0041-events-are-a-relationship.md
|
||||||
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
||||||
- 02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md
|
- 02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md
|
||||||
- 02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md
|
- 02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md
|
||||||
- 02-DECISIONS/0135-a-module-version-prepares-its-state-before-it-runs.md
|
|
||||||
- 02-DECISIONS/0136-a-step-gates-its-module-not-the-machine.md
|
|
||||||
- 02-DECISIONS/0134-the-mesh-says-what-it-applied.md
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 32. What a module declares, and what the bus makes of it
|
# 32. What a module declares, and what the bus makes of it
|
||||||
@@ -270,30 +258,10 @@ queue.
|
|||||||
and publishes it last-per-subject. A node that was away gets exactly the current one, never a
|
and publishes it last-per-subject. A node that was away gets exactly the current one, never a
|
||||||
queue of superseded ones, and a replayed older one is refused by sequence.
|
queue of superseded ones, and a replayed older one is refused by sequence.
|
||||||
|
|
||||||
**A version prepares its state before it runs.** *Built 2026-09-28.* A module version may declare an
|
|
||||||
entrypoint that brings its state to the shape that version needs — the same vocabulary as the entrypoints it declares for its
|
|
||||||
tools and its provisioner, and nothing about how a machine runs it. The mesh runs that entrypoint as it
|
|
||||||
runs the module's own code, to completion, in the module's own context, and a version whose preparation
|
|
||||||
did not succeed does not run: the step gates that module and nothing else on the machine
|
|
||||||
([ADR 0136](../../02-DECISIONS/0136-a-step-gates-its-module-not-the-machine.md)), and the rollout stops
|
|
||||||
at the first machine that did not take it
|
|
||||||
([ADR 0135](../../02-DECISIONS/0135-a-module-version-prepares-its-state-before-it-runs.md), superseding
|
|
||||||
[ADR 0133](../../02-DECISIONS/0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md)).
|
|
||||||
Once per state, and the mesh derives what a state is: a consumer is a module on a machine, so what the
|
|
||||||
mesh provisions is per consumer and preparation is too. No level to choose, and no race to lock against.
|
|
||||||
|
|
||||||
**Applying is reported to a role.** The host applies and reports to the `mesh-controller` seat —
|
**Applying is reported to a role.** The host applies and reports to the `mesh-controller` seat —
|
||||||
not to an address it was given at genesis. Held and retried while the store restarts
|
not to an address it was given at genesis. Held and retried while the store restarts
|
||||||
([ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)).
|
([ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)).
|
||||||
|
|
||||||
**And the mesh says what it applied.** *Built 2026-09-28.* A report is control traffic only the control plane reads, so the
|
|
||||||
chain above went dark at the moment it touched a machine: nothing said which version a machine now runs,
|
|
||||||
or that it refused to. The control plane states those as facts under its own seat's namespace, when what
|
|
||||||
a machine runs changes rather than on every convergence pass, and anything that cares subscribes the way
|
|
||||||
the catalogue subscribes to `built` ([ADR 0134](../../02-DECISIONS/0134-the-mesh-says-what-it-applied.md)).
|
|
||||||
The facts are second-hand by design — one emitter, one ordering — and a machine that cannot reach the bus
|
|
||||||
produces none, so absence is not health.
|
|
||||||
|
|
||||||
What disappears across that chain is every address. No webhook URL, no registered callback, no
|
What disappears across that chain is every address. No webhook URL, no registered callback, no
|
||||||
"which node is the builder on", no controller endpoint baked into a joining node. That is the
|
"which node is the builder on", no controller endpoint baked into a joining node. That is the
|
||||||
class of bug
|
class of bug
|
||||||
@@ -389,7 +357,7 @@ controller — because the bus's accounts are configuration rather than somethin
|
|||||||
creates, so there is no provisioner process in the path and nothing waiting on a bus account in
|
creates, so there is no provisioner process in the path and nothing waiting on a bus account in
|
||||||
order to make bus accounts. A module that runs a NATS server of its own and offers it as a
|
order to make bus accounts. A module that runs a NATS server of its own and offers it as a
|
||||||
backing service provides **`nats`**, exactly as the deprecated broker provides `amqp`
|
backing service provides **`nats`**, exactly as the deprecated broker provides `amqp`
|
||||||
([ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md): AMQP is not a provision, and everything speaks to the `mesh-broker` seat)).
|
([ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md)).
|
||||||
|
|
||||||
They are never the same name. A manifest saying `nats` could otherwise mean either the mesh's
|
They are never the same name. A manifest saying `nats` could otherwise mean either the mesh's
|
||||||
nervous system or a private queue, and the difference between those is the whole architecture.
|
nervous system or a private queue, and the difference between those is the whole architecture.
|
||||||
@@ -459,7 +427,7 @@ it as an ordinary module once the registry exists.
|
|||||||
|
|
||||||
So there are exactly two things the normal path cannot make, both at genesis, both ending the
|
So there are exactly two things the normal path cannot make, both at genesis, both ending the
|
||||||
moment the mesh can mint for itself: **the bus's own accounts** (§the bootstrap argument in
|
moment the mesh can mint for itself: **the bus's own accounts** (§the bootstrap argument in
|
||||||
[ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md): AMQP is not a provision, and everything speaks to the `mesh-broker` seat) (superseding [ADR 0125](../../02-DECISIONS/0125-the-bus-is-the-only-broker.md)) — a provisioner is a module and
|
[ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) (superseding [ADR 0125](../../02-DECISIONS/0125-the-bus-is-the-only-broker.md)) — a provisioner is a module and
|
||||||
needs an account before it can run) and **the vault's own credential**. Any third exception is a
|
needs an account before it can run) and **the vault's own credential**. Any third exception is a
|
||||||
design failure, and naming these two is what makes a third one visible.
|
design failure, and naming these two is what makes a third one visible.
|
||||||
|
|
||||||
@@ -471,10 +439,6 @@ it is the residue of a question the rest of §8 answers and the part a fingerpri
|
|||||||
**Whether a module may declare a seat it does not itself claim** — the contract as one thing, the
|
**Whether a module may declare a seat it does not itself claim** — the contract as one thing, the
|
||||||
implementation as another, which is how two competing implementations would ever exist.
|
implementation as another, which is how two competing implementations would ever exist.
|
||||||
|
|
||||||
**Whether a container should have a readiness notion.** Only an action carries `verify`, so a step that
|
|
||||||
must run once a service *answers* — seeding through its own API — cannot be declared at all. Named here
|
|
||||||
because the steps above make the gap obvious, not because they caused it.
|
|
||||||
|
|
||||||
**Whether `consumes` naming another module couples too tightly.** It is kept here deliberately —
|
**Whether `consumes` naming another module couples too tightly.** It is kept here deliberately —
|
||||||
an event's provenance is its meaning — but a consumer of `billing.order.placed` does depend on
|
an event's provenance is its meaning — but a consumer of `billing.order.placed` does depend on
|
||||||
billing existing under that name.
|
billing existing under that name.
|
||||||
@@ -483,11 +447,6 @@ billing existing under that name.
|
|||||||
|
|
||||||
- **A manifest holds no subject.** A catalogue test: no manifest contains a string matching the
|
- **A manifest holds no subject.** A catalogue test: no manifest contains a string matching the
|
||||||
subject grammar. The rule is worthless if it is followed by convention.
|
subject grammar. The rule is worthless if it is followed by convention.
|
||||||
- **A preparation is given what the module is given.** A composition test: what the preparation
|
|
||||||
entrypoint receives equals what the module's own code receives, asserted rather than written twice —
|
|
||||||
which is the drift a hand-written step invites, three times over in the catalogue today.
|
|
||||||
- **A convergence that changed nothing says nothing.** Two identical reports, one emitted fact: what is
|
|
||||||
guarded against is a fact per minute per machine, which is a stream nobody reads.
|
|
||||||
- **Permissions are exactly the three namespaces.** A composition test per module: the derived
|
- **Permissions are exactly the three namespaces.** A composition test per module: the derived
|
||||||
permission set equals what its declaration implies, and a hand-written addition to it fails.
|
permission set equals what its declaration implies, and a hand-written addition to it fails.
|
||||||
- **A sender cannot read the queue it writes to.** A bed: a module declaring `uses` is refused
|
- **A sender cannot read the queue it writes to.** A bed: a module declaring `uses` is refused
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
---
|
|
||||||
layer: to-be
|
|
||||||
status: implemented
|
|
||||||
code: [mesh-controller, mesh-tools]
|
|
||||||
updated: 2026-09-30
|
|
||||||
decisions:
|
|
||||||
- 02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md
|
|
||||||
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
|
||||||
- 02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md
|
|
||||||
- 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 33 — The tools the mesh answers
|
|
||||||
|
|
||||||
**An agent can call the mesh's tools and cannot find out what they are.** Both halves were measured
|
|
||||||
on the live mesh on 2026-09-28: a client holding an operator credential connected to the bus, asked a
|
|
||||||
module for its repositories and got them; the same client's request for the tool list found nothing
|
|
||||||
serving it. The transport works, the account model works, the adapter that speaks the agent protocol
|
|
||||||
works. What is missing is the mesh being able to say what it can do.
|
|
||||||
|
|
||||||
This design is the answer to that question, and it has three families in it, because a tool belongs to
|
|
||||||
whoever is accountable for answering it.
|
|
||||||
|
|
||||||
## 1. Three families, and why the split is not arbitrary
|
|
||||||
|
|
||||||
| Family | Addressed to | Where the definition lives | Example |
|
|
||||||
|---|---|---|---|
|
|
||||||
| A **role's** tools | the seat: `mesh.seat.<seat>.tool.<verb>` | the seat's protocol, in the mesh's records | ask *the forge* to list its repositories |
|
|
||||||
| A **module's** tools | the module: `mesh.mod.<module>.tool.<name>` | that module's code | ask *this gitea* for `gitea_list_repos` |
|
|
||||||
| The **mesh's** own verbs | the `mesh-controller` seat | the seat's protocol, as above | `status`, `push`, `build`, `assign` |
|
|
||||||
|
|
||||||
The split follows accountability. A role is something the mesh guarantees exactly one holder of, so
|
|
||||||
what the role answers is the mesh's to define and a holder's to implement
|
|
||||||
([ADR 0132](../../02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md)). A module's
|
|
||||||
own tools are nobody's business but the module's, and their definitions live where they are
|
|
||||||
implemented, because a copy kept anywhere else drifts from the code that answers.
|
|
||||||
|
|
||||||
The mesh's own verbs are the third family only in where they come from, not in kind: the control plane
|
|
||||||
holds a seat like anything else, and its tools are that seat's. This is what keeps them addressable
|
|
||||||
while the control plane is being replaced, which is the moment they are most needed.
|
|
||||||
|
|
||||||
**Both names for one capability is deliberate and bounded to this.** A forge holding the `git` seat
|
|
||||||
answers the role's `list_repos` and its own `gitea_list_repos`, because the same module may run
|
|
||||||
without the seat — a second instance, kept for one purpose — and then only the second name is true.
|
|
||||||
The caller chooses which question it is asking. Nothing else in the mesh gets two names.
|
|
||||||
|
|
||||||
## 2. What a seat's tool is
|
|
||||||
|
|
||||||
A verb, what it does, and the schema of its arguments and its answer. A name alone is not callable by
|
|
||||||
something that has never seen the mesh before, which is the whole population this surface exists for.
|
|
||||||
|
|
||||||
The protocol a seat carries today is three lists of bare verbs
|
|
||||||
([ADR 0129](../../02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md)), and it must widen to
|
|
||||||
carry the rest. Two constraints on that widening:
|
|
||||||
|
|
||||||
- **It lives in the mesh's records, not in the control plane's binary.** Today a seat's protocol comes
|
|
||||||
from compiled defaults, merged in as a row is read, because the seat rows never gained the columns.
|
|
||||||
Discovery that reads a binary is discovery that disagrees with the mesh the moment the two are on
|
|
||||||
different versions.
|
|
||||||
- **The schema is stated in the form an agent protocol already uses**, so nothing translates between a
|
|
||||||
seat's idea of an argument and the caller's. A translation layer would be a second definition of
|
|
||||||
what a tool is.
|
|
||||||
|
|
||||||
## 3. Holding a seat means serving its tools
|
|
||||||
|
|
||||||
A module may not occupy a seat unless it serves every verb that seat declares. This joins the
|
|
||||||
conditions of holding that already exist — providing what the seat delivers, being assigned at the
|
|
||||||
seat's scope — and is refused the same way: at registration and at handover, naming the verbs that are
|
|
||||||
missing rather than the fact that something is.
|
|
||||||
|
|
||||||
A module knows which seats it claims, so knowing which tools it must serve is not a discovery problem
|
|
||||||
for the module: the seat says, the module implements, and anything beyond that is its own.
|
|
||||||
|
|
||||||
## 4. Addressing a node-scoped seat
|
|
||||||
|
|
||||||
A seat's subject is flat today — `mesh.seat.<seat>.<kind>.<verb>` — which is correct for a seat the
|
|
||||||
mesh has one holder of and wrong for the six node-scoped seats, where one subject would reach every
|
|
||||||
machine's holder and the holders' queue group would hand the call to whichever answered first. A
|
|
||||||
node-scoped seat's tool therefore carries the node it is asked of. Nothing about a mesh-scoped seat
|
|
||||||
changes.
|
|
||||||
|
|
||||||
## 5. Discovery
|
|
||||||
|
|
||||||
**What a role answers is a read.** The seats and their protocols are records, so the list is a query
|
|
||||||
against the mesh's own store: no call to a module in the path, nothing that has to be running, and an
|
|
||||||
answer that stays true while a holder is restarting or being replaced.
|
|
||||||
|
|
||||||
**What a module answers comes from the module.** Its definitions live in its code, so it is asked, and
|
|
||||||
the answer is as available as the module is — which is the right coupling for a tool that only exists
|
|
||||||
while that module does.
|
|
||||||
|
|
||||||
A caller therefore gets one list assembled from two sources, and the difference is visible in it: a
|
|
||||||
role's tool names a seat, a module's names a module. An agent that wants to survive a holder being
|
|
||||||
replaced binds to the first.
|
|
||||||
|
|
||||||
## 6. What serves this to an agent
|
|
||||||
|
|
||||||
A module the mesh assigns to the machine where the agent runs, holding a credential the mesh minted,
|
|
||||||
with authority derived from what it may call — not a program started by hand with a credential printed
|
|
||||||
to a terminal. The adapter itself already exists and is thin by design; what changes is that it stops
|
|
||||||
being something a person carries and becomes something the mesh runs, on a node, like everything else.
|
|
||||||
|
|
||||||
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
|
|
||||||
break a caller takes the version token the subject already has room for, and the two versions run side
|
|
||||||
by side until nothing is bound to the old one.
|
|
||||||
|
|
||||||
## How it is checked
|
|
||||||
|
|
||||||
- **A holder missing a verb cannot take the seat.** One test per condition of holding, as the existing
|
|
||||||
conditions have, and the live refusal names the verbs.
|
|
||||||
- **A verb nobody declared is a subject nobody may use.** The bus grants are derived from the seat's
|
|
||||||
protocol already, and the golden composition of the user list is what keeps that honest: a holder is
|
|
||||||
granted exactly the seat's verbs, a user of the seat exactly the publish side.
|
|
||||||
- **Discovery needs no running module.** The test for a role's tools reads records and asserts the
|
|
||||||
answer equals what the seats declare — if it needed a module up, it would not be a read.
|
|
||||||
- **Two nodes holding one node-scoped seat derive two addresses.** Checked by the same test as the rest
|
|
||||||
of the subject table.
|
|
||||||
|
|
||||||
## What is built, 2026-09-30
|
|
||||||
|
|
||||||
Under [ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md): §2's
|
|
||||||
two constraints (the protocol in the store's row, seeded additively; a verb with description and
|
|
||||||
schema, a bare name still accepted), §3 for the mesh's seats (holding refused by naming the missing
|
|
||||||
verbs), §4 (a node-scoped seat's tool carries the node as its last subject token; a user publishes
|
|
||||||
`*`), and the third family — twelve verbs on the `mesh-controller` seat, each running the command it
|
|
||||||
names in the controller's own binary. §5's first half is served rather than read: the seat's `tools`
|
|
||||||
verb answers every seat's tools from the records, because the console cannot read the store; the
|
|
||||||
console lists a role's tools beside the modules' own and resolves `<seat>.<verb>` to the seat when the
|
|
||||||
seat declares that verb. Which verbs each *other* seat serves stays a decision per seat, still untaken.
|
|
||||||
|
|
||||||
## What shipped, 2026-09-30
|
|
||||||
|
|
||||||
mesh-controller PRs 166 and 167, mesh-tools PR 21. Verified on the live mesh the same evening: the
|
|
||||||
console on a workstation lists the twelve verbs as `mesh-controller.<verb>` beside 67 module tools, and
|
|
||||||
`mesh-controller.nodes` and `mesh-controller.status` answer through it with what the commands print.
|
|
||||||
|
|
||||||
Two things shipped bent. **The grant arrived after the holder started**: the controller composes the
|
|
||||||
bus's user list and a push delivers it, so the first controller to serve its seat subscribed before
|
|
||||||
the broker's list named the grant, the server refused all twelve subscriptions, and the client never
|
|
||||||
retried — a holder now rebinds a refused subscription every thirty seconds (PR 167), and a controller
|
|
||||||
roll-out that adds a grant is followed by a push to the broker node. **A JSON verb's answer was parsed
|
|
||||||
from both output streams**, so `status --json`'s warnings hid the document as data; the answer is now
|
|
||||||
parsed from standard output alone (mesh-controller PR 168, pending). The `output` field carried it
|
|
||||||
either way.
|
|
||||||
|
|
||||||
What stays as designed and not built: which verbs any *other* seat serves, and §3 for module-declared
|
|
||||||
seats' schemas beyond the names their manifests already list.
|
|
||||||
|
|
||||||
## What this does not settle
|
|
||||||
|
|
||||||
- Which verbs each seat should serve. That is a decision per seat, and the reason to do it slowly: a
|
|
||||||
seat's tools bind every future holder.
|
|
||||||
- Whether a module's own tool definitions should also be recorded when a build resolves its manifest.
|
|
||||||
There is an argument for it — the mesh could then answer for a module that is down — and an argument
|
|
||||||
against, which is that a recorded copy of a live definition is a copy that can be wrong.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0132](../../02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md) — the decision this designs
|
|
||||||
- [ADR 0129](../../02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md) — a seat carries the protocol of its role
|
|
||||||
- [ADR 0095](../../02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md) — a tool call passes one process where an audit belongs
|
|
||||||
- [`26-the-seats.md`](26-the-seats.md) — what a seat is, how it is held and handed over
|
|
||||||
- [`25-the-bus-on-nats.md`](25-the-bus-on-nats.md) §7 — a person's account, their inbox, and the adapter
|
|
||||||
@@ -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,99 +0,0 @@
|
|||||||
---
|
|
||||||
layer: to-be
|
|
||||||
status: implemented
|
|
||||||
code: [mesh-catalog modules/records]
|
|
||||||
updated: 2026-09-30
|
|
||||||
decisions:
|
|
||||||
- 02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md
|
|
||||||
- 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
|
|
||||||
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 35 — Reading the record
|
|
||||||
|
|
||||||
**The design record, answered from a checkout the mesh keeps, at the commit it read.** A module,
|
|
||||||
`records`, holds a working copy of a repository of markdown — this one, for this mesh — and answers
|
|
||||||
where a phrase appears, what a document says, what a folder holds and where the copy stands. The
|
|
||||||
console lists those answers beside every other tool
|
|
||||||
([ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)).
|
|
||||||
|
|
||||||
## 1. What it keeps, and why that is not a copy
|
|
||||||
|
|
||||||
A git checkout, cloned from the forge that holds the `git` seat, brought up to date on every merge the
|
|
||||||
forge announces and every ten minutes besides. The bytes are the repository's; nothing is derived
|
|
||||||
from them and stored. What [ADR 0025](../../02-DECISIONS/0025-the-design-record-is-read-not-copied.md)
|
|
||||||
refused was a second store that is *searched* while the first is *edited*, drifting silently. A
|
|
||||||
checkout cannot drift; it can lag, and the lag is in every answer as the commit it was read at and
|
|
||||||
in `records_status` as when it was last brought up to date.
|
|
||||||
|
|
||||||
The checkout is reset to the origin on every sync, never merged: it is the mesh's, so a local change
|
|
||||||
is nobody's.
|
|
||||||
|
|
||||||
## 2. What it answers
|
|
||||||
|
|
||||||
| tool | answers |
|
|
||||||
|---|---|
|
|
||||||
| `records_search` | every place a phrase appears, as written and case-insensitively: document, line, the nearest heading above it; bounded, and says when it was |
|
|
||||||
| `records_read` | one document, whole, or its first part with a note when very long |
|
|
||||||
| `records_list` | what a folder holds: sub-folders and documents |
|
|
||||||
| `records_status` | repository, forge, commit and its date, last sync, document count, last error |
|
|
||||||
| `records_sync` | bring the checkout up to date now |
|
|
||||||
|
|
||||||
No ranking and no summary, on purpose: a record is found by its own words, and the reasoning is in
|
|
||||||
the document, not in the tool.
|
|
||||||
|
|
||||||
## 3. What it is told, and what it refuses to guess
|
|
||||||
|
|
||||||
Three things, none from a manifest ([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md)):
|
|
||||||
the directory its checkout lives in (a resource the mesh gives it), the forge's address (the `git`
|
|
||||||
provision's binding, written into a file as `scheme://host:port`), and the repository's path on the
|
|
||||||
forge (a **setting**, `{"repository": "<owner>/<name>"}`). Without the third it serves no tools and its
|
|
||||||
log says so. Public repositories only; it holds no credential.
|
|
||||||
|
|
||||||
## 4. How it is found
|
|
||||||
|
|
||||||
The console asks every module what it serves and lists `records_search` with a description that says
|
|
||||||
when to call it — *search the literal words of a symptom or a term before forming a hypothesis*. That
|
|
||||||
is 0025's second half in today's mesh: there is no store to be beside, and an agent choosing from a
|
|
||||||
tool list is the search.
|
|
||||||
|
|
||||||
## 5. Where it runs
|
|
||||||
|
|
||||||
Anywhere a node has the forge in reach; one assignment is enough, and a second on another machine is
|
|
||||||
harmless. The mesh session of design 15, when it exists, calls this rather than reading for itself.
|
|
||||||
|
|
||||||
## How it is checked
|
|
||||||
|
|
||||||
| Check | Defends |
|
|
||||||
|---|---|
|
|
||||||
| a phrase in one document of a repository the test makes comes back from that document, with the commit; a second commit on the origin is pulled and the next answer names it | ADR 0025's check, ADR 0153 |
|
|
||||||
| a path outside the checkout is refused; an empty search is refused | the reader reads the repository and nothing else |
|
|
||||||
| a sync against an unreachable origin leaves the checkout standing and says why | silence and success never look alike |
|
|
||||||
| live: through the console, `records_search` for a phrase that appears only in a design document here returns it | issue 006's closing check |
|
|
||||||
|
|
||||||
## What shipped, 2026-09-30
|
|
||||||
|
|
||||||
mesh-catalog PR 183, then PR 185. Verified on the live mesh the same evening: `records` assigned to the
|
|
||||||
control node with `{"repository": …}` as its setting, its checkout at the repository's `main` with 478
|
|
||||||
documents, its five tools listed by the console beside every other tool, and — ADR 0025's check —
|
|
||||||
`records_search` for a phrase from this document's title returned it from where it is written, with
|
|
||||||
the commit. The first live search missed: the phrase chosen from ADR 0025 straddled a line break under
|
|
||||||
emphasis, and the reader matched single lines. PR 185 matches a line together with the next and
|
|
||||||
ignores emphasis marks, which the module's test now covers; until it rolls, a phrase that wraps is one
|
|
||||||
to shorten.
|
|
||||||
|
|
||||||
The reader's `consumes` names the forge module's event rather than the `git` seat, because the seat
|
|
||||||
declares none; a merge into the repository was seen and pulled within seconds.
|
|
||||||
|
|
||||||
## What this does not settle
|
|
||||||
|
|
||||||
- Ranking or meaning. A search that understands a question is the session's job, not the reader's.
|
|
||||||
- A private repository. That is a credential the module would have to hold, and a decision about
|
|
||||||
what may read what.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)
|
|
||||||
- [ADR 0025](../../02-DECISIONS/0025-the-design-record-is-read-not-copied.md)
|
|
||||||
- [34 — The console](34-the-console.md) — what lists it
|
|
||||||
- [issue 006](../../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md)
|
|
||||||
@@ -40,7 +40,7 @@ document is written and this one's status becomes `implemented`.
|
|||||||
| [`28-building-the-bus.md`](28-building-the-bus.md) | **Proposed.** The five steps of the bus work in the order their dependencies allow, each ending at a bed — with the surface measured, so no step's size is a guess | [ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md), [ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md), [ADR 0074](../../02-DECISIONS/0074-the-wire-is-specified-not-the-types.md) |
|
| [`28-building-the-bus.md`](28-building-the-bus.md) | **Proposed.** The five steps of the bus work in the order their dependencies allow, each ending at a bed — with the surface measured, so no step's size is a guess | [ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md), [ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md), [ADR 0074](../../02-DECISIONS/0074-the-wire-is-specified-not-the-types.md) |
|
||||||
| [`29-a-node-has-operator-accounts.md`](29-a-node-has-operator-accounts.md) | **Proposed.** The mesh models machines but not the humans on them: a node gains operator accounts, and a resource may live under a home owned by its account — what would own ~/.ssh, dotfiles and ~/.config when HAL retires | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md) |
|
| [`29-a-node-has-operator-accounts.md`](29-a-node-has-operator-accounts.md) | **Proposed.** The mesh models machines but not the humans on them: a node gains operator accounts, and a resource may live under a home owned by its account — what would own ~/.ssh, dotfiles and ~/.config when HAL retires | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md) |
|
||||||
|
|
||||||
| [`32-what-a-module-declares.md`](32-what-a-module-declares.md) | **Proposed.** What a module declares and what the bus derives from it: three namespaces, subjects from local names, queues never declared, the five relationships, and the build-publish-deploy lifecycle on one bus | [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md), [ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md), superseded by [ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md) (superseding [ADR 0125](../../02-DECISIONS/0125-the-bus-is-the-only-broker.md)), [ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) |
|
| [`32-what-a-module-declares.md`](32-what-a-module-declares.md) | **Proposed.** What a module declares and what the bus derives from it: three namespaces, subjects from local names, queues never declared, the five relationships, and the build-publish-deploy lifecycle on one bus | [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md), [ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) (superseding [ADR 0125](../../02-DECISIONS/0125-the-bus-is-the-only-broker.md)), [ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) |
|
||||||
|
|
||||||
## Not yet written
|
## Not yet written
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
status: resolved
|
status: located
|
||||||
opened: 2026-08-23
|
opened: 2026-08-23
|
||||||
located-in: [mesh-catalog modules/records, mesh-catalog modules/mesh-console]
|
located-in: [hal, hq]
|
||||||
fixed-by: ADR 0153; mesh-catalog PR 183 (records), PR 185 (a phrase that wraps)
|
fixed-by:
|
||||||
amended-design: 03-DESIGN/01-to-be/35-reading-the-record.md
|
amended-design: 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
|
||||||
---
|
---
|
||||||
|
|
||||||
# 006 — This repository is not indexed into the knowledge base, and the claim that it is holds up a decision
|
# 006 — This repository is not indexed into the knowledge base, and the claim that it is holds up a decision
|
||||||
@@ -155,54 +155,3 @@ design document here, and get it back. That check fails today by design.
|
|||||||
|
|
||||||
**What stands until then** is the signpost, and the honest description of it: reachable, not
|
**What stands until then** is the signpost, and the honest description of it: reachable, not
|
||||||
surfacing.
|
surfacing.
|
||||||
|
|
||||||
## Where this stands, 2026-09-29
|
|
||||||
|
|
||||||
*Added in a grooming pass.* The knowledge base this record is about is the **predecessor's**, and it
|
|
||||||
is no longer reachable from anything: the surface that answered `recall_search` speaks the transport
|
|
||||||
the mesh removed at the cut-over
|
|
||||||
([issue 147](../147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md)).
|
|
||||||
|
|
||||||
So the sentence in `README.md` that this record catches — *these documents are still indexed into
|
|
||||||
the knowledge base* — is now wrong twice over: nothing indexed them, and there is nothing to index
|
|
||||||
them into. The record stays open, and its answer is no longer "index this repository somewhere"; it
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Built, 2026-09-30
|
|
||||||
|
|
||||||
[ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md): the
|
|
||||||
reader is a module, `records` (mesh-catalog PR 183), keeping a checkout of this repository from the
|
|
||||||
forge and answering `records_search`, `records_read`, `records_list`, `records_status` and
|
|
||||||
`records_sync` at the commit it read; the console lists them beside every other tool, which is where
|
|
||||||
"beside everything else" lives in a mesh with no store. Design
|
|
||||||
[35 — Reading the record](../../03-DESIGN/01-to-be/35-reading-the-record.md). The module's test runs
|
|
||||||
0025's check against a repository it makes; this record closes when the same check passes through the
|
|
||||||
console on the live mesh, and says so below.
|
|
||||||
|
|
||||||
## Resolved, 2026-09-30
|
|
||||||
|
|
||||||
The check ADR 0025 names passed on the live mesh: through the console on a workstation,
|
|
||||||
`records_search` for a phrase that appears in one design document here returned that document and the
|
|
||||||
commit it was read at, from a checkout the mesh keeps and nobody copied. What this record asked on
|
|
||||||
2026-08-23 — *does the searcher find it without already suspecting it exists?* — is answered by where
|
|
||||||
the tool sits: in the same list as the forge's and the mesh's own, described as the thing to search
|
|
||||||
before forming a hypothesis. Reachable became surfacing when the surface became a list.
|
|
||||||
|
|
||||||
Open beside it, and not this record's: the mesh has no symptom-indexed memory at all since the
|
|
||||||
cut-over ([as-is 07](../../03-DESIGN/00-as-is/07-knowledge.md) says so), and the lessons of these
|
|
||||||
days are in this repository by hand.
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: resolved
|
status: open
|
||||||
opened: 2026-09-22
|
opened: 2026-09-22
|
||||||
located-in: [mesh-controller internal/link/protocol.go (the report field that was missing), internal/inventory, cmd/mesh-controller (node show and status)]
|
located-in: []
|
||||||
fixed-by: mesh-controller 7683ba8, corrected by 1d9c102
|
fixed-by:
|
||||||
amended-design:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,143 +0,0 @@
|
|||||||
# 087 — resolved: the mesh knows which host runs a machine
|
|
||||||
|
|
||||||
*2026-09-30.*
|
|
||||||
|
|
||||||
## The field existed and was thrown away on arrival
|
|
||||||
|
|
||||||
The machine has reported its host version since
|
|
||||||
[ADR 0141](../../02-DECISIONS/0141-the-host-delivers-its-own-successor.md) — `Host` on the report, with
|
|
||||||
a comment saying why it must be there: *"without it nothing can say a machine is behind."*
|
|
||||||
|
|
||||||
**The controller's own copy of the report did not have the field.** Two structs describe one message,
|
|
||||||
one on each side of the wire, and only the sending side had it — so it unmarshalled into nothing and the
|
|
||||||
mesh could not answer a question the machine had been answering for a week. That is the whole of this
|
|
||||||
issue's mechanism, and it is worth stating plainly because neither side was wrong on its own.
|
|
||||||
|
|
||||||
## What it says now
|
|
||||||
|
|
||||||
`node show` names it per machine:
|
|
||||||
|
|
||||||
```
|
|
||||||
last heard from here
|
|
||||||
host 2026-09-30-0214
|
|
||||||
```
|
|
||||||
|
|
||||||
`not reported — this machine has not said since the mesh began keeping it` where the mesh has not been
|
|
||||||
told, because a machine that has not said is a different thing from a machine running nothing.
|
|
||||||
|
|
||||||
`status` names the machines that are behind another:
|
|
||||||
|
|
||||||
```
|
|
||||||
1 machine(s) run an older host than another machine does:
|
|
||||||
ace 2026-09-29-0113
|
|
||||||
|
|
||||||
the newest any machine reports is 2026-09-30-0214. A host refuses a declaration carrying a
|
|
||||||
field it does not know, whole — so a new field reaches these machines last
|
|
||||||
```
|
|
||||||
|
|
||||||
## Disagreement, not staleness, and that is deliberate
|
|
||||||
|
|
||||||
The open questions asked whether the controller should refuse to send a declaration a node cannot
|
|
||||||
parse. It cannot yet, honestly: **nothing delivers a host version** (ADR 0141 is accepted and not
|
|
||||||
built, which is [issue 142](../142-the-host-is-the-one-thing-the-mesh-does-not-deliver/00-report.md)),
|
|
||||||
so the mesh holds no canonical current version and "behind" has no fixed point to be behind.
|
|
||||||
|
|
||||||
What it can say truthfully is that these machines do not all run the same host, and which is newest of
|
|
||||||
the ones it has been told about. That is the fact that matters before a declaration gains a field: **the
|
|
||||||
oldest host in the mesh is what the mesh may send.**
|
|
||||||
|
|
||||||
Two deliberate refusals to guess:
|
|
||||||
|
|
||||||
- **A machine that has reported nothing is not called behind.** It may be running anything. `node show`
|
|
||||||
says it has not said, per machine, which is the honest form.
|
|
||||||
- **Versions compare as strings.** That suits the timestamps and commits this mesh uses and is wrong
|
|
||||||
for a scheme where `10` sorts before `9`. Said in the code at the place that would have to learn,
|
|
||||||
rather than left as a surprise.
|
|
||||||
|
|
||||||
## What I shipped first was wrong, and the mesh said so within the hour
|
|
||||||
|
|
||||||
The first version reported *"N machine(s) run an older host than another machine does"* and worked out
|
|
||||||
which by comparing versions as strings. **A host reports its version as a commit, and commits have no
|
|
||||||
order.**
|
|
||||||
|
|
||||||
On the live mesh, with the adopted machine pushed for the first time:
|
|
||||||
|
|
||||||
```
|
|
||||||
3 machine(s) run an older host than another machine does:
|
|
||||||
g14 04a27ca
|
|
||||||
novox 04a27ca
|
|
||||||
shanks 04a27ca
|
|
||||||
```
|
|
||||||
|
|
||||||
Those three run the **newer** host — installed 09:18, against the adopted machine's 01:13. `ced54d4`
|
|
||||||
sorts above `04a27ca` and that is all it means. An arbitrary lexicographic result, presented as a fact,
|
|
||||||
about the one thing this record exists to make trustworthy.
|
|
||||||
|
|
||||||
The code carried a caveat saying versions compare as strings and that this "is enough for the timestamps
|
|
||||||
and commits this mesh uses". That was the error, written down and not noticed: it is enough for
|
|
||||||
timestamps and it is **meaningless** for commits, and the mesh reports commits.
|
|
||||||
|
|
||||||
It now reports the split and claims no ordering:
|
|
||||||
|
|
||||||
```
|
|
||||||
4 machine(s) do not all run the same host:
|
|
||||||
04a27ca g14, novox, shanks
|
|
||||||
ced54d4 ace
|
|
||||||
|
|
||||||
a host refuses a declaration carrying a field it does not know, whole — so the mesh may
|
|
||||||
send only what every one of these understands. Which of them is newer is not readable
|
|
||||||
from a commit; that needs a version the host reports as ordered
|
|
||||||
```
|
|
||||||
|
|
||||||
More useful as well as more honest — the reader sees who is on which side of the split, which is what
|
|
||||||
decides whether a field can be sent — and it leaves the ordering where it belongs: with the host, which
|
|
||||||
would have to report something ordered for anybody to have it.
|
|
||||||
|
|
||||||
**This is [issue 145](../145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md)
|
|
||||||
arriving by my own door**, an hour after closing it: a report that confidently says the opposite of the
|
|
||||||
truth is worse than one that says less, because it trains a reader to distrust the whole surface.
|
|
||||||
|
|
||||||
## Measured on the mesh, and one limitation it exposed
|
|
||||||
|
|
||||||
All four machines run the identical host binary — same digest, installed within eighteen seconds of each
|
|
||||||
other — and at first only one reported its version. The other three said *not reported*, which read as a
|
|
||||||
difference between machines where there was none.
|
|
||||||
|
|
||||||
**A machine states its host version only when the mesh sends it a declaration.** The report is published
|
|
||||||
after an apply; the periodic reconcile that runs every five minutes publishes nothing, because it is the
|
|
||||||
machine keeping itself as declared rather than answering anything. So a machine that is current and idle
|
|
||||||
never says, and the mesh cannot distinguish that from a machine running something ancient.
|
|
||||||
|
|
||||||
Confirmed by pushing: before, `not reported`; after, `04a27ca` — the same version the machine that had
|
|
||||||
been pushed already reported.
|
|
||||||
|
|
||||||
```
|
|
||||||
novox host 04a27ca
|
|
||||||
shanks host 04a27ca
|
|
||||||
g14 host 04a27ca
|
|
||||||
ace host not reported — this machine has not said since the mesh began keeping it
|
|
||||||
```
|
|
||||||
|
|
||||||
`ace` has not been pushed since the field existed; it is adopted and parked.
|
|
||||||
|
|
||||||
**This is enough for the purpose and not enough for the claim.** For deciding whether a new declaration
|
|
||||||
field is safe it is sufficient, because pushing is what the mesh is about to do anyway and the answer
|
|
||||||
arrives with the act. For *knowing what the mesh is running*, it is not: a long-idle machine's entry is
|
|
||||||
as old as its last push, and the honest reading of `not reported` is "nobody has asked recently" rather
|
|
||||||
than "this machine is silent". The words say the first, which is why they are those words.
|
|
||||||
|
|
||||||
Making a heartbeat carry it would close the gap and is a change to what a heartbeat is — a bare word
|
|
||||||
that the node is there, deliberately carrying nothing else. Left alone rather than widened in passing.
|
|
||||||
|
|
||||||
## The open questions, answered as far as they can be
|
|
||||||
|
|
||||||
- *Should a node report the version of its host?* It already did. The gap was the reading.
|
|
||||||
- *Should the mesh refuse to send a field no node understands yet, or refuse per node and say so?*
|
|
||||||
Neither, yet — refusing needs the mesh to know which fields need which version, which is the third
|
|
||||||
question below and is not answered here. What it does is make the disagreement visible before
|
|
||||||
somebody adds a field.
|
|
||||||
- *Is there a general shape — a declaration saying which version of the host it needs?* Still open, and
|
|
||||||
now cheaper to answer: the versions are recorded, so a minimum-version field on a declaration has
|
|
||||||
something to compare against. It belongs with
|
|
||||||
[issue 107](../107-a-declaration-carries-no-order/00-report.md), which wants to add a field and is the
|
|
||||||
first thing this makes safe.
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: resolved
|
status: open
|
||||||
opened: 2026-09-22
|
opened: 2026-09-22
|
||||||
located-in: [mesh-catalog modules/gitea, mesh-controller internal/catalogue/declaration.go]
|
located-in: []
|
||||||
fixed-by: mesh-controller 7352c84, merged in #46 — a module is told its port in a container's environment too, as a file already was
|
fixed-by:
|
||||||
amended-design:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -39,9 +39,3 @@ the assignment happens to differ.
|
|||||||
- Should composition refuse an environment value that names a port the module does not fix, the
|
- Should composition refuse an environment value that names a port the module does not fix, the
|
||||||
way it refuses other claims a module cannot make?
|
way it refuses other claims a module cannot make?
|
||||||
- Which other modules write their own address, with a port, into their environment?
|
- Which other modules write their own address, with a port, into their environment?
|
||||||
|
|
||||||
## Closed
|
|
||||||
|
|
||||||
*2026-09-29, in a grooming pass rather than by whoever fixed it.* The forge's address follows a moved port the same way every other reader does. Found by
|
|
||||||
reading what the code repositories' commits cite: the fix names this issue and is on `main`. It was
|
|
||||||
not re-verified on a machine, and this record says so rather than implying a run that did not happen.
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: resolved
|
status: open
|
||||||
opened: 2026-09-22
|
opened: 2026-09-22
|
||||||
located-in: [mesh-controller internal/catalogue/declaration.go, mesh-catalog (every routed module)]
|
located-in: []
|
||||||
fixed-by: mesh-controller bdf965d (a route names the endpoint it serves) with `portOfEndpoint` and `AtPublishedPort` — the contribution carries the endpoint's declared port and the machine-side redirection is applied to it like any other
|
fixed-by:
|
||||||
amended-design:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -45,9 +45,3 @@ precisely because the predecessor holds the usual one.
|
|||||||
keeping the mapping out of rendered configuration?
|
keeping the mapping out of rendered configuration?
|
||||||
- What should refuse a declaration whose contributed route names a port nothing on that node
|
- What should refuse a declaration whose contributed route names a port nothing on that node
|
||||||
listens on?
|
listens on?
|
||||||
|
|
||||||
## Closed
|
|
||||||
|
|
||||||
*2026-09-29, in a grooming pass rather than by whoever fixed it.* A route names an endpoint rather than a port, and the redirection that turns a declared port into the published one is applied to contributions too. Found by
|
|
||||||
reading what the code repositories' commits cite: the fix names this issue and is on `main`. It was
|
|
||||||
not re-verified on a machine, and this record says so rather than implying a run that did not happen.
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: resolved
|
status: located
|
||||||
opened: 2026-09-23
|
opened: 2026-09-23
|
||||||
located-in: [mesh-controller internal/link, mesh-host internal/link]
|
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:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -40,16 +40,3 @@ exists there and is thrown away at the wire.
|
|||||||
separate genesis-digest branch is needed on the host?
|
separate genesis-digest branch is needed on the host?
|
||||||
- Is a sequence enough, or does a mode change deserve its own marker, so a replayed converged
|
- Is a sequence enough, or does a mode change deserve its own marker, so a replayed converged
|
||||||
declaration is refused by mode as well as by order?
|
declaration is refused by mode as well as by order?
|
||||||
|
|
||||||
## What has since made this safer to do (2026-09-30)
|
|
||||||
|
|
||||||
Adding a `sequence` to a declaration is adding a field, and a host refuses a declaration carrying a
|
|
||||||
field it does not know — whole. That was
|
|
||||||
[issue 087](../087-the-controller-cannot-tell-a-host-is-too-old/00-report.md), and it is resolved: the
|
|
||||||
mesh now records which host each machine reports and `status` names every machine running an older one
|
|
||||||
than another does.
|
|
||||||
|
|
||||||
So the flag day is visible before it is walked into, which it was not when this was filed. It does not
|
|
||||||
make the field free: **the oldest host in the mesh is still what the mesh may send**, and one machine of
|
|
||||||
four is behind today. A sequence that an old host refuses takes that machine out of the mesh's reach
|
|
||||||
entirely — worse than the replay it prevents, which has never been observed.
|
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
# 107 — diagnosis: the fix is a flag day, and it should wait for delivery
|
|
||||||
|
|
||||||
*2026-09-30. Read, measured, and not built — deliberately.*
|
|
||||||
|
|
||||||
## The premise is confirmed
|
|
||||||
|
|
||||||
A host parses a declaration with unknown fields refused, and the code says why rather than leaving it
|
|
||||||
to be inferred:
|
|
||||||
|
|
||||||
> `DisallowUnknownFields` is the whole point rather than strictness for its own sake: a field the host
|
|
||||||
> does not know is a thing the control plane believes it asked for.
|
|
||||||
|
|
||||||
So adding `sequence` and `supersedes` is not an additive change. **Any host that has not been upgraded
|
|
||||||
refuses the whole declaration and applies nothing** — which is exactly the behaviour that keeps a
|
|
||||||
half-understood declaration off a machine, and exactly what makes this expensive.
|
|
||||||
|
|
||||||
## What has changed since this was filed
|
|
||||||
|
|
||||||
[Issue 087](../087-the-controller-cannot-tell-a-host-is-too-old/00-report.md) is resolved: the mesh now
|
|
||||||
records the host version each machine reports and `status` names every machine running an older host
|
|
||||||
than another does. The flag day is visible before it is walked into, which it was not on 2026-09-23.
|
|
||||||
|
|
||||||
That makes the cost measurable rather than hypothetical, and the measurement is the reason this is not
|
|
||||||
being built today.
|
|
||||||
|
|
||||||
## Why it waits
|
|
||||||
|
|
||||||
**One machine of four runs an older host, and it cannot be upgraded.** `ace` is adopted, deliberately
|
|
||||||
parked until the network and module-assignment work is settled, and **nothing delivers a host version at
|
|
||||||
all** — [ADR 0141](../../02-DECISIONS/0141-the-host-delivers-its-own-successor.md) is accepted and not
|
|
||||||
built, which is [issue 142](../142-the-host-is-the-one-thing-the-mesh-does-not-deliver/00-report.md).
|
|
||||||
Every machine takes a hand-placed binary.
|
|
||||||
|
|
||||||
So shipping the field means, in order: place a host by hand on three machines, unpark the fourth, place
|
|
||||||
it there too, and only then turn the controller half on. A machine missed in that sequence is a machine
|
|
||||||
the mesh cannot send anything to at all — not degraded, unreachable.
|
|
||||||
|
|
||||||
**And the fault it prevents has never been observed.** The record says so itself: *"Not observed;
|
|
||||||
constructed from the code, and narrow."* It needs a backlog of more than sixteen declarations queued
|
|
||||||
across a `converge`/`adopt` pair, or a broker slow enough to split one, and the host already applies the
|
|
||||||
newest of a drained batch and refuses a declaration that is not the last by digest.
|
|
||||||
|
|
||||||
**Trading a machine's reachability for a replay nobody has seen is the wrong way round.** The right
|
|
||||||
order is [issue 142](../142-the-host-is-the-one-thing-the-mesh-does-not-deliver/00-report.md) first —
|
|
||||||
when the mesh can deliver a host, a declaration field costs a rollout instead of an expedition — and the
|
|
||||||
work order already puts that in its last group, as the proof that the mesh can make another of itself.
|
|
||||||
|
|
||||||
## What the open questions look like now
|
|
||||||
|
|
||||||
- *A per-node `sequence` under the controller's node hold, and `supersedes` as the previous digest?*
|
|
||||||
Still the right shape. The controller already holds the lock and already records each send, so the
|
|
||||||
order exists and is thrown away at the wire — unchanged since this was filed.
|
|
||||||
- *Genesis signing its bundle as sequence zero?* Yes, and it is the cheaper half: the bundle is written
|
|
||||||
by the host that will read it, so it has no flag day of its own.
|
|
||||||
- *Is a sequence enough, or does a mode change deserve its own marker?* A sequence alone does not stop
|
|
||||||
a replayed *converged* declaration reaching a node that has since been returned to adopted, which is
|
|
||||||
the incident of issue 104 by another door and is what this record names as its real risk. It wants
|
|
||||||
both, and the second is the one worth having first.
|
|
||||||
- **And one this record did not ask:** should a declaration say which host version it needs? 087 makes
|
|
||||||
that comparable for the first time, and it is the general form of the answer — a field that announces
|
|
||||||
its own requirement, rather than a flag day per field, for ever.
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
Left `located`. The owner is unchanged, the shape of the fix is agreed, and the gate is
|
|
||||||
[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
|
opened: 2026-09-24
|
||||||
located-in: [mesh-controller internal/catalogue/declaration.go (every container was given the roster at creation)]
|
located-in: [mesh-host internal/apply]
|
||||||
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)
|
fixed-by:
|
||||||
amended-design:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -59,27 +59,3 @@ container is made with are the same kind of input, read once at creation, and ar
|
|||||||
and is the stronger statement; it is also what the mesh's own resolver exists for.
|
and is the stronger statement; it is also what the mesh's own resolver exists for.
|
||||||
- Either way: what tells an operator that a container is running with an address the node no longer
|
- Either way: what tells an operator that a container is running with an address the node no longer
|
||||||
has? Nothing did.
|
has? Nothing did.
|
||||||
|
|
||||||
## Answered at the cause (2026-09-30)
|
|
||||||
|
|
||||||
This was the first of three arrivals of one fact: a container is given the mesh's names when it is
|
|
||||||
created and never looks again, so a name that moves afterwards is wrong inside it for as long as it
|
|
||||||
runs. It arrived again as [issue 135](../135-a-containers-mesh-names-are-not-compared/00-report.md),
|
|
||||||
whose fix made the names comparable — and that fix made the roster part of every container's identity,
|
|
||||||
which arrived as [issue 151](../151-a-new-name-recreates-every-container-in-the-mesh/00-report.md).
|
|
||||||
|
|
||||||
[ADR 0148](../../02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md) ends the
|
|
||||||
copying: a container resolves through its machine's resolver at the moment it asks. The shape this
|
|
||||||
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
-23
@@ -1,9 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: resolved
|
status: open
|
||||||
opened: 2026-09-24
|
opened: 2026-09-24
|
||||||
located-in:
|
located-in: []
|
||||||
- mesh-catalog modules/dnsmasq (the runtime was never told; the resolver answered by interface)
|
fixed-by:
|
||||||
fixed-by: mesh-catalog PR 175 (the runtime is reloaded and keeps its containers over a restart) and PR 176 (the resolver answers by address, so a query from a bridge is admitted) — measured 2026-09-30, 01-resolution.md
|
|
||||||
amended-design:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -52,22 +51,3 @@ knows that is what the rule means.
|
|||||||
the runtime's default one? That is a stronger rule and would have prevented 109 as well.
|
the runtime's default one? That is a stronger rule and would have prevented 109 as well.
|
||||||
- What checks it? A converged bed with a container on the default network resolving a mesh name is
|
- What checks it? A converged bed with a container on the default network resolving a mesh name is
|
||||||
the missing assertion; nothing in the resolver's own beds covers the filter.
|
the missing assertion; nothing in the resolver's own beds covers the filter.
|
||||||
|
|
||||||
## What now depends on this (2026-09-30)
|
|
||||||
|
|
||||||
This stopped being a container-DNS inconvenience.
|
|
||||||
[ADR 0148](../../02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md) decides
|
|
||||||
that a container resolves the mesh's names rather than being given a copy of them, which is what stops
|
|
||||||
one name moving from replacing every container in the mesh
|
|
||||||
([issue 151](../151-a-new-name-recreates-every-container-in-the-mesh/00-report.md)) and what makes a
|
|
||||||
stale address impossible rather than merely noticed
|
|
||||||
([issues 109](../109-a-container-keeps-the-address-it-was-made-with/00-report.md)
|
|
||||||
and [135](../135-a-containers-mesh-names-are-not-compared/00-report.md)).
|
|
||||||
|
|
||||||
**That decision cannot land until this one does**, and not partly: a container on the runtime's default
|
|
||||||
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,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: resolved
|
status: open
|
||||||
opened: 2026-09-24
|
opened: 2026-09-24
|
||||||
located-in: [mesh-controller module.json, mesh-host internal/apply]
|
located-in: [mesh-controller module.json, mesh-host internal/apply]
|
||||||
fixed-by: ADR 0142 — the mesh's own components are delivered as binaries on the machine, so the controller is a process; the delivery itself is issue 142
|
fixed-by:
|
||||||
amended-design:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -84,21 +84,3 @@ restart and run-to-completion semantics — so this would not need host-side wor
|
|||||||
|
|
||||||
The two do not collapse into one. The controller is not a code-carrying sidecar, and `network: host`
|
The two do not collapse into one. The controller is not a code-carrying sidecar, and `network: host`
|
||||||
is what makes the asymmetry visible here and nowhere else.
|
is what makes the asymmetry visible here and nowhere else.
|
||||||
|
|
||||||
## Answered
|
|
||||||
|
|
||||||
*2026-09-29, in a grooming pass.* This asked a question rather than reporting a defect, and the
|
|
||||||
question was taken: [ADR 0142](../../02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md)
|
|
||||||
decides that the mesh's own components — the host, the controller, the catalogue, the builder, the
|
|
||||||
vault — are **binaries on the machine**, delivered by the mechanism
|
|
||||||
[ADR 0141](../../02-DECISIONS/0141-the-host-delivers-its-own-successor.md) built, and that
|
|
||||||
third-party software (the store, the registry, the broker) stays a container because an image is the
|
|
||||||
right way to carry somebody else's build.
|
|
||||||
|
|
||||||
So the operating experience this record was written from — every mutating command reached through
|
|
||||||
`docker exec mesh-controller` — is answered, and answered against the container.
|
|
||||||
|
|
||||||
**The delivery is a separate matter and is not this record's.** Step 1 of it is built and no
|
|
||||||
component travels yet; that is
|
|
||||||
[issue 142](../142-the-host-is-the-one-thing-the-mesh-does-not-deliver/00-report.md).
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: resolved
|
status: located
|
||||||
opened: 2026-09-25
|
opened: 2026-09-25
|
||||||
located-in: [hq, mesh-catalog modules/showcase, mesh-sdk src/tools/index.ts, mesh-tools]
|
located-in: [hq, mesh-catalog modules/showcase, mesh-sdk src/tools/index.ts, mesh-tools]
|
||||||
fixed-by: hq 83791f0 (PR 196) — ADR 0150: a module's own code runs as supervised processes under the module's one account; designs 18 and 20 now cite it, and ADR 0047 carries a dated note pointing at it
|
fixed-by:
|
||||||
amended-design:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -99,27 +99,3 @@ what the sidecar is has nowhere in the design layer to look, which is how this w
|
|||||||
is mechanically checkable: the resource types a design doc names are a closed set, and every
|
is mechanically checkable: the resource types a design doc names are a closed set, and every
|
||||||
member of it either appears in a decision or does not. Whether that check is worth writing is
|
member of it either appears in a decision or does not. Whether that check is worth writing is
|
||||||
part of this issue, not settled by it.
|
part of this issue, not settled by it.
|
||||||
|
|
||||||
## Answered (2026-09-30)
|
|
||||||
|
|
||||||
[ADR 0150](../../02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md)
|
|
||||||
settles all three disagreements, and the design documents win two of them:
|
|
||||||
|
|
||||||
1. **Container or unit — a supervised process.** ADR 0047's argument never required a container. It
|
|
||||||
argued for a runtime *per module*, because a node-wide one could not hold a per-module account and
|
|
||||||
per-module runtimes on one tool key would be handed calls for tools they do not have. A unit per
|
|
||||||
module satisfies that exactly, and a unit runs as an account. The container was the mechanism to
|
|
||||||
hand. [ADR 0142](../../02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md) had
|
|
||||||
already gone the same way for the mesh's own components, and a module is not a container.
|
|
||||||
2. **One process or several — several, under one account.** 0047's "one module, one process, one
|
|
||||||
account" carried its weight in the last clause; its stated worry was "not a second one to scope and
|
|
||||||
seal", which is about a second *identity*. Processes sharing the module's one account create none.
|
|
||||||
What a module may not have is two accounts.
|
|
||||||
3. **Whether the record was consulted — fixed rather than answered.** Designs 18 and 20 now name 0150
|
|
||||||
in `decisions:`, and 0047 carries a dated note saying where its hosting form was settled, so neither
|
|
||||||
door leads to the wrong answer any more.
|
|
||||||
|
|
||||||
**What this does not fix.** A module's code delivered as a binary is behind the same gap as the host's
|
|
||||||
own ([ADR 0141](../../02-DECISIONS/0141-the-host-delivers-its-own-successor.md), accepted and not
|
|
||||||
built): a container's code arrives by `docker pull` and this does not, so until delivery exists such a
|
|
||||||
module is one somebody places by hand. 0150 records that as the cost of the decision, unpaid.
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: resolved
|
status: located
|
||||||
opened: 2026-09-25
|
opened: 2026-09-25
|
||||||
located-in: [mesh-host internal/apply (the spec comparison, via issue 135) — not mesh-catalog modules/umami, which this record first named and which was never at fault]
|
located-in: [mesh-catalog modules/umami]
|
||||||
fixed-by: mesh-host e82789a (issue 135's fix) — recreating the container with a current roster ended it; verified live 2026-09-30
|
fixed-by:
|
||||||
amended-design:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
# 118 — resolved: it was issue 135, and it is over
|
|
||||||
|
|
||||||
*Verified on the machine, 2026-09-30.*
|
|
||||||
|
|
||||||
## It no longer happens
|
|
||||||
|
|
||||||
```
|
|
||||||
$ docker inspect -f '{{.RestartCount}}' umami
|
|
||||||
0
|
|
||||||
$ docker logs umami --tail 12
|
|
||||||
26 migrations found in prisma/migrations
|
|
||||||
No pending migrations to apply.
|
|
||||||
✓ Database is up to date.
|
|
||||||
▲ Next.js 16.3.4
|
|
||||||
✓ Ready in 0ms
|
|
||||||
$ curl -o /dev/null -w '%{http_code}' https://umami.novox.be/
|
|
||||||
200
|
|
||||||
```
|
|
||||||
|
|
||||||
No restart loop, no `prisma.$queryRaw()` timeout, and the public name that had answered `502` since
|
|
||||||
2026-09-25 answers `200`. The raw query that could not complete now runs twenty-six migrations and
|
|
||||||
reports the store up to date.
|
|
||||||
|
|
||||||
## What it was
|
|
||||||
|
|
||||||
**The same fault as [issue 135](../135-a-containers-mesh-names-are-not-compared/00-report.md), which
|
|
||||||
was diagnosed three days later without either record noticing the other.** 135's container is this
|
|
||||||
one, named in its own evidence table:
|
|
||||||
|
|
||||||
```
|
|
||||||
umami created 2026-09-23 novox.internal:10.42.0.1
|
|
||||||
mesh-catalog created today novox.internal:10.10.0.1
|
|
||||||
```
|
|
||||||
|
|
||||||
The mesh's overlay range had moved. Umami had been created before the move and held the store's name
|
|
||||||
at an address that no longer existed, while every container made after the move held the current one.
|
|
||||||
That is why the dial appeared to succeed and the first real query timed out, and why the same query
|
|
||||||
from the same network with the same credential answered in milliseconds — **what differed was the
|
|
||||||
name, not the path, not the credential and not the store.**
|
|
||||||
|
|
||||||
Today it holds `novox.internal:10.10.0.1`.
|
|
||||||
|
|
||||||
## Why this record did not find it
|
|
||||||
|
|
||||||
The report's own reasoning is worth keeping as a warning, because it is careful and it is wrong:
|
|
||||||
|
|
||||||
> A dial that succeeds and a query that times out, from a container on one network to a store on
|
|
||||||
> another, has the shape of a path-MTU or conntrack fault (large response packets dropped after the
|
|
||||||
> small handshake ones pass), or of the store accepting the TCP connection while the backend it
|
|
||||||
> proxies for is wedged.
|
|
||||||
|
|
||||||
Both are good hypotheses about a network path. Neither is the answer, and the record also names the
|
|
||||||
move that would have found it — *"what is known to differ for umami against every working consumer of
|
|
||||||
the same store tonight is nothing yet — that comparison is the first move"* — and then did not make it.
|
|
||||||
Comparing umami's hosts entries against any container created that week would have shown a five-day-old
|
|
||||||
address in one field.
|
|
||||||
|
|
||||||
**A stale name presents as a network fault.** That is the lesson, and it is the reason
|
|
||||||
[ADR 0148](../../02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md) stops
|
|
||||||
copying names into containers at all: not because detecting staleness is hard, but because it disguises
|
|
||||||
itself as something else for five days while every check reports success.
|
|
||||||
|
|
||||||
## What actually ended it
|
|
||||||
|
|
||||||
Issue 135's fix — `mesh-host` e82789a, *a container's mesh names are part of what it is* — put the
|
|
||||||
roster into the spec digest the host compares, so a container whose names moved is recreated like one
|
|
||||||
whose image moved. That recreated umami with a current roster and ended this.
|
|
||||||
|
|
||||||
That fix has since been superseded in turn, by 0148, because making the roster part of every
|
|
||||||
container's identity meant one name moving replaced every container in the mesh
|
|
||||||
([issue 151](../151-a-new-name-recreates-every-container-in-the-mesh/00-report.md)). So this record
|
|
||||||
closes on a fix that is itself on the way out — which does not make it less closed, and is worth saying
|
|
||||||
plainly rather than leaving a reader to find out.
|
|
||||||
|
|
||||||
## Not carried forward
|
|
||||||
|
|
||||||
The `502` had one other contributor worth recording as ruled out: a stale duplicate Traefik router for
|
|
||||||
this name, hand-authored in the predecessor's era beside the mesh-written one, removed on 2026-09-25
|
|
||||||
and — as the report says — changing nothing. It was not the cause and it is gone.
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
status: resolved
|
status: located
|
||||||
opened: 2026-09-25
|
opened: 2026-09-25
|
||||||
located-in: [mesh-catalog modules, mesh-controller internal/catalogue]
|
located-in: [mesh-catalog modules, mesh-controller internal/catalogue]
|
||||||
fixed-by: mesh-catalog PR 193 (the conversion), mesh-controller PR 170 (TestPlacedDirectoriesKeepTheirPaths, which proves it moved nothing); the placed-directory mechanism itself predates this in mesh-controller internal/catalogue/dir_into.go
|
fixed-by:
|
||||||
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
# 119 — A module definition decides where its files live on the machine
|
# 119 — A module definition decides where its files live on the machine
|
||||||
@@ -133,30 +133,3 @@ not by any check.
|
|||||||
- What identifies an assignment, if a module may be assigned to one node more than once?
|
- What identifies an assignment, if a module may be assigned to one node more than once?
|
||||||
- What would the contributions file carry instead of host paths, so a provider needs no
|
- What would the contributions file carry instead of host paths, so a provider needs no
|
||||||
identical-path mount?
|
identical-path mount?
|
||||||
|
|
||||||
## Resolved, 2026-09-30 — the module's half; the mesh's half is issue 174
|
|
||||||
|
|
||||||
**A definition no longer decides where its own data lives.** Twenty-eight definitions that named their
|
|
||||||
data directories now place them: the module's root as `place: "."`, a sub-directory by its id, and every
|
|
||||||
host-side reference — bindings, secrets, own secrets, grants, receives, file paths, mounts, env-files —
|
|
||||||
as `${dir:<id>}`. Twenty-eight others had already been written that way. Five directories whose id is
|
|
||||||
not their last segment keep their path as a placement, which is the exception the design allows and
|
|
||||||
the reason nothing else has to move for them.
|
|
||||||
|
|
||||||
**Nothing moved, and a test says so.** The controller's `TestPlacedDirectoriesKeepTheirPaths` takes the
|
|
||||||
catalogue before and after, resolves every converted definition on the default root with the
|
|
||||||
controller's own rule, and compares it whole with the definition before it: identical for all
|
|
||||||
twenty-eight. So the retirement this record said was a data migration turned out not to be one, on
|
|
||||||
one condition — a node's default root is where the data already is, and every node's is — and the
|
|
||||||
machines see no change. A node that sets another root is the case this does not cover, and it does
|
|
||||||
not exist.
|
|
||||||
|
|
||||||
**What remains is not this record's.** The 232 host paths still in the catalogue are where the mesh
|
|
||||||
writes what it makes for a module, under `/var/lib/mesh/<module>`; design 27 says the mesh places
|
|
||||||
those itself, and it does not yet. That is [issue 174](../174-the-meshs-own-files-for-a-module-are-placed-by-the-definition/00-report.md).
|
|
||||||
The defects this record listed under *where that has already gone wrong* are unchanged by this and
|
|
||||||
stay in 174's scope where they concern the mesh's files; the operator's shared data stays an access
|
|
||||||
([ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md)).
|
|
||||||
|
|
||||||
The manifest change lands with the catalogue's next merge; the rollout is a rebuild that changes no
|
|
||||||
machine, checked by comparing each machine's plan before and after.
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: resolved
|
status: located
|
||||||
opened: 2026-09-26
|
opened: 2026-09-26
|
||||||
located-in: [mesh-sdk src/provisioner, mesh-catalog modules/redis]
|
located-in: [mesh-sdk src/provisioner, mesh-catalog modules/redis]
|
||||||
fixed-by: mesh-catalog bbda88c, merged in #84 — every credential provider says whether it still holds a consumer
|
fixed-by:
|
||||||
amended-design:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -60,9 +60,3 @@ checks it after the first pass.
|
|||||||
instance and leaves the gap for the others.
|
instance and leaves the gap for the others.
|
||||||
- Where does the record of what was applied live, if not in memory? ADR 0114, still
|
- Where does the record of what was applied live, if not in memory? ADR 0114, still
|
||||||
proposed, puts rotation state with the vault. The same place may answer this.
|
proposed, puts rotation state with the vault. The same place may answer this.
|
||||||
|
|
||||||
## Closed
|
|
||||||
|
|
||||||
*2026-09-29, in a grooming pass rather than by whoever fixed it.* A provisioner asks the backend what is there rather than trusting what it remembers doing. Found by
|
|
||||||
reading what the code repositories' commits cite: the fix names this issue and is on `main`. It was
|
|
||||||
not re-verified on a machine, and this record says so rather than implying a run that did not happen.
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
status: resolved
|
status: located
|
||||||
opened: 2026-09-26
|
opened: 2026-09-26
|
||||||
located-in: [mesh-controller internal/catalogue/declaration.go, mesh-catalog modules/keycloak, mesh-catalog modules/minio, mesh-catalog modules/nextcloud]
|
located-in: [mesh-controller internal/catalogue/declaration.go, mesh-catalog modules/keycloak, mesh-catalog modules/minio, mesh-catalog modules/nextcloud]
|
||||||
fixed-by: mesh-controller PR 149 (a module is told the name it is served under), PR 169 (an operator's value, a context on the seat); mesh-catalog PR 188; ADR 0155
|
fixed-by:
|
||||||
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
# 122 — A module cannot ask for its own public name, so three manifests wrote this mesh's names into the catalogue
|
# 122 — A module cannot ask for its own public name, so three manifests wrote this mesh's names into the catalogue
|
||||||
@@ -114,27 +114,3 @@ particular to one installation, and also has nowhere to live but the definition.
|
|||||||
ADR 0112 would put it), and if so what reads it — the provisioner, or the module's own values?
|
ADR 0112 would put it), and if so what reads it — the provisioner, or the module's own values?
|
||||||
- What check would notice the next one? A definition naming a public domain is detectable in the
|
- What check would notice the next one? A definition naming a public domain is detectable in the
|
||||||
shape of the value, which is more than nothing, and less than a rule.
|
shape of the value, which is more than nothing, and less than a rule.
|
||||||
|
|
||||||
## Resolved, 2026-09-30
|
|
||||||
|
|
||||||
The open questions, answered in order. **A module names what it will be reached at** through the
|
|
||||||
binding of the route it contributes: `${bound:route:name}`, or `:name-<local>` for several
|
|
||||||
contributions, is the composed public name; `:internal-name` the private one (controller PR 149). The
|
|
||||||
identity provider, the object store's console and the automation tool's webhook now read it there.
|
|
||||||
**The scheme is the module's**, because it is true of the route the proxy terminates and every instance
|
|
||||||
wrote `https://` in front of the name. **An adopted resource's name is a setting** on the assignment;
|
|
||||||
so is every other operator's value — a mail domain, a site name, the address a proxy forwards from —
|
|
||||||
as `${setting:<key>}` in the file the software reads
|
|
||||||
([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)).
|
|
||||||
**The check that notices the next one** is the shape of the value, as this record guessed it would be,
|
|
||||||
and it is more than nothing: it found forty-two, and the catalogue passes it now
|
|
||||||
([issue 134](../134-a-definition-may-still-name-the-mesh/00-report.md)).
|
|
||||||
|
|
||||||
**What the rollout cost, 2026-09-30 evening.** The site module's rename from its domain to `website`
|
|
||||||
was a new module to the mesh, and two things the old assignment carried by name were lost: the
|
|
||||||
container still named the old network, and a port setting on the old assignment had hidden that
|
|
||||||
`listens` said one port while the container published another. The site answered 502 for about
|
|
||||||
twenty minutes across two one-line fixes (mesh-catalog PRs 190, 191). A module's rename is an
|
|
||||||
unassign and an assign, and everything the assignment held — settings, ports, its directory — is the
|
|
||||||
new module's to get again; the mesh says nothing about that today. The mail module's settings turned
|
|
||||||
out to reach every fact it contributes, which is [issue 173](../173-a-modules-settings-reach-every-fact-it-contributes/00-report.md).
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
status: resolved
|
status: located
|
||||||
opened: 2026-09-26
|
opened: 2026-09-26
|
||||||
located-in: [hq 00-META/glossary.md, hq 02-DECISIONS/0075-two-stores-and-which-provides-what.md, mesh-controller internal/catalogue/seats.go, mesh-catalog modules/distribution]
|
located-in: [hq 00-META/glossary.md, hq 02-DECISIONS/0075-two-stores-and-which-provides-what.md, mesh-controller internal/catalogue/seats.go, mesh-catalog modules/distribution]
|
||||||
fixed-by: ADR 0156; mesh-controller migration 0048 (feat/the-artifact-store-seat-is-named-for-its-scope); mesh-catalog modules/distribution
|
fixed-by:
|
||||||
amended-design: 03-DESIGN/01-to-be/26-the-seats.md
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
# 123 — The image registry is named after a role, and *artifact* is defined as one format
|
# 123 — The image registry is named after a role, and *artifact* is defined as one format
|
||||||
@@ -72,15 +72,3 @@ adopted, rather than on protocols.
|
|||||||
exercise that leaves the code disagreeing?
|
exercise that leaves the code disagreeing?
|
||||||
- What check would keep the glossary honest — a definition tested against the kinds a definition may
|
- What check would keep the glossary honest — a definition tested against the kinds a definition may
|
||||||
actually declare, rather than restated by hand?
|
actually declare, rather than restated by hand?
|
||||||
|
|
||||||
## Resolved, 2026-09-30
|
|
||||||
|
|
||||||
Read from what the store serves rather than from what it was called: both shapes of a kept reference
|
|
||||||
— an image and an archive's blob — go to the same registry by digest, which is exactly the provision
|
|
||||||
ADR 0075 defined. So the word was wrong and the seat's name was odd, and the provision was right.
|
|
||||||
[ADR 0156](../../02-DECISIONS/0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md):
|
|
||||||
*artifact* means what a build produces, of any of the four kinds; the seat is `mesh-artifact-store`
|
|
||||||
with the old name as its alias (one migration, ADR 0122's mechanism); the provision keeps its name.
|
|
||||||
The two-implementations question stays as 0075 answered it, with the day to retire the second server
|
|
||||||
named. The mechanical check the report asked for is the alias test and the glossary naming the same
|
|
||||||
four kinds as design 18's table.
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
---
|
---
|
||||||
status: resolved
|
status: open
|
||||||
opened: 2026-09-26
|
opened: 2026-09-26
|
||||||
located-in: [mesh-host internal/link (the apply report line), mesh-controller cmd/mesh-controller (status and its all-well condition)]
|
located-in: [mesh-host internal/apply, mesh-controller]
|
||||||
fixed-by: mesh-host cbf5018, mesh-controller bfd983e
|
|
||||||
amended-design:
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 125 — a hold is not a line in the apply report, and an operator flew blind into an outage
|
# 125 — a hold is not a line in the apply report, and an operator flew blind into an outage
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
# 125 — resolved: a hold is a line in the report, and it stops the mesh reading as well
|
|
||||||
|
|
||||||
*2026-09-30.*
|
|
||||||
|
|
||||||
## What the four surfaces say now
|
|
||||||
|
|
||||||
The report named four surfaces, none of which carried the one sentence that mattered. Two of them
|
|
||||||
already did by the time this was picked up, and two did not.
|
|
||||||
|
|
||||||
**1. The apply report says what it held** — this was missing, and is the line the operator was reading
|
|
||||||
when the count did not add up:
|
|
||||||
|
|
||||||
```
|
|
||||||
applied 330 resource(s), 16 held until their module is taken (route-proxy: 13, mailu: 3)
|
|
||||||
```
|
|
||||||
|
|
||||||
Grouped by module and ordered by name, because `take` acts on a module and that is the sentence an
|
|
||||||
operator needs. An apply that held nothing says nothing extra — a line reporting `0 held` on every
|
|
||||||
converged apply is one that stops being read.
|
|
||||||
|
|
||||||
**2. `status` counts holds, and a hold breaks "all well"** — this was missing. Status now says:
|
|
||||||
|
|
||||||
```
|
|
||||||
15 resource(s) are held as found, because their module was assigned and never taken — so it is
|
|
||||||
running none of what it declares:
|
|
||||||
novox route-proxy (13), mailu (2)
|
|
||||||
|
|
||||||
`take <node> <module>` compares what runs against what it declares, and runs it
|
|
||||||
```
|
|
||||||
|
|
||||||
**And the sentence that was the fault no longer prints.** "all doing what they were told, all heard
|
|
||||||
from, running what the mesh would send them" was *true* for the whole outage, and acting on it stopped
|
|
||||||
the predecessor's proxy. A hold now suppresses it; being adopted still does not, and the difference is
|
|
||||||
deliberate — adopted is a mode somebody chose and can leave alone, a module assigned and never taken is
|
|
||||||
a half-finished action with nothing left to finish it.
|
|
||||||
|
|
||||||
**3. `node show <node>` shows the node's own held list** — already true, and recorded here as checked
|
|
||||||
rather than assumed. It reads `held` from what the machine last reported, with an `as of` beside it, and
|
|
||||||
names each hold's kind, target, id, module, whether something other than the mesh has changed it, and
|
|
||||||
where an original was kept.
|
|
||||||
|
|
||||||
**4. The push's count** is unchanged and now interpretable, which was the ask: `sent 346` against
|
|
||||||
`applied 330, 16 held until their module is taken (…)` is a pair a reader can resolve without opening a
|
|
||||||
file on the machine.
|
|
||||||
|
|
||||||
## Where the data came from
|
|
||||||
|
|
||||||
**The host already reported it.** `Held` has been on the wire since [ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md),
|
|
||||||
and the controller already recorded it and showed it in `node show`. Nothing needed a new field, a new
|
|
||||||
message or a migration — which is why this is additive, and why the report's framing (*"the semantics
|
|
||||||
are consistent and right; the reporting is what let them be forgotten"*) was exactly right.
|
|
||||||
|
|
||||||
What was missing was that two surfaces never asked. Status read the mesh's take-time listing, so a
|
|
||||||
module assigned after that listing showed nothing at all; the apply line counted what it applied and
|
|
||||||
said nothing about the difference.
|
|
||||||
|
|
||||||
## One thing deliberately not done
|
|
||||||
|
|
||||||
**Status does not call a hold a fault.** It is correct behaviour, and a reader trained to see red for
|
|
||||||
something the mesh did right will stop reading. It is reported as work outstanding, with the command
|
|
||||||
that finishes it — and it withholds the all-well sentence, which is the part that carries the weight.
|
|
||||||
|
|
||||||
## How it is checked
|
|
||||||
|
|
||||||
- The apply line names the count and the module, and is empty when nothing is held (mesh-host).
|
|
||||||
- Status finds a hold from what the machine reported, end to end through the store.
|
|
||||||
- **A held module makes the all-well condition false**, asserted against the production condition
|
|
||||||
rather than a copy of it — that condition is now one named function for this reason.
|
|
||||||
- The JSON form carries a row per machine and module, and omits the field entirely when nothing is
|
|
||||||
held.
|
|
||||||
+3
-22
@@ -1,15 +1,10 @@
|
|||||||
---
|
---
|
||||||
status: resolved
|
status: located
|
||||||
opened: 2026-09-27
|
opened: 2026-09-27
|
||||||
located-in: [mesh-controller cmd/mesh-controller/push.go, mesh-controller cmd/mesh-controller/sendable.go]
|
located-in: [mesh-controller cmd/mesh-controller/push.go]
|
||||||
fixed-by: mesh-controller sendable.go and push.go — an empty declaration is sent carrying `owns_nothing`, and the host refuses an empty body that does not carry it
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 149 — a declaration that shrinks to empty is skipped, so the node keeps what it should drop
|
# A declaration that shrinks to empty is skipped, so the node keeps what it should drop
|
||||||
|
|
||||||
*Opened as 127 and renumbered on 2026-09-29: two records were given that number on the same day.*
|
|
||||||
*The other kept it, because three documents and three source files cite it by number and nothing
|
|
||||||
cited this one but a decision and a sibling issue, both corrected with this move.*
|
|
||||||
|
|
||||||
## What was observed
|
## What was observed
|
||||||
|
|
||||||
@@ -42,17 +37,3 @@ mean "own nothing", which the host already applies correctly when it receives on
|
|||||||
|
|
||||||
On ace, one command drops it permanently (the corrected controller never re-composes it):
|
On ace, one command drops it permanently (the corrected controller never re-composes it):
|
||||||
`sudo ufw delete allow 5671`. At ace's converge it would clear on its own.
|
`sudo ufw delete allow 5671`. At ace's converge it would clear on its own.
|
||||||
|
|
||||||
## Closed
|
|
||||||
|
|
||||||
*2026-09-29, in a grooming pass.* Both halves are on `main` and both name this issue.
|
|
||||||
|
|
||||||
- The control plane **sends** it: a declaration that composes to no resources goes out with
|
|
||||||
`owns_nothing`, and `push` says *sent, not skipped*.
|
|
||||||
- The host **refuses an empty body that does not carry it**, so a truncated or mis-composed
|
|
||||||
declaration can never be read as "own nothing" — which is the failure the fix had to avoid while
|
|
||||||
making the empty case expressible.
|
|
||||||
|
|
||||||
Closed by reading the code rather than by watching a machine let go of a stray resource; the record
|
|
||||||
says so rather than implying a run.
|
|
||||||
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
---
|
---
|
||||||
status: resolved
|
status: located
|
||||||
fixed-by: mesh-host 1cb8953 and fdc768c — the mesh writes into a marked block of a text file instead of over it
|
|
||||||
opened: 2026-09-26
|
opened: 2026-09-26
|
||||||
located-in: [mesh-controller internal/catalogue/facts.go, mesh-host internal/apply]
|
located-in: [mesh-controller internal/catalogue/facts.go, mesh-host internal/apply]
|
||||||
---
|
---
|
||||||
@@ -72,9 +71,3 @@ private network loses that name too.
|
|||||||
- The host's file resource supports `into: "json"` only; anything else is a whole write.
|
- The host's file resource supports `into: "json"` only; anything else is a whole write.
|
||||||
- `node show <node>` on the adopted workstation: `holds file /etc/hosts
|
- `node show <node>` on the adopted workstation: `holds file /etc/hosts
|
||||||
mesh-wireguard.fact-node-names`, original kept.
|
mesh-wireguard.fact-node-names`, original kept.
|
||||||
|
|
||||||
## Closed
|
|
||||||
|
|
||||||
*2026-09-29, in a grooming pass rather than by whoever fixed it.* A shared hosts file keeps every line that is not the mesh's. Found by
|
|
||||||
reading what the code repositories' commits cite: the fix names this issue and is on `main`. It was
|
|
||||||
not re-verified on a machine, and this record says so rather than implying a run that did not happen.
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
---
|
---
|
||||||
status: resolved
|
status: open
|
||||||
opened: 2026-09-26
|
opened: 2026-09-26
|
||||||
located-in: [mesh-catalog modules/ca-trust]
|
located-in: [mesh-controller, mesh-catalog step-ca]
|
||||||
fixed-by: the ca-trust module registered from the catalogue and assigned to a workstation; verified in both directions 2026-09-30 (02-resolution.md)
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 129 — nothing makes a machine trust the mesh's own certificate authority
|
# 129 — nothing makes a machine trust the mesh's own certificate authority
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
# 129 — diagnosis
|
|
||||||
|
|
||||||
*2026-09-30, from the workstation the issue was opened on.*
|
|
||||||
|
|
||||||
## Still live, and reproduced exactly
|
|
||||||
|
|
||||||
The certificate is genuine, the authority is the mesh's, and nothing on the machine trusts it:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ openssl s_client -connect keycloak.novox.internal:443 -servername keycloak.novox.internal
|
|
||||||
subject=CN=keycloak.novox.internal
|
|
||||||
issuer=O=Mesh Internal CA, CN=Mesh Internal CA Intermediate CA
|
|
||||||
Verify return code: 20 (unable to get local issuer certificate)
|
|
||||||
|
|
||||||
$ curl https://keycloak.novox.internal/
|
|
||||||
curl: (60) SSL certificate OpenSSL verify result: unable to get local issuer certificate (20)
|
|
||||||
```
|
|
||||||
|
|
||||||
`trust list` holds no entry for the mesh. The anchors present are two `mkcert` development roots and
|
|
||||||
the predecessor's lab root — the report's account of the trust store is unchanged.
|
|
||||||
|
|
||||||
The public name on the same proxy verifies cleanly (`CN=keycloak.novox.be`, Let's Encrypt, return code
|
|
||||||
0), which places the fault exactly where the report puts it: not in the proxy, not in the authority,
|
|
||||||
and not in the certificate.
|
|
||||||
|
|
||||||
**The name matters, and the report's "every HTTPS name the mesh serves internally" is too broad.** The
|
|
||||||
served internal name is `<label>.<node>.internal`. The hosts file also carries
|
|
||||||
`<label>.<public-domain>.internal`, which nothing serves and which fails differently — that is
|
|
||||||
[issue 157](../157-a-routed-names-internal-alias-is-served-by-nothing/00-report.md), found while
|
|
||||||
reproducing this, and it cost the first several minutes of this diagnosis.
|
|
||||||
|
|
||||||
## The authority serves what the module needs
|
|
||||||
|
|
||||||
`step-ca` is up and healthy, and publishes `roots: /roots.pem` for both `acme-ca` and
|
|
||||||
`internal-acme-ca`. That endpoint returns PEM:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ curl -sk https://127.0.0.1:9000/roots.pem
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIBvzCCAWWgAwIBAgIQYa2CkdJk16JyG/dVy2qoEzAKBggqhkjOPQQDAjA+…
|
|
||||||
```
|
|
||||||
|
|
||||||
So `${bound:internal-acme-ca:roots}` in the `ca-trust` module composes to a URL that returns a
|
|
||||||
certificate, and the module's own check — refuse a body that is not one — is checking the right thing.
|
|
||||||
|
|
||||||
Worth recording because it was nearly filed as a defect: step-ca *also* serves `/roots`, which returns
|
|
||||||
`{"crts":["-----BEGIN CERTIFICATE-----\n…"]}`. That body contains the literal text the module greps
|
|
||||||
for, so had the module used `/roots` it would have installed JSON into the anchors directory and
|
|
||||||
reported success. It does not use it. The guard is sound only because the published path is the PEM
|
|
||||||
one, which is worth knowing before anybody changes either.
|
|
||||||
|
|
||||||
## What is actually in the way
|
|
||||||
|
|
||||||
**The module is not registered.** The report and the work plan both say it exists and is merged, which
|
|
||||||
it does — `mesh-catalog modules/ca-trust`, on `main`. But the mesh has never been told about it:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ mesh-controller module list | grep -iE 'ca-trust|step-ca'
|
|
||||||
step-ca 1 built 67f5f4cf on novox
|
|
||||||
```
|
|
||||||
|
|
||||||
39 of the catalogue's 76 manifests are registered. `ca-trust` is one of the 37 that are not, so it
|
|
||||||
cannot be assigned to anything — "assign it to one machine" has no module to name.
|
|
||||||
|
|
||||||
A dry run confirms it registers cleanly and needs no artifact built: it declares a directory, a script,
|
|
||||||
a unit and a service, and no image.
|
|
||||||
|
|
||||||
```
|
|
||||||
$ mesh-controller build <catalogue> --path modules/ca-trust --dry-run
|
|
||||||
… the manifest, parsed and validated
|
|
||||||
```
|
|
||||||
|
|
||||||
## So the remaining work is three steps, not one
|
|
||||||
|
|
||||||
1. **Register it** — build it from the catalogue, which pins nothing because it has no artifacts.
|
|
||||||
2. **Assign it** to a machine. The workstation this was observed on is the honest first choice: it is
|
|
||||||
where a person meets the fault, and it is where the check can be made with a plain client.
|
|
||||||
3. **Verify** `curl https://<label>.<node>.internal/` with no flags, and `trust list` naming the mesh.
|
|
||||||
|
|
||||||
Then removal, which the module declares and nothing has exercised: unassigning must take the anchor
|
|
||||||
away and refresh the bundles ([ADR 0147](../../02-DECISIONS/0147-a-module-anchors-the-meshs-authority.md)),
|
|
||||||
and that is the half most likely to be wrong, because it is the half nobody reaches by accident.
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
# 129 — resolved: the workstation trusts the mesh, and stops when told to
|
|
||||||
|
|
||||||
*Done and measured on the machine, 2026-09-30.*
|
|
||||||
|
|
||||||
## What was done
|
|
||||||
|
|
||||||
Three steps, not the one the plan expected — the module was merged and had never been registered
|
|
||||||
(see [the diagnosis](01-diagnosis.md)):
|
|
||||||
|
|
||||||
1. **Registered** `ca-trust` from the catalogue. No artifact to build: it declares a directory, a
|
|
||||||
script, a unit and a service, and no image.
|
|
||||||
2. **Assigned** it to the workstation the issue was opened on, and pushed.
|
|
||||||
3. **Verified** with a plain client, then **unassigned and pushed again** to exercise removal, then
|
|
||||||
assigned and pushed once more.
|
|
||||||
|
|
||||||
The host's own account of arriving:
|
|
||||||
|
|
||||||
```
|
|
||||||
created ca-trust.state (/var/lib/ca-trust)
|
|
||||||
created ca-trust.anchor (/var/lib/ca-trust/anchor)
|
|
||||||
created ca-trust.unit (/etc/systemd/system/mesh-ca-trust.service)
|
|
||||||
updated ca-trust.trust (mesh-ca-trust.service): boot disabled to enabled, stopped to running
|
|
||||||
```
|
|
||||||
|
|
||||||
## It works, by the check the report asked for
|
|
||||||
|
|
||||||
The report's own reproduction, with no flags and nothing installed by hand:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ curl -sS -o /dev/null -w '%{http_code}' https://git.novox.internal/
|
|
||||||
200
|
|
||||||
$ openssl s_client -connect git.novox.internal:443 -servername git.novox.internal
|
|
||||||
issuer=O=Mesh Internal CA, CN=Mesh Internal CA Intermediate CA
|
|
||||||
Verify return code: 0 (ok)
|
|
||||||
$ trust list | grep -A2 Mesh
|
|
||||||
label: Mesh Internal CA Root CA
|
|
||||||
trust: anchor
|
|
||||||
category: authority
|
|
||||||
```
|
|
||||||
|
|
||||||
Four internal names, all verifying: `git` 200, `umami` 200, `keycloak` 302, `drive` 302. Before this,
|
|
||||||
every one of them was `curl: (60) … unable to get local issuer certificate (20)`.
|
|
||||||
|
|
||||||
**And the consequence the report named specifically**: git over HTTPS to the mesh's forge, which it said
|
|
||||||
had forced the working clone URL to be ssh-only.
|
|
||||||
|
|
||||||
```
|
|
||||||
$ git ls-remote https://git.novox.internal/novox/hq.git HEAD
|
|
||||||
76fbe323ea3401fcdadbf500c61bd3fa5a0a8603 HEAD
|
|
||||||
```
|
|
||||||
|
|
||||||
## Removal is symmetric, which nothing had ever shown
|
|
||||||
|
|
||||||
[ADR 0147](../../02-DECISIONS/0147-a-module-anchors-the-meshs-authority.md) says the module anchors the
|
|
||||||
authority *and takes it away again*. That half had never run. Unassigning and pushing:
|
|
||||||
|
|
||||||
```
|
|
||||||
removed ca-trust.trust (mesh-ca-trust.service)
|
|
||||||
removed ca-trust.unit (/etc/systemd/system/mesh-ca-trust.service)
|
|
||||||
removed ca-trust.anchor (/var/lib/ca-trust/anchor)
|
|
||||||
removed ca-trust.state (/var/lib/ca-trust)
|
|
||||||
```
|
|
||||||
|
|
||||||
Then: the anchor file gone, `trust list` naming no authority of the mesh's, and the plain client back to
|
|
||||||
`unable to get local issuer certificate (20)`. A machine that leaves the mesh stops trusting it, as the
|
|
||||||
record claims.
|
|
||||||
|
|
||||||
**The order is what makes it work, and is worth saying.** The service is removed *first*, so systemd
|
|
||||||
runs the unit's `ExecStop` — which is what deletes the certificate and refreshes the bundles — while the
|
|
||||||
script it calls still exists. Had the script or the state directory gone first, stopping the unit would
|
|
||||||
have had nothing to run, and the anchor would have been left behind with nothing declaring it. Nothing
|
|
||||||
in the module says this; it is the host's removal order that makes the module's symmetry real.
|
|
||||||
|
|
||||||
## What this leaves
|
|
||||||
|
|
||||||
- **Three machines of four** *(extended the same day, after the first was proven)*. `ca-trust` is now
|
|
||||||
assigned to every converged machine, and each verifies with a plain client:
|
|
||||||
|
|
||||||
```
|
|
||||||
novox mesh CA in trust store: 1 https://git.<node>.internal/ -> 200
|
|
||||||
g14 mesh CA in trust store: 1 https://git.<node>.internal/ -> 200
|
|
||||||
shanks mesh CA in trust store: 1 https://git.<node>.internal/ -> 200
|
|
||||||
```
|
|
||||||
|
|
||||||
Before, each of the two that had not been assigned it answered
|
|
||||||
`curl: (60) … unable to get local issuer certificate (20)` and held no entry for the mesh.
|
|
||||||
|
|
||||||
**`ace` is deliberately not among them.** It is the adopted machine, still carrying the
|
|
||||||
predecessor's resolver and filter, and it is not converged until the network and module-assignment
|
|
||||||
work is settled. Assigning a module to it would hold rather than run
|
|
||||||
([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)), which is
|
|
||||||
correct and is not the same as trusting anything.
|
|
||||||
|
|
||||||
- **It arrives per assignment, which is a shape worth questioning.** The report's reasoning — that
|
|
||||||
being on the private network is what makes a machine one that speaks to the mesh's names — argues
|
|
||||||
for a fact carried to every machine on the network, the way the roster and the registry trust are.
|
|
||||||
ADR 0147 chose a module assigned per machine, and this record does not reopen it; the cost is that
|
|
||||||
a machine joining the mesh trusts nothing until somebody remembers a second command.
|
|
||||||
- **`service-manager` reports `degraded` on this machine** and the module ran anyway. Worth knowing that
|
|
||||||
the capability gate passes on a degraded service manager, since a module whose whole delivery is a
|
|
||||||
unit is the kind that would be worst served by one.
|
|
||||||
- **The predecessor's authority is still in the trust store**, beside the mesh's now rather than instead
|
|
||||||
of it. The report notes that it cannot be retired while anything on the machine speaks TLS to a mesh
|
|
||||||
name; that is no longer true here, and retiring it is its own piece of work.
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
---
|
---
|
||||||
status: resolved
|
status: located
|
||||||
fixed-by: mesh-host 3112c88 — undeclaring gives a unit back the state it was found in, and removes only a process the mesh made
|
|
||||||
opened: 2026-09-27
|
opened: 2026-09-27
|
||||||
located-in: [mesh-host internal/apply/apply.go (remove)]
|
located-in: [mesh-host internal/apply/apply.go (remove)]
|
||||||
amended-design: 02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md
|
amended-design: 02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md
|
||||||
@@ -16,7 +15,7 @@ found that the host's `remove` path stops every `service` resource that is no lo
|
|||||||
delete". `store.Orphans` matches by id alone. So any of these stops the unit:
|
delete". `store.Orphans` matches by id alone. So any of these stops the unit:
|
||||||
|
|
||||||
- the module is unassigned — by mistake, or to switch it for another;
|
- the module is unassigned — by mistake, or to switch it for another;
|
||||||
- the node is sent a deliberately-empty declaration ([issue 149](../149-a-declaration-that-shrinks-to-empty-is-skipped-not-sent/00-report.md));
|
- the node is sent a deliberately-empty declaration ([issue 127](../127-a-declaration-that-shrinks-to-empty-is-skipped-not-sent/00-report.md));
|
||||||
- a later catalogue version renames the resource's `id`.
|
- a later catalogue version renames the resource's `id`.
|
||||||
|
|
||||||
That is right for a service the mesh brought into being. It is wrong for a unit the mesh
|
That is right for a service the mesh brought into being. It is wrong for a unit the mesh
|
||||||
@@ -65,9 +64,3 @@ something to settle in passing.
|
|||||||
|
|
||||||
The unassign preview is partly answered — the host's plan names each unit it will stop — and the
|
The unassign preview is partly answered — the host's plan names each unit it will stop — and the
|
||||||
controller's side is left open.
|
controller's side is left open.
|
||||||
|
|
||||||
## Closed
|
|
||||||
|
|
||||||
*2026-09-29, in a grooming pass rather than by whoever fixed it.* Undeclaring no longer stops a unit the mesh only reloaded or only kept running. Found by
|
|
||||||
reading what the code repositories' commits cite: the fix names this issue and is on `main`. It was
|
|
||||||
not re-verified on a machine, and this record says so rather than implying a run that did not happen.
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
status: resolved
|
status: open
|
||||||
opened: 2026-09-27
|
opened: 2026-09-27
|
||||||
located-in: [mesh-catalog modules/gitea, mesh-controller cmd/mesh-controller, mesh-controller internal/builder]
|
located-in: []
|
||||||
fixed-by: mesh-catalog #124 — the forge module watches for merged pull requests and emits `pull.merged` with the merge commit and the clone address; mesh-controller #110/#111 — the control plane follows that event on the bus, marks every module built from that repository and branch as moved, and builds them bases first, stopping when a base fails; mesh-controller #113/#114 — a build records the bases it was handed and the graph is read from builds, without which "bases first" had no edges to order by.
|
fixed-by:
|
||||||
amended-design: 03-DESIGN/01-to-be/28-building-the-bus.md
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
# 131 — Nothing tells the mesh a source moved, and it reports itself current anyway
|
# 131 — Nothing tells the mesh a source moved, and it reports itself current anyway
|
||||||
@@ -82,26 +82,3 @@ failures.
|
|||||||
- Does this want to be the same mechanism as the build request on the bus
|
- Does this want to be the same mechanism as the build request on the bus
|
||||||
([ADR 0129](../../02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md)), or does it sit in
|
([ADR 0129](../../02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md)), or does it sit in
|
||||||
front of it?
|
front of it?
|
||||||
|
|
||||||
## What was done (2026-09-28)
|
|
||||||
|
|
||||||
The shape above was built as described: the forge's module emits the merge, the control plane
|
|
||||||
consumes it, and nothing on either side knows the other's internals. The build is asked for the
|
|
||||||
merge commit, not each commit the merge brought — the trunk moved once, to one place. Several merges
|
|
||||||
for one module arriving in a row are followed in turn, each moving the recorded source to its own
|
|
||||||
commit, so the last one to arrive is the one the mesh ends up built from.
|
|
||||||
|
|
||||||
The hand-operated form stays. `module moved` is how a source is recorded without a forge — a module
|
|
||||||
built from a repository elsewhere, or a mesh whose forge module is down — and it is the same act the
|
|
||||||
event performs, so the two cannot disagree about what "moved" means.
|
|
||||||
|
|
||||||
**Bases first needed edges, and there were none.** The order this report asked for was written and
|
|
||||||
walked a graph that no build had ever recorded: a recipe reads its base from a build argument, so the
|
|
||||||
digest was never in the file the builder read edges from. A build now reports what it was handed, the
|
|
||||||
control plane records it by artifact path, and the order is read from each module's newest build.
|
|
||||||
|
|
||||||
**What still can lie.** The overview compares what was built against where it was last told the
|
|
||||||
source is; the forge's event is now what moves that mark, so it is right for as long as the forge
|
|
||||||
module was listening. A merge made while that module was down is a merge the mesh does not know of
|
|
||||||
until the module next polls — it announces what merged since it last looked, so the gap closes when
|
|
||||||
it comes back, and not before. The overview does not say so.
|
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
---
|
|
||||||
status: resolved
|
|
||||||
opened: 2026-09-28
|
|
||||||
located-in: [mesh-controller cmd/mesh-controller]
|
|
||||||
fixed-by: mesh-controller — `module add` takes `--path` and `--self`, so a module handed over by hand records the whole location it came from; a record naming a repository and no directory says so in the reply; the rule is one function with a test beside it. The nine records already wrong were corrected by rebuilding each with its real directory, which is the same act through the same door.
|
|
||||||
amended-design:
|
|
||||||
---
|
|
||||||
|
|
||||||
# 132 — A module can be recorded without the directory it lives in
|
|
||||||
|
|
||||||
## What was observed
|
|
||||||
|
|
||||||
Nine modules on one mesh could not be rebuilt. Each attempt failed the same way:
|
|
||||||
|
|
||||||
> has no module.json at its root, so there is nothing saying what it is
|
|
||||||
|
|
||||||
All nine were recorded as coming from a repository that holds many modules, each in its own
|
|
||||||
directory — and each record named the repository and no directory. So every build cloned the
|
|
||||||
repository and looked for a manifest where there has never been one.
|
|
||||||
|
|
||||||
The failure only surfaced when something asked for all of them at once. Before that, the overview
|
|
||||||
said every module was current with its source, because what it compares is what was built against
|
|
||||||
what the mesh was last told the source has, and neither half knows whether the source can be found
|
|
||||||
at all.
|
|
||||||
|
|
||||||
## Why it matters beyond this instance
|
|
||||||
|
|
||||||
**A module is a repository and a directory inside it** ([ADR 0069](../../02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md)),
|
|
||||||
and one of the two doors into the catalogue could record only the first half. A build records the
|
|
||||||
directory it was given, so a module that arrived by being built is always whole; a module handed over
|
|
||||||
by hand had no way to say where it lived, and the flag to say it did not exist. The rule was decided
|
|
||||||
and enforced on one path out of two.
|
|
||||||
|
|
||||||
**Half a location reads exactly like a whole one.** Nothing in the record is empty in a way a person
|
|
||||||
would notice: the repository is there, the branch is there, the commit is there. The mesh only finds
|
|
||||||
out at the moment it needs the manifest, which is the moment it is trying to rebuild — and the module
|
|
||||||
stays on whatever it last built, indefinitely, with nothing saying why.
|
|
||||||
|
|
||||||
**It is the same shape as [131](../131-nothing-tells-the-mesh-a-source-moved/00-report.md).** A
|
|
||||||
comparison between two facts the mesh holds about itself will agree with itself. Whether the source
|
|
||||||
can be found is a question only an attempt to read it answers, and the answer had nowhere to go.
|
|
||||||
|
|
||||||
## What was done
|
|
||||||
|
|
||||||
`module add` takes the directory and which forge holds the repository, so a hand-registered module
|
|
||||||
records the same whole location a built one does. What a record must say to be worth anything is one
|
|
||||||
function with a test beside it, rather than a paragraph in a help string: provenance together or not
|
|
||||||
at all, a directory needs a repository to be inside, a path on the mesh's own forge is not an address.
|
|
||||||
And a record that names a repository but no directory says so when it is made — not refused, because a
|
|
||||||
module really at a repository's root is ordinary, but said, because the person adding it is the one
|
|
||||||
who knows which it is.
|
|
||||||
|
|
||||||
The nine wrong records were corrected by building each with its real directory, which re-records it.
|
|
||||||
No row was written by hand.
|
|
||||||
|
|
||||||
## What is still true
|
|
||||||
|
|
||||||
A directory that does not exist in the repository cannot be refused when the module is added: the
|
|
||||||
control plane does not clone, and inventing a check there would mean it did. The first build says so
|
|
||||||
plainly, which is one build rather than nine, and the record it leaves behind is right from then on.
|
|
||||||
-78
@@ -1,78 +0,0 @@
|
|||||||
---
|
|
||||||
status: resolved
|
|
||||||
opened: 2026-09-28
|
|
||||||
located-in: [mesh-controller module.json]
|
|
||||||
fixed-by: mesh-controller — the control plane's module declares a run-once `migrate` step before its server, which is the shape ADR 0052 prescribes for exactly this. A step's record of having run is the digest of its declaration and the image is part of that digest, so a new build of the control plane re-runs it; and because a run-once step gates what the declaration places after it, a migration that fails stops the new server from starting at all rather than letting it run against a schema it does not have.
|
|
||||||
amended-design: 03-DESIGN/01-to-be/32-what-a-module-declares.md
|
|
||||||
---
|
|
||||||
|
|
||||||
# 133 — The control plane's schema is migrated at birth and never again
|
|
||||||
|
|
||||||
## What was observed
|
|
||||||
|
|
||||||
On 2026-09-28 at 08:17 the control plane was replaced, by the mesh's own upgrade path, with a build
|
|
||||||
whose code writes a column that a migration **in that same build** creates. Nothing ran the migration.
|
|
||||||
|
|
||||||
For the next three quarters of an hour the mesh built things and recorded none of them. Every build
|
|
||||||
answered:
|
|
||||||
|
|
||||||
> ERROR: column "built_contexts" of relation "build" does not exist (SQLSTATE 42703)
|
|
||||||
|
|
||||||
and that sentence went only to whoever happened to be waiting on a build's reply. The overview kept
|
|
||||||
saying the mesh was fine. The builds themselves worked — images were built and published — so the
|
|
||||||
registry filled up with artifacts the mesh has no record of, and the graph stopped learning without
|
|
||||||
anything saying so.
|
|
||||||
|
|
||||||
The schema was created once, at genesis, by an action in the foundation bundle that runs the same
|
|
||||||
binary's `migrate`. Nothing runs it again. The mesh has updated its own control plane many times since
|
|
||||||
that bundle, and every one of those updates carried whatever migrations the new build brought and
|
|
||||||
applied none of them. This is the first time a build needed one.
|
|
||||||
|
|
||||||
## Why it matters beyond this instance
|
|
||||||
|
|
||||||
**The schema and the code that needs it ship as one artifact and are applied by two mechanisms, only
|
|
||||||
one of which is automatic.** A module's version is atomic everywhere else in the mesh — the manifest,
|
|
||||||
the image and what the machine runs move together. Its schema did not, so "the mesh updates itself on
|
|
||||||
a push" was true of the code and false of what the code needs.
|
|
||||||
|
|
||||||
**The failure is quiet exactly where quiet is worst.** A build that cannot be recorded is a build that
|
|
||||||
happened and left no trace, which is the fault [issue 050](../050-the-catalogue-knows-nothing-built-before-it/00-report.md)
|
|
||||||
and [issue 131](../131-nothing-tells-the-mesh-a-source-moved/00-report.md) are both about. The mesh
|
|
||||||
has three mechanisms for noticing a module is behind its source and none for noticing that what it
|
|
||||||
recorded was refused.
|
|
||||||
|
|
||||||
**The shape was already decided, and the control plane was the one module that did not use it.**
|
|
||||||
[ADR 0052](../../02-DECISIONS/0052-a-step-that-runs-once-before-a-container.md) says a run-once container is a
|
|
||||||
step the host runs to completion before whatever the declaration places after it, and names migrating
|
|
||||||
a schema as the case it exists for. The genesis code's own comment says a manifest may name its image
|
|
||||||
in more than one resource — "a migrate step beside the server". The control plane's manifest had no
|
|
||||||
such step; it went straight from a state directory to the server.
|
|
||||||
|
|
||||||
## What is still true
|
|
||||||
|
|
||||||
**Additive migrations are load-bearing, not a style preference.** The step runs before the *new*
|
|
||||||
server starts, which means the old binary briefly runs against the new schema. A migration that
|
|
||||||
removes or renames something would break the running control plane in the window between the two.
|
|
||||||
|
|
||||||
**A hand-written step is one the next module forgets**, which is why this fix is not where the matter
|
|
||||||
ends: [ADR 0135](../../02-DECISIONS/0135-a-module-version-prepares-its-state-before-it-runs.md) makes it
|
|
||||||
derived and puts it where an author works: a module version declares an entrypoint that prepares its
|
|
||||||
state, and the mesh composes the gated work from it, so the control plane stops being the only module
|
|
||||||
that had to remember. That record also settles the level question HAL answered with stages — a consumer
|
|
||||||
is a module on a machine, so the scope of preparation is the scope of the state — and
|
|
||||||
[ADR 0134](../../02-DECISIONS/0134-the-mesh-says-what-it-applied.md) answers the second open question
|
|
||||||
below: what a machine applied, and what it refused, become facts on the bus rather than a line in a log.
|
|
||||||
|
|
||||||
**The mesh now has two shapes for one problem.** The catalogue module migrates its own schema in its
|
|
||||||
own code when it starts; the control plane migrates in a step the host gates on. Both work and the
|
|
||||||
reasons differ — a module that owns its store entirely can do it at start, while a step is visible in
|
|
||||||
the declaration and refuses to let a broken upgrade serve. Which one the mesh should standardise on is
|
|
||||||
a decision, not a fix, and it is not made here.
|
|
||||||
|
|
||||||
## Open questions
|
|
||||||
|
|
||||||
- Should a module be refusable at registration when it ships migrations and declares no step and no
|
|
||||||
other way to apply them? The mesh can see both halves.
|
|
||||||
- Should a record the store refuses reach the overview? Today the only reader of that failure is
|
|
||||||
whoever asked for the thing that failed, and for an event arriving on the bus there is no such
|
|
||||||
person.
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user