Compare commits
171
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4151e6bc4 | ||
|
|
d8083bcf9e | ||
|
|
6f26f97fdb | ||
|
|
63ed4a7c96 | ||
|
|
48ca2fb41b | ||
|
|
3976f09738 | ||
|
|
2904c359b8 | ||
|
|
b277f3b4ba | ||
|
|
50e4d9c2a7 | ||
|
|
a7d0dd83d1 | ||
|
|
a2c9fbb665 | ||
|
|
0278766dfb | ||
|
|
606fbb7add | ||
|
|
a92e4bf121 | ||
|
|
187442ec7b | ||
|
|
47c45d4386 | ||
|
|
82496536cd | ||
|
|
b0de267301 | ||
|
|
b0a74b23fd | ||
|
|
6a5f68d11f | ||
|
|
821cd3b489 | ||
|
|
49b0319230 | ||
|
|
86d1763cfa | ||
|
|
a7e9e6dea0 | ||
|
|
ea0853ca46 | ||
|
|
fcd27c8399 | ||
|
|
03e39316ea | ||
|
|
65a252dc00 | ||
|
|
6be284c781 | ||
|
|
ea9a433385 | ||
|
|
9ddd7215ad | ||
|
|
626af3e8e2 | ||
|
|
180e3b8f7e | ||
|
|
4ae452d0ad | ||
|
|
f35f3757bb | ||
|
|
6b8fb562ce | ||
|
|
2175d13935 | ||
|
|
39f0d64840 | ||
|
|
eef03f2b08 | ||
|
|
64acc94de8 | ||
|
|
99eb322de5 | ||
|
|
5460681117 | ||
|
|
0acb47fa55 | ||
|
|
7a633f2780 | ||
|
|
bef510fda2 | ||
|
|
c58f4d6790 | ||
|
|
d29d3dfc23 | ||
|
|
d85b41ae4d | ||
|
|
e00e3bc3ce | ||
|
|
b8d8101c45 | ||
|
|
85961f8348 | ||
|
|
48a620249b | ||
|
|
7f0fe27cf2 | ||
|
|
bfc410fafe | ||
|
|
d436122be2 | ||
|
|
76a535e69e | ||
|
|
027e5b8d73 | ||
|
|
79d1619f16 | ||
|
|
5a6b7ca4f8 | ||
|
|
e1f2c6bd5b | ||
|
|
cf8134e318 | ||
|
|
35f7f4401b | ||
|
|
62d61938ad | ||
|
|
f841845b0d | ||
|
|
3627f7e9db | ||
|
|
2ffe1d0915 | ||
|
|
763e327610 | ||
|
|
93f828c5eb | ||
|
|
fe706af63a | ||
|
|
52e9df0f02 | ||
|
|
36454d7e4a | ||
|
|
e84c822e89 | ||
|
|
a170913202 | ||
|
|
9a20c16d9b | ||
|
|
8bd0ca0bdc | ||
|
|
598f6a8952 | ||
|
|
3341c037cb | ||
|
|
22a28ad548 | ||
|
|
1e1957a9c4 | ||
|
|
5292f4176a | ||
|
|
822e8b03f8 | ||
|
|
a82941ee0c | ||
|
|
9ffb7eec55 | ||
|
|
7499f1e50c | ||
|
|
214b486a50 | ||
|
|
90b44a48df | ||
|
|
860331dc37 | ||
|
|
37b46d5349 | ||
|
|
16855ade02 | ||
|
|
8dd566c0aa | ||
|
|
a98ee0f529 | ||
|
|
ad4a5ea004 | ||
|
|
0cf1ad5dad | ||
|
|
69a002fce3 | ||
|
|
16a1a52cd8 | ||
|
|
af170e3a67 | ||
|
|
6c2d5f5913 | ||
|
|
04c9500b5b | ||
|
|
d0044cf555 | ||
|
|
846c1f85f2 | ||
|
|
9eef0bd525 | ||
|
|
105ae9a56a | ||
|
|
ee801a6441 | ||
|
|
90b89aa1c9 | ||
|
|
e33191161d | ||
|
|
6e08cdf3d6 | ||
|
|
02f291a129 | ||
|
|
a0a930b1cd | ||
|
|
8d9c9ab6b5 | ||
|
|
9b14430d3f | ||
|
|
a4384f13d3 | ||
|
|
49b1136ded | ||
|
|
743051efe7 | ||
|
|
e8470057aa | ||
|
|
a841e2c173 | ||
|
|
3c535ead31 | ||
|
|
4cf941d858 | ||
|
|
5042ffd8d3 | ||
|
|
39340fcd76 | ||
|
|
4f9dc3906e | ||
|
|
a2542e51f8 | ||
|
|
b19b29cd3a | ||
|
|
4bf4fe2d06 | ||
|
|
5e12081774 | ||
|
|
9ad64881a9 | ||
|
|
266ade6e28 | ||
|
|
a794ef9a3f | ||
|
|
e10084fed6 | ||
|
|
941b920bd7 | ||
|
|
ecbd2ce2e6 | ||
|
|
b7f7b97d8a | ||
|
|
c2cf0d72d6 | ||
|
|
0a5006b366 | ||
|
|
3ea9601904 | ||
|
|
f7a37ee4f3 | ||
|
|
601d004fcb | ||
|
|
d009c3efef | ||
|
|
5036b927b9 | ||
|
|
1f72e82b84 | ||
|
|
76fbe323ea | ||
|
|
295bf2f3ab | ||
|
|
e1b74810a8 | ||
|
|
e41eed0852 | ||
|
|
ba30286896 | ||
|
|
53b94c51bb | ||
|
|
83791f0921 | ||
|
|
bf4d4e2e7b | ||
|
|
ec42ee0846 | ||
|
|
5a3dee9e9e | ||
|
|
47909c6b71 | ||
|
|
7e5edab8da | ||
|
|
3649f82204 | ||
|
|
b400ce2c54 | ||
|
|
995c8cb266 | ||
|
|
f89aef992d | ||
|
|
b967ef7be3 | ||
|
|
e417906241 | ||
|
|
b1bf895688 | ||
|
|
0d64677c70 | ||
|
|
04205c1dc8 | ||
|
|
9dc49cd831 | ||
|
|
66b413a076 | ||
|
|
fcba05fed9 | ||
|
|
18f37c25b2 | ||
|
|
3c2b4fc6b6 | ||
|
|
72eaf52867 | ||
|
|
741625e725 | ||
|
|
c3730b9a23 | ||
|
|
3a59099c81 | ||
|
|
3bd6f34de3 | ||
|
|
ec8676c225 |
@@ -54,4 +54,6 @@ 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)):
|
||||
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
|
||||
research overview says what it became. `python3 00-META/checks/cycle.py`
|
||||
research overview says what it became, and no two issue records share a number (issue 155 — the
|
||||
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`
|
||||
|
||||
+23
-1
@@ -14,7 +14,8 @@ What is enforced:
|
||||
its owning code (`code:`) -- no development without a design that says where.
|
||||
issues a known `status:`; once `located`, `located-in:` names the owner;
|
||||
once `resolved`, `fixed-by:` says what fixed it (prose counts --
|
||||
"nothing, the capability existed" is an answer).
|
||||
"nothing, the capability existed" is an answer). And no two records share a
|
||||
number -- the number is how a record is cited.
|
||||
research a known `status:`; a `graduated` overview says what it `became:`, and every
|
||||
target it names exists.
|
||||
decisions every accepted record is REACHABLE from the cycle: cited by a design doc's
|
||||
@@ -109,6 +110,27 @@ def main():
|
||||
"without a design that says where" % status)
|
||||
|
||||
# ---- 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"))):
|
||||
front = frontmatter(path)
|
||||
if front is None:
|
||||
|
||||
+17
-2
@@ -52,8 +52,11 @@ 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
|
||||
**version**. Served by the **package-registry** (gitea). Only a builder talks to it.
|
||||
- **artifact** — what the mesh delivers to a machine to **install and run**: an OCI image, by
|
||||
**digest**. Served by the **artifact-store** (distribution). Every node pulls from it.
|
||||
- **artifact** — anything a build produces and the mesh delivers to a machine by **digest**: an
|
||||
`image`, a mirrored `upstream` image, a `bundle` of the module's own code, an `archive`. Served by
|
||||
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).
|
||||
|
||||
## How modules relate to the mesh
|
||||
@@ -75,6 +78,18 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
|
||||
is a role you occupy, and the two meet where a seat delivers a provision: occupying the seat is
|
||||
what makes a module *the* provider of it.
|
||||
|
||||
## The surfaces
|
||||
|
||||
- **console** — the module (`mesh-console`) that puts the mesh's tools in front of whoever is on a
|
||||
machine: an MCP endpoint on the machine's loopback for an agent, the same endpoint for a person. It
|
||||
is assigned like any module, holds a credential the mesh minted, and calls tools under a grant its
|
||||
manifest declares (`invokes`). Loopback is the authority boundary: whoever is on the machine owns the
|
||||
mesh there ([ADR 0152](../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)).
|
||||
Not "the tool bridge", "the brain" or "the MCP server" — those name the predecessor's program or a
|
||||
protocol, and the console is a module.
|
||||
- **invokes** — the manifest word for the tools a module calls, `<module>.<tool>` each or `*` for
|
||||
every one. A grant on the publish side and nothing else; a module that declares none calls nothing.
|
||||
|
||||
## How this page is kept
|
||||
|
||||
A new name for an existing thing lands here first, in the same change that introduces it in code. A
|
||||
|
||||
@@ -21,7 +21,12 @@ incident someone must **clear**.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Take the next free number. Create `04-ISSUES/NNN-short-name/00-report.md`:
|
||||
1. Take the next free number — **across `main` and every open pull request**, not `main` alone.
|
||||
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
|
||||
---
|
||||
@@ -42,6 +47,14 @@ incident someone must **clear**.
|
||||
## Rules
|
||||
|
||||
- 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
|
||||
the next person searching a symptom finds it. Both, not either.
|
||||
- `status: wontfix` is legitimate and requires a sentence saying why.
|
||||
|
||||
@@ -13,6 +13,14 @@ 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.
|
||||
|
||||
> **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
|
||||
|
||||
It boots a stock Linux image, runs the real install, and becomes a node. **It is not a model of
|
||||
|
||||
@@ -72,6 +72,12 @@ answers from it. Nothing flows back: this repository is public, the mesh is not,
|
||||
would be how installation-specific detail arrives into documents that must not carry it
|
||||
([`README.md`](../README.md)).
|
||||
|
||||
> **The mechanism changed — 2026-09-30, by ADR 0153.** What stands: read where it is written, no copy,
|
||||
> one-way. What moved: the reader is a module the mesh assigns (`records`, a checkout at a commit every
|
||||
> answer names) rather than the agent session of design 15, which is not built; and "the search consults
|
||||
> the agent" has no store to consult since the cut-over — the console's tool list is where the record
|
||||
> appears beside everything else. [ADR 0153](0153-the-record-is-read-by-a-module-and-the-console-lists-it.md).
|
||||
|
||||
## Consequences
|
||||
|
||||
**This repository stops being a fourth knowledge system, properly.** The original objection was
|
||||
|
||||
@@ -26,6 +26,14 @@ 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,
|
||||
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
|
||||
|
||||
### A module with tools or events runs a process of its own
|
||||
|
||||
@@ -80,6 +80,15 @@ 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
|
||||
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
|
||||
|
||||
- **Lab-versus-production is one node-level `public-domain` setting**, not an override on every
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
---
|
||||
topic: building it
|
||||
status: proposed
|
||||
status: superseded
|
||||
date: 2026-09-12
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
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
|
||||
|
||||
> **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
|
||||
|
||||
**The lab is exclusive hardware, and today a person holds it.** Raising a scenario takes over
|
||||
|
||||
@@ -35,6 +35,16 @@ The development cycle is enforced mechanically, to the extent frontmatter can ca
|
||||
capability existed" is an answer).
|
||||
- **No silent graduation** — a `graduated` research overview says what it `became:`, and the
|
||||
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`
|
||||
and `index.py`; all three run before any merge here. What frontmatter cannot see — that code work
|
||||
|
||||
@@ -106,3 +106,14 @@ is refused with the candidates named, never resolved by picking.
|
||||
gap, and the day-one evidence.
|
||||
- [`03-DESIGN/01-to-be/23-choosing-a-provider.md`](../03-DESIGN/01-to-be/23-choosing-a-provider.md)
|
||||
— the design.
|
||||
|
||||
> **Widened, 2026-10-01 (issue #258).** The pin named a node, on the reasoning that "the same
|
||||
> module on two machines is two answers, and which machine is the whole question". Half right:
|
||||
> two modules on one machine can both answer a provision — `public-acme` and `step-ca` both offer
|
||||
> `acme-ca` on novox — and then which *module* is the whole question, and a node alone cannot ask
|
||||
> it. A provider is a (node, module) pair ([design 23](../03-DESIGN/01-to-be/23-choosing-a-provider.md)),
|
||||
> and a pin now names the pair: `pin <node> <provision> <from-node> <module>`. The resolver
|
||||
> refuses a node that answers twice instead of taking the last one listed, and refuses two
|
||||
> providers beside the consumer instead of settling them by a map walk — the same stance design 23
|
||||
> takes: ambiguity is refused, never resolved by picking. Records made before are completed by
|
||||
> migration where the node they name answers once. mesh-controller: `feat/pin-names-the-provider`.
|
||||
|
||||
@@ -47,6 +47,14 @@ Anything with the control plane in reach can ask any module anything it serves.
|
||||
harder: nothing outside the control plane can, and the control plane's connection is one more
|
||||
thing on the path of every question — a cost accepted for the audit it buys.
|
||||
|
||||
> **The mechanism changed — 2026-09-30, by ADR 0152.** What stands: `ask` on the control plane, and
|
||||
> that every call passes an account whose permission list says what it may ask. What moved: "nothing
|
||||
> outside the control plane can" stopped being true when a person's account gained a publish grant per
|
||||
> tool (design 25 §7, 2026-09-28), and [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)
|
||||
> takes the first option above for a module as well — a manifest declares `invokes`, and the bus grants
|
||||
> exactly that publish side. The audit the second option bought is the bus's permission list, which
|
||||
> derives both.
|
||||
|
||||
## How it is checked
|
||||
|
||||
A tools-only bed asks a served tool through the control plane and asserts an answer arrived —
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: proposed
|
||||
status: accepted
|
||||
date: 2026-09-26
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
@@ -163,6 +163,14 @@ value, the requirement is marked not rotatable by the mesh, and a rotation is re
|
||||
**The number of parties decides, never the provider.** The resolver knows it from the requirement's
|
||||
recipients, leaving out the vault's custody copy, so no definition declares it.
|
||||
|
||||
> **Progressive insight — 2026-10-01.** The number of parties is the resolver's to know; *which form*
|
||||
> a single party's credential takes is not, and cannot be: whether a module reads its secret when it
|
||||
> starts or applies it once to a backend is a fact about the software, visible nowhere in the graph.
|
||||
> So the definition declares that half — `taken: at-start` or `taken: applied` on an own secret — and
|
||||
> a secret that declares neither is not rotated, refused with the word to write (issue 180). The
|
||||
> read-at-start form is built; the staged form for an applied credential is not. The decision stands;
|
||||
> the sentence above was one fact short.
|
||||
|
||||
### Until an adapter can
|
||||
|
||||
**An adapter that cannot yet ensure a second credential says so.** The two-party credentials it applies
|
||||
@@ -189,6 +197,23 @@ 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.
|
||||
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
|
||||
|
||||
- **Every credential provider's adapter changes**, in two steps. The first separates *retire a
|
||||
|
||||
+3
-1
@@ -81,7 +81,9 @@ 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
|
||||
renames with no migration.
|
||||
- **The registry seats — `the-artifact-store`, `npm-package-registry` (→ `mesh-artifact-store`,
|
||||
`mesh-npm-package-registry`) — and `git` (→ `mesh-git`) — are decided but deferred.** They each
|
||||
`mesh-npm-package-registry`) — and `git` (→ `mesh-git`) — are decided but deferred.** *(The
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -109,6 +109,23 @@ understated as none. The remaining work is a way to build the host and a way to
|
||||
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
|
||||
|
||||
@@ -113,6 +113,36 @@ the authority it was bound to, checked in the control plane's own test suite —
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
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
|
||||
@@ -0,0 +1,106 @@
|
||||
---
|
||||
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
@@ -0,0 +1,113 @@
|
||||
---
|
||||
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
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
topic: the tiers
|
||||
status: accepted
|
||||
date: 2026-09-30
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0066-public-routing-is-name-agnostic.md
|
||||
---
|
||||
|
||||
# 151. A route's internal name is composed under the node that serves it
|
||||
|
||||
## Context
|
||||
|
||||
A module that requires a route is given two names from one label: a public one, `<label>.<public
|
||||
domain>`, and an internal one, `<label>.<node>.internal`
|
||||
([ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md)). Both were composed
|
||||
from the node the module runs on.
|
||||
|
||||
The two are answered differently. The public name is published into every machine's roster at the
|
||||
address of the node whose proxy serves it ([ADR 0066](0066-public-routing-is-name-agnostic.md)), so
|
||||
it reaches the proxy from anywhere in the mesh. The internal name is answered by every machine's
|
||||
resolver as *anything under a node's name goes to that node*
|
||||
([design 08 §2](../03-DESIGN/01-to-be/08-connectivity.md)) — the node it was composed from, which is
|
||||
the consumer's. Where the proxy runs on another machine, that name sends a client to a machine with
|
||||
nothing listening, while the public name works
|
||||
([issue 139](../04-ISSUES/139-an-internal-route-name-resolves-to-the-consumers-node/00-report.md)).
|
||||
Every route on this mesh today is served beside its module, so it has not been seen; `route` is
|
||||
provided mesh-wide precisely so that stops being true.
|
||||
|
||||
Beside it, the roster gave every routed name a second entry with the mesh's suffix appended —
|
||||
`<name>.<public domain>.internal` — because it composed a full name for every entry as it does for a
|
||||
machine. That name resolved on every machine, was served by nothing, and was refused by the proxy at
|
||||
the handshake; the first three names tried while reproducing an unrelated issue were those, and the
|
||||
evidence pointed at a regression that had not happened
|
||||
([issue 157](../04-ISSUES/157-a-routed-names-internal-alias-is-served-by-nothing/00-report.md)).
|
||||
|
||||
## Considered Options
|
||||
|
||||
**1. Keep the consumer's name and publish it at the serving node's address**, as the public name is.
|
||||
The name stays `<label>.<consumer>.internal` and an exact roster entry overrides the wildcard.
|
||||
Rejected: it makes `<x>.<node>.internal` mean *goes to that node* except when it does not, which is
|
||||
the one rule the resolver design states; it needs an entry per route where the wildcard needed none;
|
||||
and which of an exact entry and a wildcard a resolver answers first is the resolver's business, which
|
||||
the mesh deliberately does not know.
|
||||
|
||||
**2. A proxy on every machine, so the serving node is always the consumer's.** Rejected for this
|
||||
question: it is a different decision about what `route` is — a node-scoped seat with a mesh-wide
|
||||
fallback — and this mesh runs one proxy on the hub today. Whatever is decided there, a route served
|
||||
from another machine must have a name that reaches it.
|
||||
|
||||
**3. Compose the internal name under the node that serves the route.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
**A route's internal name is `<label>.<serving node>.internal` — composed under the node whose proxy
|
||||
answers the route, which is the machine the request arrives at.** The public name is unchanged:
|
||||
`<label>.<public domain>` of the node the module runs on, which is where the operator put it.
|
||||
|
||||
Where the proxy runs beside the module — every route on this mesh today — the two nodes are one and
|
||||
nothing changes. Where it does not, the name says where the request goes, which is what a name under
|
||||
a node's name has always meant.
|
||||
|
||||
**A routed name has no mesh form.** The roster publishes it as itself, once, at the serving node's
|
||||
address. Only a machine has a bare name beside its full one.
|
||||
|
||||
What certifies the internal name is unchanged by this: the proxy that terminates it obtains a
|
||||
certificate from the mesh's authority for the names it is given, and it is given this one.
|
||||
|
||||
Taken on the operator's standing instruction to answer the open design questions in the work order.
|
||||
|
||||
## How this is checked
|
||||
|
||||
- **Composition.** A controller test contributes a route from a module on one node to a proxy offered
|
||||
from another, gathered the way the controller gathers a consumer's contribution for a provider on
|
||||
another machine, and asserts the internal name carries the serving node.
|
||||
- **Publication.** A controller test renders a roster with a machine and a routed name and asserts
|
||||
the routed name appears as itself, once, and never with the suffix appended.
|
||||
- **On the mesh.** After the change no machine's roster carries a `<domain>.internal` entry, and a
|
||||
route's internal name still answers from a container with a certificate from the mesh's authority.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **A route served from another machine now has a usable internal name.** The first module assigned
|
||||
that way will resolve, where before it would have resolved to the wrong machine with no error.
|
||||
- **The internal name of a route can change when its proxy moves.** A route re-homed from one proxy
|
||||
to another gets a new internal name, as the design's rule implies; clients that dialled the old one
|
||||
reach the old machine. The public name does not move with the proxy and is the stable one.
|
||||
- **The roster is one line shorter per routed name**, and a person reading a hosts file no longer
|
||||
finds names that resolve to a refusal.
|
||||
- **Issue 139's second question — a per-node route holder — is left open**, and is a decision about
|
||||
what a seat is rather than about a name.
|
||||
|
||||
## References
|
||||
|
||||
- [issue 139](../04-ISSUES/139-an-internal-route-name-resolves-to-the-consumers-node/00-report.md) — the question
|
||||
- [issue 157](../04-ISSUES/157-a-routed-names-internal-alias-is-served-by-nothing/00-report.md) — the alias
|
||||
- [ADR 0066](0066-public-routing-is-name-agnostic.md) — routed names propagate mesh-wide; extended here
|
||||
- [ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md) — how the two names are composed and how far each reaches
|
||||
- [design 08 §2](../03-DESIGN/01-to-be/08-connectivity.md) — anything under a node's name goes to that node
|
||||
@@ -0,0 +1,160 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-09-30
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
|
||||
---
|
||||
|
||||
# 152. The operator's surface is a module the mesh assigns: the console
|
||||
|
||||
## Context
|
||||
|
||||
**Since the bus moved, nobody can ask the mesh anything without opening a shell on a machine.** Every
|
||||
tool call an operator's assistant makes fails, on every machine including the one the operator sits
|
||||
at, with *AMQP not connected*
|
||||
([issue 147](../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md)).
|
||||
The program answering is the predecessor's tool server, started on the workstation by hand, with the
|
||||
predecessor's broker address written into the assistant's own configuration. It has no manifest, no
|
||||
assignment, no account on the bus, and the mesh has never known it exists. Nothing regressed: the
|
||||
mesh removed a transport that a program outside the mesh still dials.
|
||||
|
||||
**The mesh has a tool model, and it works.** A module serves each tool on its own subject and its
|
||||
account may serve nothing else ([ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md));
|
||||
a person is issued an account whose only permission is to publish the tool subjects named at issue
|
||||
([25 — The bus on NATS](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §7); a client on the
|
||||
runtime repository's main branch speaks that account as a command line and as an MCP server. Measured
|
||||
on the live mesh on 2026-09-28: a call to the forge's `gitea_list_repos` answered with real
|
||||
repositories; the tool list came back empty, because it asks the catalogue for a tool nothing serves
|
||||
([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)).
|
||||
|
||||
**Two records have already said where the surface belongs.** ADR 0132's consequences: *the MCP
|
||||
surface belongs inside the mesh — a module the mesh assigns to the machine where the agent sits, with
|
||||
a credential the mesh minted and authority derived from what it may call, not a program started by
|
||||
hand with a credential printed to a terminal.* Design [33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md)
|
||||
§6 says the same. Neither is a decision about the surface: 0132 decided where a seat's tools live,
|
||||
and named the surface in passing.
|
||||
|
||||
**And one record says the opposite, in the letter.** [ADR 0095](0095-the-control-plane-is-the-way-to-ask-a-module.md)
|
||||
made the control plane *the* way to ask a module, deferred "calling is a grant" until something asked
|
||||
for it, and recorded that *nothing outside the control plane can*. A person's account (design 25 §7,
|
||||
built 2026-09-28) is exactly that grant, minted for a person. So 0095's exclusivity has already been
|
||||
widened once without a record saying so; a module that calls tools widens it a second time, and this
|
||||
record is where that is said.
|
||||
|
||||
**What a module's account may do today, counted from the composition** (`internal/broker`): publish
|
||||
its own events, publish the accept subjects of seats it uses, subscribe its own tools and what it
|
||||
consumes. No module principal may publish another module's tool subject. Of 72 modules in the
|
||||
catalogue, 45 serve tools and 0 may call one.
|
||||
|
||||
The question the work order asks before any code: **does the mesh grow its own operator surface, or is
|
||||
the surface an ordinary module that happens to serve tools?**
|
||||
|
||||
## Considered Options
|
||||
|
||||
**1. The control plane serves the agent protocol itself** — a listener on the controller, or a verb
|
||||
its binary runs. Rejected. It puts a surface for tools the control plane does not implement on the one
|
||||
component that must stay answerable while it is itself being replaced, which is the reason 0132
|
||||
rejected the control plane as the answer to discovery. A person on a workstation would reach it over
|
||||
the network, and the networked surface [ADR 0035](0035-one-implementation-several-surfaces.md)
|
||||
reserves for that authenticates through an OAuth2 provider that is not configured — so the controller's
|
||||
`api` verb correctly serves nothing today, and this option would either wait for it or bypass it.
|
||||
|
||||
**2. A program a person installs and starts by hand with a printed credential** — what exists on the
|
||||
runtime repository's main. Rejected as the end state. It is outside the mesh in every way issue 147
|
||||
names: no assignment, no declaration, no seat, no check that it reaches anything, revoked only by a
|
||||
person remembering to. It is the predecessor's arrangement one bus later, and it fails the same way
|
||||
the next time an address moves. It stays as the recovery path, the way the command line does
|
||||
(ADR 0035): a credential from `operator issue` and the `mesh` client work with no console assigned.
|
||||
|
||||
**3. An ordinary module, assigned to the machine the person sits at, holding a credential the mesh
|
||||
minted, serving the mesh's tools on that machine's loopback.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
**The console is a module.** `mesh-console` is built by the mesh, registered like any module,
|
||||
assigned to a machine, and given a bus credential sealed to that machine. It serves the mesh's tools to
|
||||
whoever is on that machine: to an agent over MCP, and to a person through the same endpoint. Assigning
|
||||
it to a machine is what makes the mesh reachable from there; unassigning it revokes that, at the next
|
||||
composition, with nothing on the machine to remember to remove.
|
||||
|
||||
**A grant to call is a manifest word: `invokes`.** A module declares the tools it calls, each as
|
||||
`<module>.<tool>`, or the single entry `*` for every tool on the mesh. The bus grants exactly that
|
||||
publish side and nothing beside it — no event, no subscription, no seat. This is ADR 0095's deferred
|
||||
first option, taken now that a consumer asks for it; a person's account already has this shape, and
|
||||
the same composition derives both. The control plane's `ask` stands, and 0095's audit point with it:
|
||||
every call still passes one account whose permission list says what it may ask.
|
||||
|
||||
**Authority is the machine's login.** The console listens on the machine's loopback only, declared
|
||||
`from: machine`, so whoever can open a socket on the machine is whoever owns the machine, and *the
|
||||
account that installed the host owns the mesh on that node*
|
||||
([ADR 0034](0034-the-local-account-owns-the-mesh.md)). Anything on a machine may call anything on it,
|
||||
and that is the whole of local ([ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md)).
|
||||
The mesh knows no person: what the audit sees is which console asked, under the account
|
||||
`<node>.mesh-console`. How a person's identity reaches a session is the question design 15 leaves
|
||||
open, and this record does not close it.
|
||||
|
||||
**What the console lists is asked of the modules.** Design 33 §5: a module's own tools are answered by
|
||||
the module, from the code that defines them. The tool runtime therefore answers one reserved verb for
|
||||
every module it serves — `tools`, the module's tool names, descriptions and argument schemas — and the
|
||||
console assembles its list by asking the catalogue which modules the mesh holds and each module what
|
||||
it answers. A module that is not running is absent from the list and says so; a tool an agent already
|
||||
knows the name of can be called whether or not it was listed. A module may not name a tool of its own
|
||||
`tools`; the runtime refuses the collision at load rather than letting one shadow the other. A
|
||||
role's tools, and the mesh's own verbs, join the list when the `mesh-controller` seat serves them
|
||||
(design 33 §1, third family) — the console reads whatever the mesh can say about itself, and grows as
|
||||
that does.
|
||||
|
||||
**The console holds one credential and one grant: `*`.** It is the operator's surface on a machine the
|
||||
operator owns; narrowing what it may call is a setting on its assignment, which
|
||||
[ADR 0046](0046-a-module-configuration-is-its-assignments-not-its-manifest.md) already provides for
|
||||
and nothing here builds.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **The way a person drives the mesh is inside the mesh.** It is declared, delivered, replaced and
|
||||
revoked by the same machinery as everything else, and `status` says whether the machine carrying it
|
||||
has applied. Issue 147's shape — a surface kept alive by an address in a file — cannot recur, because
|
||||
there is no file: the console's credential names the bus the mesh is on, and moves when it does.
|
||||
- **A module may now call tools, which ADR 0095 had reserved to the control plane.** The grant is
|
||||
explicit, per tool or `*`, and derived by the same composition that grants everything else. A module
|
||||
that declares no `invokes` gains nothing. What got harder: a manifest reviewer has one more field to
|
||||
read for authority, and `*` in it deserves the reader's attention every time.
|
||||
- **A new manifest word ships one release before any manifest uses it**, and must reach both parsers:
|
||||
the build machine's and the running controller's. The console's manifest cannot be registered until
|
||||
the controller and the builder that packages it have been rebuilt with the word.
|
||||
- **Discovery costs a fan-out per list.** One request per module the mesh holds, answered at once by
|
||||
the bus for every module nothing serves, so the cost is bounded by the modules that are up. The list
|
||||
is cached briefly in the console; a module assigned a moment ago appears at the next refresh.
|
||||
- **Every tool runtime must be rebuilt once** to answer `tools`. Until a module is, it is callable and
|
||||
not listed, and the console says which modules did not answer.
|
||||
- **The mesh's own verbs are not in the console yet.** `status`, `push`, `assign` are the
|
||||
`mesh-controller` seat's tools under 0132, and the three prerequisites 0132 names are still not in
|
||||
place. A person asking what a node runs still opens a shell for that question, and that gap is design
|
||||
33's to close, not this record's — recorded here so nobody reads the console as the whole of 147.
|
||||
- **The person's credential is not retired.** `operator issue` and the `mesh` client remain the path
|
||||
when no console is assigned, and the path an operator uses to bring a mesh up far enough to assign
|
||||
one.
|
||||
|
||||
## How this is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| A module's `invokes` becomes exactly that publish grant, and nothing else | the bus user composition test: a module invoking `shop.price` may publish that subject and no other tool's; `*` may publish every tool subject; neither may publish an event or subscribe anything it did not consume |
|
||||
| A malformed `invokes` entry is refused at registration | a parser test: an entry naming no tool is a problem named in the manifest's words |
|
||||
| The runtime answers `tools` for every module it serves | the runtime's test: a module registering two tools answers three names, and a module naming one of its own `tools` is refused at load |
|
||||
| The console's list is what the modules answer | the client's test against a real bus: two modules up, a third the catalogue holds and nothing serves, and the list carries the two and names the third as not answering |
|
||||
| A call from the console reaches a module over the bus | the same test, and the live mesh: the console assigned to a workstation answers `tools/list` on its loopback and a call to the forge returns repositories |
|
||||
| The console listens on loopback and nowhere else | its manifest declares `from: machine`, and the filter composed for the machine opens nothing for it |
|
||||
|
||||
## References
|
||||
|
||||
- [issue 147](../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md) — the surface outside the mesh
|
||||
- [ADR 0095](0095-the-control-plane-is-the-way-to-ask-a-module.md) — extended: a grant to call, for a module as for a person
|
||||
- [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) — where a seat's tools live, and the sentence that named the surface
|
||||
- [ADR 0035](0035-one-implementation-several-surfaces.md) — three surfaces over one implementation
|
||||
- [ADR 0034](0034-the-local-account-owns-the-mesh.md), [ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md) — why loopback is the authority boundary
|
||||
- [33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §5, §6 — discovery, and what serves it to an agent
|
||||
- [34 — The console](../03-DESIGN/01-to-be/34-the-console.md) — the design this record authorises
|
||||
- mesh-tools `src/client.ts`, `src/mcp.ts`, `src/mesh.ts` — the client this makes a module of
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
topic: how we work
|
||||
status: accepted
|
||||
date: 2026-09-30
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
|
||||
---
|
||||
|
||||
# 153. The record is read by a module the mesh assigns, and the console lists it
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0025](0025-the-design-record-is-read-not-copied.md) decided that this repository is **read where
|
||||
it is written, never copied to be found**: an agent reads it directly, and a search of the mesh's
|
||||
memory consults that agent so its answers appear beside ordinary results. It named the check that
|
||||
closes [issue 006](../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md): search
|
||||
for a phrase that appears only in a design document here, and get it back. It gated the build on an
|
||||
agent that did not exist — the mesh session of
|
||||
[15 — The agent session](../03-DESIGN/01-to-be/15-the-agent-session.md) — and on a search that
|
||||
does not exist either, now: the knowledge base 0025 meant was the predecessor's, and since the
|
||||
cut-over nothing reaches it ([issue 147](../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md)).
|
||||
|
||||
**So the two halves of 0025 have no home.** There is no store to be "beside", and no session to be
|
||||
the reader. What the mesh has instead, since today: a tool model in which every module answers what
|
||||
it serves, and a console on the machine a person sits at that lists every tool the running modules
|
||||
answer ([ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)). An agent holding the
|
||||
console does not search a store; it reads a tool list and calls what fits the question.
|
||||
|
||||
**What 0025 could not tolerate was a derived copy** — the enforced copy winning while the reasoned one
|
||||
quietly stops being true. It rejected a sync for that reason and for no other. A git checkout is not a
|
||||
derived copy: it is the same bytes at a commit the answer names, and the only way it can differ from
|
||||
the source is by lagging behind it, which is measurable and stated. 0025's own words allow it —
|
||||
*retrieval is an agent reading this repository, not a copy living in a second store* — and the
|
||||
transformation that makes a copy dangerous is exactly what a checkout does not do.
|
||||
|
||||
## Considered Options
|
||||
|
||||
**1. Wait for the mesh session.** Rejected. Design 15 is `designed` with nothing built, its model
|
||||
access is a provisions question with no consumer identity yet, and 006 has waited since 2026-08-23.
|
||||
A record whose check cannot run is a rule enforced by nothing.
|
||||
|
||||
**2. The console reads the repository itself.** Rejected. The console holds nothing and decides
|
||||
nothing (ADR 0152, ADR 0035); a reader inside it would be a second implementation of a thing that
|
||||
should be one module, unavailable to a person's client and to any other module.
|
||||
|
||||
**3. A module that keeps a checkout of the repository and answers questions about it, listed by the
|
||||
console like any tool.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
**The reader is a module: `records`.** It requires the `git` provision — the forge — clones the
|
||||
repository its settings name, keeps the checkout current on every merge the forge announces and on a
|
||||
timer, and answers over the bus: where a phrase appears as written (document, line, nearest heading),
|
||||
one document whole, what a folder holds, and where the checkout stands — always with the commit it
|
||||
read. Nothing is indexed, ranked or summarised: a design record is found by its own words, and a
|
||||
reader deciding which words matter would be a second opinion about somebody else's document.
|
||||
|
||||
**The repository is a setting, not a manifest field.** The module names no mesh
|
||||
([ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md)): which repository it reads is
|
||||
the assignment's business, and an installation that keeps its record elsewhere sets that. Until a
|
||||
repository is set it serves no tools and says why. Public repositories only; it asks for no
|
||||
credential, because a secret it did not need would be one more thing to seal.
|
||||
|
||||
**"Beside everything else" is the console's tool list.** 0025's second half — the search consults the
|
||||
agent — has no store to consult and needs none: the console lists `records_search` beside the forge's
|
||||
tools and the mesh's own verbs, with a description that says when to call it, and an agent choosing
|
||||
tools for a symptom is the search. That is surfacing, not merely reaching: nobody has to know this
|
||||
repository exists to be offered it.
|
||||
|
||||
**Reading stays one-way.** The module reads the forge and answers; nothing flows back into the
|
||||
repository. It holds no credential that could write.
|
||||
|
||||
**The mesh session, when it exists, is a caller of this module, not a replacement for it.** Design 15's
|
||||
*it holds the design record by reading it* is satisfied by asking `records`; the session brings
|
||||
judgement, this brings the text.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Issue 006 closes on 0025's own check**, run through the console: `records_search` for a phrase
|
||||
that appears in one design document here returns that document. The module's test does the same
|
||||
against a repository it makes.
|
||||
- **The as-is knowledge document is rewritten.** [`07-knowledge.md`](../03-DESIGN/00-as-is/07-knowledge.md)
|
||||
described the predecessor's two stores; neither is reachable from the mesh, and what the mesh knows
|
||||
is now what its modules answer. Saying otherwise is the failure this repository exists to name.
|
||||
- **A checkout lags.** Between a merge and the next sync — seconds when the forge announces it,
|
||||
minutes when it does not — an answer is the previous commit's, and says which. That is the cost of
|
||||
no copy, and it is a number rather than a silence.
|
||||
- **The reader depends on the forge module's event, by name.** `consumes: gitea.pull.merged` names a
|
||||
module rather than the `git` seat, because the seat declares no events. A forge that is not gitea
|
||||
leaves the timer as the only refresh, which still works.
|
||||
- **What got harder:** the record is now reachable from every machine holding a console, which is what
|
||||
was wanted, and a reader must remember that this repository is public and the mesh is not — the
|
||||
module reads the public repository and nothing about the installation.
|
||||
|
||||
## How this is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| A phrase in one document comes back from where it is written, with the commit | the module's test against a repository it makes; and live, through the console |
|
||||
| A merge on the origin is pulled and the next answer names the new commit | the same test |
|
||||
| A path outside the checkout is refused, not resolved | a test per shape |
|
||||
| A failed sync leaves the checkout standing and is said | a test against an unreachable origin |
|
||||
| Without a repository set, no tools are served and the log says why | the module's own start |
|
||||
| The console lists `records_search` beside every other tool | the console's listing, live |
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0025](0025-the-design-record-is-read-not-copied.md) — extended: the reader is a module, the search is the console's list
|
||||
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — what lists it
|
||||
- [issue 006](../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md) — what closes
|
||||
- [35 — Reading the record](../03-DESIGN/01-to-be/35-reading-the-record.md) — the design
|
||||
- mesh-catalog `modules/records` — the module (PR 183)
|
||||
@@ -0,0 +1,133 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-09-30
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
||||
---
|
||||
|
||||
# 154. The mesh's own verbs are the mesh-controller seat's tools, and which verbs those are
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) decided that a seat's protocol
|
||||
carries its tools in full, that holding a seat means serving them, and that the mesh's own verbs are
|
||||
the `mesh-controller` seat's. It named three prerequisites, none in place: the protocol in the store
|
||||
rather than in compiled defaults; a protocol richer than a list of verbs; a node-scoped seat's subject
|
||||
carrying the node. And it left one thing to a decision per seat: **which verbs each seat serves**,
|
||||
because a seat's tools bind every future holder.
|
||||
|
||||
The console shipped the same day ([ADR 0152](0152-the-operators-surface-is-a-module-the-console.md))
|
||||
and made the gap visible from the operator's chair: a person on a workstation could call every tool a
|
||||
*module* serves and none of the mesh's own. What a node runs, what is assigned, whether a push
|
||||
applied — the questions issue 147 opened with — still meant a shell on the control node. The console's
|
||||
own handshake said so.
|
||||
|
||||
The control plane already answers every one of those questions, as commands: `status --json`,
|
||||
`node show`, `plan --json`, `assign`, `push`. [ADR 0035](0035-one-implementation-several-surfaces.md)
|
||||
says a surface is an adapter over those with no decisions in it, and the `api` verb proves the shape:
|
||||
every route calls the function the command line calls.
|
||||
|
||||
## Considered Options
|
||||
|
||||
**1. Leave the mesh's verbs to the shell until an identity provider authenticates the HTTP API.**
|
||||
Rejected. The authenticated network surface is for a browser on another machine; the console is
|
||||
already behind the machine's login (0152), and the bus already carries every other tool call under an
|
||||
account whose permission list says what it may ask. Waiting would keep the one surface the mesh has
|
||||
from answering the mesh's own questions, for a reason that does not apply to it.
|
||||
|
||||
**2. Serve the verbs as the mesh-controller *module's* tools, `mesh.mod.mesh-controller.tool.<verb>`.**
|
||||
Rejected; 0132 rejected it already. The controller holds a seat, and the verbs must keep their address
|
||||
while the control plane is being replaced, which is the moment they are most needed. A module's name
|
||||
would change with the implementation; the seat's does not.
|
||||
|
||||
**3. Call each command's function inside the serving process.** Rejected on two facts: the commands
|
||||
print, to the process's standard output, and two calls answered at once would read each other's
|
||||
words; and each command opens and closes its own stores, which the serving process holds open. Making
|
||||
every command return a value is the larger refactor, and it would give the tools a second code path to
|
||||
keep in step with the command line — the thing ADR 0035 forbids.
|
||||
|
||||
**4. The holder of the seat runs the command it names, in its own binary, and answers what it
|
||||
printed.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
**The `mesh-controller` seat serves twelve verbs**, and these are its interface, additive within a
|
||||
version ([33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §7):
|
||||
|
||||
| verb | answers with | takes |
|
||||
|---|---|---|
|
||||
| `tools` | every seat's tools, from the mesh's records | nothing |
|
||||
| `status` | what is wrong, quiet, behind, waiting — `status --json` | nothing |
|
||||
| `nodes` | every machine and its mode | nothing |
|
||||
| `node` | what one machine reported, what it is assigned, why | `node` |
|
||||
| `modules` | every module, its version, commit and machines | nothing |
|
||||
| `seats` | every seat, what it delivers, who holds it — `seats --json` | nothing |
|
||||
| `builds` | what was built lately and what came of it | `module` (optional) |
|
||||
| `plan` | the declaration a machine would be sent — `plan --json` | `node` |
|
||||
| `assign`, `unassign` | the mesh's own words, refusal included | `node`, `module` |
|
||||
| `push` | that it was sent; `status` says what the machine did | `node` (optional: every machine behind) |
|
||||
| `build` | that the build machine was asked; `builds` says what came of it | `repository`, `path`, `ref` |
|
||||
|
||||
**Each verb runs the command it names, in the controller's own binary, and answers what the command
|
||||
printed** — the output, whether it succeeded, and, where the command speaks JSON, the same as data. A
|
||||
refusal is the command's refusal in the command's words, because it is the same output. A verb takes
|
||||
only the arguments its schema names; nothing reaches a flag the schema did not declare. `push` and
|
||||
`build` are sent and not waited for: a call that blocked for a whole apply would time out on every
|
||||
machine that takes a minute and say nothing about the others.
|
||||
|
||||
**The three prerequisites are built.** A seat's protocol is three columns on its row, seeded from the
|
||||
compiled defaults where a row had none and additively thereafter, so a verb a release adds joins the
|
||||
row and nothing an operator wrote is taken away. A served verb is its name, what it does, and the
|
||||
schema of its arguments and answer; a manifest may still write a bare name. A node-scoped seat's tool
|
||||
carries the node it is asked of, as the last token of its subject; a mesh-scoped seat's stays flat.
|
||||
|
||||
**Holding a mesh seat requires serving its verbs**, judged where the store's set is loaded, and the
|
||||
refusal names the missing verbs. The controller's own manifest lists the twelve under `tools`.
|
||||
|
||||
**Discovery reads the records, through the seat.** `tools` is one of the twelve because the console
|
||||
cannot read the store and should not: the mesh answers for its own records through the role that owns
|
||||
them, and the answer is true while any *other* holder restarts. It is not true while the control plane
|
||||
itself restarts, and the console says so rather than hiding the modules' tools with it.
|
||||
|
||||
**A grant of `*` reaches a role's tools; `seat:<seat>.<verb>` grants one.** The console's `*` needed no
|
||||
change to reach the mesh's verbs, which is what a grant meaning *every tool* should mean.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **The console answers the mesh's own questions.** Issue 147's first paragraph closes: what a node
|
||||
runs, what is assigned, whether a push applied, from the machine the person sits at, over the bus,
|
||||
under an account whose permission list says so.
|
||||
- **Whoever may call `mesh-controller.push` may change the mesh.** That is the console's `*` on a
|
||||
machine whose login owns the mesh (0152), and a person's account only if `operator issue` says so.
|
||||
A grant reviewer reads `*` and `seat:mesh-controller.` with the same care.
|
||||
- **A verb here binds every future controller.** Twelve is deliberate: what an operator asks weekly,
|
||||
and nothing that is still finding its shape (`take`, `converge`, `settings`, `secret` stay commands).
|
||||
- **A command's text is the answer**, and text changes. The three verbs that speak JSON carry it as
|
||||
data; the rest are read by a person or an agent, not parsed. Anything that needs a shape asks for
|
||||
`--json` to be added to the command first, which is the right order.
|
||||
- **What got harder:** the mesh-controller seat's row now carries a protocol an operator could edit, and
|
||||
a verb removed from the row is a verb the controller stops serving without a build. That is
|
||||
ADR 0122's arrangement applied to tools, and `seats` shows the row.
|
||||
|
||||
## How this is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| Every declared verb is one the binary can run, with the arguments its schema names | a test walks the table and derives a command line for each |
|
||||
| A verb missing a required argument is refused in its own words, before anything runs | a test per shape |
|
||||
| A holder that does not serve a mesh seat's verbs cannot hold it, and the refusal names them | a catalogue test against a seat with two verbs and a holder with one |
|
||||
| A node-scoped seat's tool carries the node; a mesh seat's does not | the bus composition test: two nodes derive two addresses |
|
||||
| The controller subscribes its seat's tools and may answer | the composition test, and the golden user list |
|
||||
| `*` reaches a role's tools; `seat:` grants one and refuses a name with no verb | the composition test |
|
||||
| The protocol is seeded into the row and widened additively | the store-backed seat test |
|
||||
| Live: the console lists `mesh-controller.status` and a call answers what `status --json` prints | the rollout of this record |
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) — extended: the prerequisites built, the verbs decided
|
||||
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — the surface that lists them
|
||||
- [ADR 0035](0035-one-implementation-several-surfaces.md) — a surface is an adapter with no decisions in it
|
||||
- [ADR 0122](0122-a-seat-is-data-a-rename-is-a-database-update.md) — the row is the mesh's, and now carries the protocol
|
||||
- [33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) — the design this completes
|
||||
@@ -0,0 +1,128 @@
|
||||
---
|
||||
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.
|
||||
|
||||
> **Progressive insight — 2026-09-30.** The list was empty the day the check landed — every remaining name declared with its reason — and the operator asked for registration to refuse at once rather than after a release. It does, since mesh-controller PR 175: `module add` and a build's result are refused in the check's words, naming the way out, and the build stays recorded. The decision stands; only the day moved.
|
||||
|
||||
**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/` |
|
||||
| A definition naming an installation is refused at registration, and one declaring its names passes | `TestRegistrationRefusesADefinitionNamingAnInstallation` (2026-09-30) |
|
||||
| `${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
@@ -0,0 +1,83 @@
|
||||
---
|
||||
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`
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-01
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md
|
||||
---
|
||||
|
||||
# 157. A build says what it does on the bus, as it happens
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) made a build work submitted to a role: the
|
||||
build-machine seat accepts a build and emits its outcome, one publish that reaches whoever asked, the
|
||||
controller that records it and the catalogue that places it. Everything **between** the request and
|
||||
the outcome — which command is running, how long it has taken, where it hung, the compiler's error,
|
||||
the clone's refusal — lived in one container's standard error on one machine.
|
||||
|
||||
The night of 2026-09-30 showed the cost three times over. A build that failed showed a person one
|
||||
line, the first of its failure, in the controller's `builds`; the rest was read with `docker logs` over
|
||||
ssh, which the mesh's own rule forbids. A build that ran for minutes could not be told from one that
|
||||
had hung. And the builder has no tools and emits nothing but the outcome, so the console
|
||||
([ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)) had nothing to show while a
|
||||
build ran, and no viewer could be built on top of it. The operator's ask was plain: the builder is to
|
||||
be fully transparent, with its log on the bus, so that a log viewer can be built on the bus later.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Keep the log in the outcome.** The result carries the whole log when the build ends. Nothing new
|
||||
on the bus; nothing while the build runs; a viewer sees a build only once it is over, which is
|
||||
exactly when the log matters least.
|
||||
2. **A log store.** The builder writes its log to a file or a table and a tool reads it. A second
|
||||
place to keep something the bus already carries, with its own retention, access and failure modes,
|
||||
and no live reading without inventing a subscription over it.
|
||||
3. **The log is the role's own events.** Two more events on the build-machine seat beside `built`:
|
||||
`started` when work is taken, and `log.<build id>` for every line, published as the build runs.
|
||||
The events stream already retains every role's events for a week, so a reader follows a build
|
||||
live by subscribing its subject, or reads it back afterwards from the stream, and a viewer is a
|
||||
subscriber and nothing more.
|
||||
|
||||
## Decision
|
||||
|
||||
**Option 3.** A build machine says everything it does on the bus, as the role it holds, under the
|
||||
build's id, and the mesh keeps no other copy.
|
||||
|
||||
- The build-machine seat's protocol gains `started` and `log.*`. A holder may therefore publish
|
||||
`mesh.seat.mesh-build-machine.event.started` and `…event.log.<id>`, and no other subject, by the
|
||||
same derivation every seat's grants follow ([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)).
|
||||
The event's tail token is the build's id, so one build is one subject: a reader filters by subject
|
||||
alone, on the server, and a week of other builds does not travel to show one.
|
||||
- **Every line goes two ways**: to the machine's own standard error as before, and onto the bus. That
|
||||
includes every command the builder runs, its duration and its failure, and on failure the command's
|
||||
own output line by line — the compiler's words, the clone's refusal. A build machine with nobody
|
||||
listening still prints; a listener reads the same lines.
|
||||
- A line is a core publish, unawaited. The stream that holds the role's events captures it on its way
|
||||
through, and a build does not slow to the pace of an acknowledgement per line. Each line carries a
|
||||
sequence number from one, so a reader who joined late, or reads two copies, sees order and gaps.
|
||||
`started` and `built` are published into the stream and awaited, because they are the two facts a
|
||||
later reader must never find missing.
|
||||
- **The mesh reads it back from the stream**, never from a record of its own: `builds --log <id>`, and
|
||||
the same verb on the controller's seat ([ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)),
|
||||
reads one build's subject with a consumer that is gone when the reading is done. `builds` lists
|
||||
each build's id beside it, and `build` says the id it asked with, so a person can follow.
|
||||
- Nothing is declared by the builder module for this. The protocol is the seat's, seeded additively
|
||||
into the store ([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)), and the
|
||||
holder's grant follows on the next composition of the broker node.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A build is watchable while it runs, from anywhere on the mesh, with no access to the build machine.
|
||||
The console's gap of 2026-10-01 — no live progress, no per-merge view — closes on the progress half;
|
||||
the per-merge view is a reader over these subjects and the outcome, and is not built here.
|
||||
- A log viewer on the bus is now a plain subscriber: live on `mesh.seat.mesh-build-machine.event.>`,
|
||||
historical from the events stream filtered by a build's subject. NATS carries and retains; it does
|
||||
not view. The `nats` command-line client can tail or replay a subject today; a viewer of our own is
|
||||
later work and needs nothing more from the builder.
|
||||
- The events stream grows by a build's log per build, for a week. A build is a few hundred lines; the
|
||||
stream's limits are the bound, as for every other event, and a stream that fills drops the oldest.
|
||||
- A line the bus did not take is lost, deliberately, and visible as a gap in the sequence. The outcome
|
||||
is not affected: a build's result never depended on its narration.
|
||||
|
||||
## How this is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| The seat's holder may publish `started` and `log.<id>` and nothing wider | `TestTheBuildMachineMaySayWhatItDoesUnderTheBuildsId` (broker) |
|
||||
| A build's lines reach a reader of its subject in order, and the stream holds them afterwards | `TestNatsABuildIsTakenAndItsOutcomeReachesEverybody` against a real server (link) |
|
||||
| The seat verb `builds` with a build's id reads that build's log | `TestBuildsWithAnIdReadsThatBuildsLog` |
|
||||
| Every command the builder runs is said, with its output on failure | `Command` speaks through the hook every build sets; the builder's tests still see the lines on standard error when nothing listens |
|
||||
| Live: a build triggered after the roll-out is readable line by line through the console | done by hand after the merge of mesh-controller PR — see the design's note |
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) — extended: the role now narrates as well as answers
|
||||
- [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md), [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md) — the protocol and the verb
|
||||
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — the console this feeds
|
||||
- [Design 25 — The bus on NATS](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §3, [Design 18 — Building a module](../03-DESIGN/01-to-be/18-building-a-module.md)
|
||||
- [Issue 176](../04-ISSUES/176-the-consoles-build-tool-neither-waits-nor-registers/00-report.md) — the tool that starts a build and hears nothing; this gives it something to hear
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-01
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md
|
||||
---
|
||||
|
||||
# 158. A provider with one credential shares it with every consumer, and the vault remakes it for all of them at once
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) gave every consumer of a
|
||||
provision its own credential: the mesh mints one per pair, the provider's own code creates the
|
||||
login, and rotating one consumer's touches nothing else. That is right for a database, a broker, an
|
||||
object store — software that can hold many logins.
|
||||
|
||||
The media software on the home server cannot. A download client has one web password; an indexer
|
||||
has one API key; each of the library managers has one key in its configuration; the media server
|
||||
holds one token issued elsewhere. There is no login per consumer to create, so
|
||||
[ADR 0113](0113-the-vault-makes-every-secret.md)'s only remaining form applied: the value is
|
||||
*accepted*. On 2026-10-01 the home server held forty-seven accepted own secrets and twelve accepted
|
||||
pair credentials, every one rotatable only by a person changing the software by hand and accepting
|
||||
the new value, and one pair credential sat *made* and wrong because nobody could accept the real one.
|
||||
The operator asked for every password in the vault and rotatable, and for a library manager's
|
||||
definition to receive the download client's credential and address through provisioning like
|
||||
anything else (filed as the forge's issue 243 on this repository).
|
||||
|
||||
The address half already works: the library manager requires the download client's API provision,
|
||||
the provider serves scheme, port and user name, and the binding carries them. Only the credential
|
||||
half had no form.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Keep accepting.** Honest about what the software can do and what the mesh cannot, and it is
|
||||
the state the home server was in: nothing rotates, a consumer added later needs a person, and an
|
||||
unknown predecessor password stays unknown for ever.
|
||||
2. **Put a login per consumer in front of the software.** A proxy that holds the one credential and
|
||||
issues many. A second service per provider, with its own credential to keep, to make the mesh's
|
||||
model fit software that does not share it.
|
||||
3. **Let the provider say its one credential is the credential.** An offer names which of the
|
||||
provider's own secrets *is* what every consumer receives. The vault keeps one record, sealed to
|
||||
the provider's machine, every current consumer's machine and the operator, and because it stores
|
||||
no plaintext it cannot seal an existing value to a later consumer — so it **remakes the value
|
||||
for all of them at once** whenever the set of consumers changes or a rotation is asked. The
|
||||
provider takes it the way an own secret is taken ([ADR 0114](0114-a-shared-credential-rotates-over-two-credentials.md),
|
||||
issue 180); consumers read it at start.
|
||||
|
||||
## Decision
|
||||
|
||||
**Option 3.** A provider whose software holds one credential shares that credential, and the mesh
|
||||
owns its whole lifecycle.
|
||||
|
||||
- **The offer says so.** `{"name": "download-client-api", "credential": {"own": "password"}}` on a
|
||||
provider's `provides` entry names one of its own secrets as the credential of that provision. The
|
||||
named own secret must say how it is taken (`taken: at-start` or `taken: applied`); an offer
|
||||
naming an undeclared or untaken secret is refused at parse.
|
||||
- **One record, many seals.** The vault keeps one value per (provider assignment, provision). It is
|
||||
sealed to the provider's machine, to each consumer's machine that currently binds the provision,
|
||||
and to the operator. Every consumer's binding file carries the provider's one user name and the
|
||||
secret file carries the shared value; the shape a consumer reads is the pair credential's, so a
|
||||
consumer's definition does not know whether its credential is shared.
|
||||
- **Remade for all, together.** When a consumer binds or unbinds, or `secret rotate` is asked on the
|
||||
provider's own secret, the vault makes a new value and seals it to every current holder in one
|
||||
act, and the mesh sends every holding machine. The provider restarts on the new value or applies
|
||||
it at start; each consumer restarts on it. There is no window between two credentials, because
|
||||
there is one credential; there is the restart, stated as the cost below.
|
||||
- **An accepted shared value is sealed to everyone the moment it is accepted.** `secret accept` on
|
||||
the provider's own secret is the one moment the mesh holds the plaintext, and it seals copies for
|
||||
every current consumer then. It is not remade afterwards ([ADR 0113](0113-the-vault-makes-every-secret.md)):
|
||||
a consumer that binds later is refused until the value is accepted again, in words that say so.
|
||||
- **A value the software issues itself stays accepted.** A token the media server obtains from its
|
||||
vendor cannot be set by the mesh; its provision keeps the accepted form until a module can deliver a
|
||||
value it did not mint to the vault, which this record does not build.
|
||||
- **Nothing changes for software that holds many logins.** ADR 0048's form stays the default; this
|
||||
is the form for an offer that says it has one credential.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The media stack's six providers stop needing a person per consumer. A library manager binding
|
||||
the download client gets a working credential the mesh made, and an unknown predecessor password
|
||||
is replaced by one the mesh knows, recoverable with the operator's key.
|
||||
- **Adding or removing a consumer restarts every consumer of that provision and the provider.**
|
||||
That is the price of one credential, and it is paid when a definition binds, not at an hour of
|
||||
nobody's choosing. It is stated in the plan's words when it happens.
|
||||
- Rotation of a shared credential is [ADR 0114](0114-a-shared-credential-rotates-over-two-credentials.md)'s
|
||||
single-party form across several machines: in place, all holders sent together. The staged form
|
||||
for a backend that takes its credential once is still not built, and a provider whose own secret
|
||||
says `applied` refuses rotation by name until it is.
|
||||
- The vault can name who holds a shared value — the copies are the record — so *who has this* stays
|
||||
a query, as design 13 requires.
|
||||
- The accepted count on the home server becomes a list that shrinks, provider by provider, as each
|
||||
one's start applies the file.
|
||||
|
||||
## How this is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| An offer may name one of its own secrets as its credential; an undeclared or untaken secret is refused at parse | manifest tests |
|
||||
| A consumer of a shared provision receives the provider's value as its pair credential, under the provider's one user name | resolver and declaration tests |
|
||||
| The record is sealed to the provider, every current consumer and the operator; a consumer binding or unbinding remakes it for all | inventory tests against a raised store |
|
||||
| Rotating the provider's own secret remakes every holder's copy, and an accepted value is sealed to current consumers once and not remade | inventory tests |
|
||||
| Live: a library manager on the home server binds the download client with a value the mesh made, the client takes it at start, and a rotation through the console reaches both | done by hand after the media catalogue's providers apply the file at start |
|
||||
|
||||
*2026-10-01:* the first four rows pass in mesh-controller PR 184 (`make check` green); the live row waits for the first provider definition to say `credential` and `taken`.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) — extended: the per-consumer form stays the default; this is the form for one credential
|
||||
- [ADR 0113](0113-the-vault-makes-every-secret.md), [ADR 0114](0114-a-shared-credential-rotates-over-two-credentials.md) — the accepted form and the single-party rotation this rests on
|
||||
- [Issue 180](../04-ISSUES/180-a-modules-own-secret-cannot-be-rotated/00-report.md) — the `taken` word and the rotation this reuses
|
||||
- [Design 24 — The secrets vault](../03-DESIGN/01-to-be/24-the-secrets-vault.md), [Design 13 — Credentials and their rotation](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md)
|
||||
- The forge's issue 243 on this repository, where the operator's ask and the home server's count were recorded
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-01
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
|
||||
---
|
||||
|
||||
# 159. A tool call names the machine it is for, every answer says which machine answered, and a holder's runtime serves its seat's verbs
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) made a module's tools subjects on
|
||||
the bus and the console the place a person reaches them. [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)
|
||||
made the mesh's own verbs the controller seat's tools, served by the controller. Design 33 said what
|
||||
a seat's tools are, that holding a seat means serving them, and that a node-scoped seat's verb
|
||||
carries the machine.
|
||||
|
||||
What was built stopped short in two places ([issue 182](../04-ISSUES/182-a-tool-call-reaches-whichever-instance-answers-first/00-report.md)).
|
||||
A module's tools were one subject per module in one queue group, so with the database engine on two
|
||||
machines a call reached whichever instance answered first, unnamed, and nobody could ask one machine's.
|
||||
And no module served the verbs of a seat it held: the runtime did not know which seats its module
|
||||
claimed, and no seat but the controller's declared verbs. The operator named it: a tool call must be
|
||||
able to say *the store on the control node*, and the engine holding the store seat must serve the
|
||||
store's tools as well as its own.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Leave the queue group and ask the controller which machine answered.** Nothing changes on the
|
||||
bus; a caller cannot choose, only learn afterwards. Useless for the question that was asked.
|
||||
2. **A subject per machine instead of one per module.** Every call names a machine; a stateless
|
||||
module on three machines loses the one-of-them answer a queue group gives for free, and every
|
||||
caller has to know where things run.
|
||||
3. **Both subjects, and the machine in every answer.** An instance serves its module's subject in the
|
||||
queue group as before, and the same subject with its machine as the last token. A caller that
|
||||
names no machine gets one instance and is told which; a caller that names one gets that one. The
|
||||
grant for a tool covers both. And a holder's runtime serves its seat's verbs by the same means,
|
||||
from what the credential tells it.
|
||||
|
||||
## Decision
|
||||
|
||||
**Option 3.**
|
||||
|
||||
- **Two subjects per tool, one default.** `mesh.mod.<module>.tool.<tool>` in the queue group, and
|
||||
`mesh.mod.<module>.tool.<tool>.<node>` served by the instance on that machine alone. In the
|
||||
caller's words, `<module>.<tool>@<node>`. A runtime that does not know its machine serves only the
|
||||
first, which is what it always did.
|
||||
- **Every answer says which machine answered.** The reply carries the node; the console appends
|
||||
*answered by <node>* as its own line after the module's unshaped answer, and `mesh call` prints it.
|
||||
An answer from a module on several machines is never an answer from nowhere.
|
||||
- **The console offers the machine on every module tool** as an optional `node` argument, lists it,
|
||||
strips it into the subject and never passes it to the module. A seat's verb takes none: the seat's
|
||||
scope decides where it is served.
|
||||
- **The grant covers both subjects.** `invokes: [<module>.<tool>]` permits the plain subject and the
|
||||
machine-addressed one; `*` already permitted everything beneath `tool`.
|
||||
- **A holder's runtime serves its seat's verbs.** The broker credential the mesh writes names the
|
||||
seats the module claims and, for each, its scope and the verbs the seat promises. The runtime
|
||||
serves each verb with the module's tool of the same name on the seat's own subject — flat for a
|
||||
mesh seat, with the machine for a node-scoped one — and the bus admits that subscription only
|
||||
where the module holds the seat, because the holder's grant is composed from the holding. A
|
||||
claimant that does not hold the seat here is refused the subscription and serves nothing. A
|
||||
claimant missing a tool a seat promises is already refused at registration (design 33 §3).
|
||||
- **The store seat's first verbs**, so the operator's question has an answer: `databases`, every
|
||||
database the store holds with its owner and size, and `query`, one read-only statement against one
|
||||
database. The database engine serves both as tools of those names and lists them in its definition.
|
||||
Which verbs a seat serves is a decision per seat and binds every holder; these two are the smallest
|
||||
set that makes the store askable.
|
||||
|
||||
## Consequences
|
||||
|
||||
- *List the databases of the store on the control node* is `mesh-store.databases` through the seat,
|
||||
answered by its holder wherever it sits, or `postgres.databases@novox` through the module on one
|
||||
named machine. Both say who answered.
|
||||
- Every module's runtime serves one more subscription per tool and, for a claimant, one per promised
|
||||
verb. No manifest changes for the per-machine half; the seat half needs each holder's definition to
|
||||
list the seat's verbs among its tools, which registration already demands.
|
||||
- The runtime change reaches a module when the module is rebuilt on the new runtime image; until
|
||||
then that module answers only on its plain subject, and a call naming its machine is refused as
|
||||
unserved, in words that say so.
|
||||
- The credential gains `claims`; a module issued before this carries none and serves no seat verb
|
||||
until it is issued again. `rollout mint` for the holders is the one-time cost.
|
||||
|
||||
## How this is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| A call naming a machine reaches that machine's instance; an unnamed call reaches one and says which | mesh-tools, against a real bus: a module on two machines |
|
||||
| A claimant serves a seat's verb on the seat's subject, and the answer names the machine | the same test |
|
||||
| The console lists `node` on a module's tool and not on a seat's verb, and the answer carries *answered by* | mesh-tools, the MCP conformance test |
|
||||
| The grant for a tool covers the plain and the machine-addressed subject | `TestInvokingAToolMayAddressTheMachineToo` (controller) |
|
||||
| Live: the store's databases listed from the control node by name through the console, and through the store seat | done by hand after the roll-out and the catalogue's step |
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — extended: the surface carries the machine
|
||||
- [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md), [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) — the seat half, now for every holder
|
||||
- [Design 33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §3, §4; [Design 34 — The console](../03-DESIGN/01-to-be/34-the-console.md) §3
|
||||
- [Issue 182](../04-ISSUES/182-a-tool-call-reaches-whichever-instance-answers-first/00-report.md)
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-01
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
|
||||
---
|
||||
|
||||
# 160. The mesh issues an assignment's subjects, and a runtime serves what it is issued
|
||||
|
||||
## Context
|
||||
|
||||
A module's code names no subject. It registers tools by name and emits events by name, and design 29
|
||||
§1 says the rest: *the module names its event and the mesh decides where it lands*. What was built
|
||||
decided it twice. The runtime derives `mesh.mod.<module>.tool.<name>` from the module's name by a rule
|
||||
compiled into it; the controller derives the same subject by the same rule compiled into it, and grants
|
||||
it. They agree because two binaries carry one convention, which is the failure design 33 §2 names for
|
||||
seat protocols: *discovery that reads a binary disagrees with the mesh the moment the two are on
|
||||
different versions*. [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)
|
||||
extended the convention this morning — a second subject per tool with the machine as its last token, a
|
||||
seat's verbs served from the credential's claims — and extending it made the shape plain: every such
|
||||
change is written in the runtime and in the controller, and a module whose instances must not be
|
||||
confused is told apart by a rule in a binary rather than by the mesh that assigned it.
|
||||
|
||||
The operator put it in one sentence: the mesh knows the subjects, the modules do not; a module should
|
||||
ask what to listen on. This record decides exactly that.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Keep the convention, keep it in two places.** Cheap until the next change; every change is two
|
||||
changes, and the mesh cannot vary a subject for one assignment without a rule for all.
|
||||
2. **Keep the convention in one place by putting it in the SDK alone**, and have the controller call
|
||||
the SDK's rule. The controller is Go and the SDK is TypeScript; one of them would still carry a copy.
|
||||
3. **The mesh issues the subjects.** For every assignment the controller composes a membership: what
|
||||
this instance serves, where, in which queue if any; the seat verbs it holds; where its events land;
|
||||
what it may reach and at which subjects. It publishes it to a subject only that assignment may read,
|
||||
kept last-per-subject so a runtime that connects late reads the current one. The runtime serves
|
||||
exactly the list and nothing it did not receive. The grant is composed from the same membership, in
|
||||
the same act, so the two cannot drift.
|
||||
|
||||
## Decision
|
||||
|
||||
**Option 3.**
|
||||
|
||||
- **A membership per assignment.** The controller composes, for a module on a machine, one document:
|
||||
the tools the module serves with the subject each is served on and the queue group if any; the seat
|
||||
verbs this instance serves and their subjects; the subject each of its events lands on; what it may
|
||||
reach — the tools it invokes, resolved to the subjects the mesh issued to those modules' instances —
|
||||
and what it consumes. The runtime registers tools and events by name; the membership says where.
|
||||
- **Published, not written into the definition.** The membership is a message on
|
||||
`mesh.assignment.<node>.<module>` in a stream that keeps the last per subject, like a node's
|
||||
declaration. The controller publishes it whenever the assignment's facts change: a push, a seat
|
||||
handover, an instance added elsewhere, an upgrade. A runtime reads the current one when it connects,
|
||||
serves it, and keeps reading, so a change reaches a running instance as a re-subscription rather
|
||||
than a restart.
|
||||
- **One bootstrap rule, and only one.** The credential names the node and the module; the membership's
|
||||
subject follows from those two names and nothing else, and the account may subscribe it. Every
|
||||
other subject is data in the membership. This is the one convention the runtime keeps, the way a
|
||||
resolver keeps the address of a root.
|
||||
- **The grant is the membership, read the other way.** What an account may subscribe is what its
|
||||
membership says it serves plus its own membership's subject; what it may publish is what its
|
||||
membership says it emits and reaches. One composition yields both, so a subject the runtime serves
|
||||
without a grant, or a grant for a subject nothing serves, cannot be written.
|
||||
- **Whether an instance answers for the module, or only for its machine, is the mesh's to decide.**
|
||||
A module on one machine is issued the module's plain subject and its machine's. A module on several
|
||||
is issued only its machine's unless its definition says its instances are interchangeable, a fact
|
||||
about the software and not about the bus; then every instance is issued the plain subject in one
|
||||
queue group as well. The console lists what the memberships say: a stateful module on two machines
|
||||
appears once per machine; a stateless one appears once.
|
||||
- **A caller composes nothing.** The console's listing carries each tool's subject; the SDK's call by
|
||||
name reads the subject from the caller's own membership, where the mesh wrote what it may reach. The
|
||||
shape of a subject is the controller's business and may change without any module or runtime
|
||||
changing.
|
||||
- **Today's shape is the shape issued first.** `mesh.mod.<module>.tool.<name>`, with the machine as the
|
||||
last token for an instance, and `mesh.seat.<seat>.tool.<verb>` with the machine for a node-scoped
|
||||
seat, are what the controller composes on day one, so nothing on the mesh moves when the
|
||||
membership arrives; only who decides it moves. [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)
|
||||
stands for what it decided — a call names the machine, every answer names it, a holder serves its
|
||||
seat — and is extended in how: those facts are now issued, not derived.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The runtime loses its subject rule and its claims rule; it serves a list. The controller gains one
|
||||
composition and one stream; design 25 §2 and §3 gain a line each. The console loses `toolSubject`
|
||||
and reads subjects from the listing. The SDK's `invokeTool` reads the caller's membership.
|
||||
- A subject scheme change is a controller release and a republish of every membership, with no module
|
||||
rebuilt — the opposite of this morning's forty-three builds.
|
||||
- A membership can differ per assignment on purpose: an instance that holds a seat serves more; an
|
||||
instance the mesh wants quiet serves less; a module the mesh is retiring can be issued nothing and
|
||||
told so.
|
||||
- During the move, a runtime that finds no membership for its assignment falls back to the derived
|
||||
shape and says so in its log, so the wave of this change is a controller release followed by one
|
||||
push, and a runtime older than the change keeps working on the convention it carries.
|
||||
|
||||
## How this is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| A membership composed for an assignment and the grant composed for its account name the same subjects, both ways | a controller test over a module on one machine, on two, holding a seat, and declared interchangeable |
|
||||
| A runtime serves exactly the subjects its membership lists, and re-subscribes when the membership changes | a runtime test against a real bus: a membership published, served; republished with a subject removed and one added, followed |
|
||||
| A runtime with no membership says so and serves the derived shape | the same test, before any membership is published |
|
||||
| The console lists a stateful module on two machines once per machine, and composes no subject | the MCP conformance test |
|
||||
| Live: the store's databases asked of one named machine and through the seat, after a controller release and one push, with no module rebuilt | by hand |
|
||||
|
||||
## Built, 2026-10-01
|
||||
|
||||
> **Progressive insight — 2026-10-01.** The decision stands; these are the facts of its building.
|
||||
|
||||
- The controller's half: mesh-controller 188 — the membership, its subject, the assignments stream
|
||||
read directly, a module's account granted its own membership and nothing else of the stream, a
|
||||
membership published after each push.
|
||||
- The runtime's half: mesh-tools 23 — the one address derived, the membership read and followed,
|
||||
exactly the issued subjects served and re-served, the derived shape with a log line until one is
|
||||
issued, a seat's verbs implemented under the seat's name and never listed as the module's, the
|
||||
listing carrying subjects and the console composing none. A claim may now name the verbs it
|
||||
serves for its seat (mesh-controller 186), so a holder's own tools need not be the seat's.
|
||||
- What the first roll-out taught: the controller's own grant did not name the assignments it issues,
|
||||
so the first memberships were refused by the server and every runtime kept the derived shape —
|
||||
which is exactly the fallback this record asked for, and exactly why nobody noticed
|
||||
([issue 183](../04-ISSUES/183-the-controller-could-not-publish-the-memberships-it-issued/00-report.md)).
|
||||
The SDK's `invokeTool` still composes a subject; it reaches a membership through the runtime's
|
||||
broker, which does, so the caller-side rule is met there and not yet in the SDK's own words.
|
||||
- Live, 14:55Z the same day, through the console: the console's runtime logged *was issued a new
|
||||
membership; re-serving on it*; `mesh-store.databases` answered by the control node, the seat's
|
||||
holder; `postgres.postgres_list_databases` with the machine named answered by that machine, on
|
||||
both machines that run it; `mesh-controller.push {node}` reached the seat's verb with its own
|
||||
argument intact. Three facts the proof taught: a runtime's first read of the stream must use the
|
||||
subject-addressed direct get, the only form its account is granted (mesh-tools 25); a module's
|
||||
bus credential is a minted secret written once, so a claim added to a definition reaches a running
|
||||
module only after `module issue <module> --node <machine>` and a push (postgres, both machines);
|
||||
and a registration under a seat the credential does not yet claim must be said and skipped, not
|
||||
fatal (mesh-tools 26).
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md) — extended: the same facts, issued rather than derived
|
||||
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md), [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) — the surface and the seat's tools this applies to
|
||||
- [Design 25 — The bus on NATS](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §2, §3; [Design 32 — What a module declares](../03-DESIGN/01-to-be/32-what-a-module-declares.md) §1; [Design 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md); [Design 34](../03-DESIGN/01-to-be/34-the-console.md)
|
||||
@@ -0,0 +1,102 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-01
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0126-a-module-declares-its-own-seats.md
|
||||
---
|
||||
|
||||
# 161. What deserves a seat: a role of a module is a seat, a singular fact about machines is a placement with a capacity of one, and a holder's software is the machine's
|
||||
|
||||
## Context
|
||||
|
||||
Three issues asked the same question from three sides. The vault provides `secret` to the whole
|
||||
mesh and claims no seat, so nothing refuses a second vault by name
|
||||
([issue 106](../04-ISSUES/106-the-vault-claims-no-seat/00-report.md)). The hub of the private
|
||||
network is a placement, `overlay place <node> --hub`, and the issue asked whether "there is exactly
|
||||
one hub" is a seat's shape ([issue 105](../04-ISSUES/105-the-hub-of-the-private-network-is-not-a-seat/00-report.md)).
|
||||
Three modules claim the one uplink seat, one per network manager a machine might run, and nothing
|
||||
checks that the holder names the manager the machine actually runs
|
||||
([issue 138](../04-ISSUES/138-two-modules-claim-one-seat-and-are-not-interchangeable/00-report.md)).
|
||||
|
||||
Read against the code on the day of deciding:
|
||||
|
||||
- The mesh's own seats are five by [design 26](../03-DESIGN/01-to-be/26-the-seats.md)'s table and
|
||||
four in the controller's seed: `mesh-vault` is in the table and not in the seed, and the vault's
|
||||
definition claims nothing. The design also says `secret` is reserved; no parser or resolution rule
|
||||
reserves it. A second provider of `secret` would be a second candidate, settled by a pin.
|
||||
- The store already keeps one hub: a unique index since the overlay's first migration, and the
|
||||
placing command refuses a second hub naming the first. What 105 observed as silent is not.
|
||||
[ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) decided that
|
||||
the private network becomes a mesh-scoped seat held by a server module, with client modules —
|
||||
the overlay is the host's own today, so that seat has nothing to be held by yet.
|
||||
- A machine's capabilities are its profile, detected by the host at enrolment and never since, and
|
||||
resolution refuses a module on a machine lacking one it declares, naming the capability. The uplink
|
||||
holders declare `package-manager` and `service-manager`, which every machine has.
|
||||
|
||||
[ADR 0126](0126-a-module-declares-its-own-seats.md) gave the reason the mesh's own seats exist:
|
||||
**the mesh's own code looks them up by name.** `mesh-store` is an identifier the controller
|
||||
dereferences, not a convention. That reason decides the first question; the other two are decided
|
||||
by what a seat is — a role held by a module assignment — and by what the mesh can check.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. A provision the mesh itself dereferences is delivered by a mesh seat its provider claims.**
|
||||
The vault's `secret` is one: the controller seals every minted credential with it. `mesh-vault` is
|
||||
the fifth seat of the mesh's own, mesh-scoped, delivering `secret`, under
|
||||
[ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md)'s convention; the vault's
|
||||
definition claims it; a second provider of `secret` is a second claimant and refused by name. The
|
||||
word *reserved* leaves design 26: the effect it described is the seat's. Every other mesh-scoped
|
||||
provision — `smtp`, `oidc-client`, `s3-bucket`, `route`, `acme-ca` and the rest — may have several
|
||||
providers, and a consumer with several and none local is a person's choice, as the glossary says.
|
||||
The test for "deserves a seat" is the question 0126 asked: does the mesh's own code find it by name?
|
||||
|
||||
**2. A singular fact about machines is a placement with a capacity of one; a singular role of a
|
||||
module is a seat.** A seat is held by a module assignment and points at it; the hub is a machine,
|
||||
and the private network is the host's own until 0121's server and client modules exist. So the hub
|
||||
stays a placement, and what a seat would have given — refusal of a second by name, and the one
|
||||
named when asked — a placement of capacity one gives: the store keeps one (the unique index), the
|
||||
placing command refuses a second naming the one that stands, and the overlay listing names it.
|
||||
0121's seat for the private network stands, deferred with the split it needs. The rule generalises:
|
||||
a fact of the shape *exactly one machine is X* is a placement checked by the store and said by name,
|
||||
never a seat with no module to hold it.
|
||||
|
||||
**3. A holder of a seat whose role is "speak to what this machine runs" must be the dialect the
|
||||
machine runs, and the machine says which.** The host's profile gains one capability per network
|
||||
manager found active — `uplink-networkmanager`, `uplink-systemd-networkd`, `uplink-dhcpcd`, each
|
||||
`systemctl is-active` of the manager's unit — and each uplink holder declares its own. Assignment
|
||||
then refuses the wrong holder with the refusal that already exists, naming the capability; nothing
|
||||
new is judged. The profile is detected again by every apply and travels in the report, and the
|
||||
controller keeps the latest, so a machine that switches managers is, at its next push, a machine
|
||||
whose holder lacks a capability: the plan refuses and names it, which is the one thing the machine
|
||||
is the only one to know. `node-uplink` stays one seat: its three holders are three dialects of one
|
||||
role, and the capability picks the dialect. One module speaking all three is allowed by this and
|
||||
built by nobody.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The controller's seed gains `mesh-vault`; the seat table takes it additively at the next start,
|
||||
as every seed row does. The vault's definition claims it, one release after the controller.
|
||||
- The uplink definitions declare their capability one release after the host reports it, or they
|
||||
are refused on every machine in between; the order is controller (the report carries a profile),
|
||||
host, then catalogue.
|
||||
- Design 26 loses the word *reserved* for `secret` and states rules 2 and 3; the uplink row of the
|
||||
seat table names the capability its holders declare.
|
||||
- Issue 106 is resolved by rule 1, 105 by rule 2 with nothing to build, 138 by rule 3.
|
||||
|
||||
## How this is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| `mesh-vault` is in the mesh's own set, mesh-scoped, delivering `secret`, and the vault claims it | a catalogue test on the default seats; registration refuses a second claimant by name (`CanHold`'s existing test, with the vault's seat) |
|
||||
| A second hub is refused naming the first, and the listing names the hub | the overlay command's test; the store's unique index |
|
||||
| A machine's profile names the network manager it runs, and is renewed by every report | a host detector test per manager; a controller test that a report carrying a profile replaces the stored one |
|
||||
| An uplink holder on a machine running another manager is refused, naming the capability | the existing capability refusal, exercised by a resolution test with a networkmanager machine and the systemd-networkd holder |
|
||||
| Live | `mesh-controller.seats` lists `mesh-vault` held by the vault on the control node; `plan` of a machine refuses the wrong uplink holder naming `uplink-<manager>` |
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md), [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md), [ADR 0117](0117-a-machines-uplink-is-a-seat.md), [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0126](0126-a-module-declares-its-own-seats.md)
|
||||
- [Design 26 — The seats](../03-DESIGN/01-to-be/26-the-seats.md)
|
||||
- Issues [105](../04-ISSUES/105-the-hub-of-the-private-network-is-not-a-seat/00-report.md), [106](../04-ISSUES/106-the-vault-claims-no-seat/00-report.md), [138](../04-ISSUES/138-two-modules-claim-one-seat-and-are-not-interchangeable/00-report.md)
|
||||
@@ -0,0 +1,128 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-01
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
|
||||
---
|
||||
|
||||
# 162. A merge produces a tiered plan the mesh keeps, and a module's dependencies are one relation in the catalogue
|
||||
|
||||
## Context
|
||||
|
||||
A merge on the forge reaches the controller as an event, and the controller asks the build
|
||||
machine for what that merge changed. Until today that meant the modules whose recorded source is
|
||||
that repository; since this afternoon it also means everything standing on what moved
|
||||
([issue 186](../04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md)).
|
||||
Both are done inside the handler that received the event: it asks one build, waits for it, asks the
|
||||
next, and returns when the last is done. Three things followed from that shape on 2026-10-01:
|
||||
|
||||
- The controller hears nothing else for the length of the work — twenty-five minutes for the runtime
|
||||
image and its forty-three dependents ([issue 184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md)).
|
||||
- A controller replaced mid-merge loses the rest of the merge: the redelivered announcement reads as
|
||||
history, and the dependents are asked by hand.
|
||||
- Nothing is deployed between builds. A merge that changes the build machine and something the build
|
||||
machine builds asks for both in order, but the second is built by whichever build machine is running
|
||||
— the old one, unless somebody pushed in between. The order the dependents are sorted in exists for
|
||||
the artifacts; it says nothing about what must be *running*.
|
||||
|
||||
And the knowledge the order is computed from is scattered: a manifest's `build.on`, the artifacts a
|
||||
build was made against, the repositories a build read, and the fact that every source-built module is
|
||||
built by the build machine, each read by a different function in the merge handler.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. A module's dependencies are one relation in the catalogue.** `depends-on` edges, each with the
|
||||
kind of dependency on it: `stands-on` (the module's artifact is built on the other's), `packages` (the
|
||||
module's build reads the other's repository), `built-by` (the module is built by the holder of the
|
||||
build-machine seat), and `declared` (a manifest's `build.on`). The relation is answered by one query
|
||||
of the catalogue — the controller's inventory today, the catalogue seat's tool when something outside
|
||||
the controller needs it — and nothing else computes an edge. The edges are derived from facts
|
||||
recorded at two moments and written by nobody: registration records the manifest (`declared`, and
|
||||
`built-by` for anything with a source), a build's take-in records what the image was built on and
|
||||
which repositories it read (`stands-on`, `packages`). A module's first build places it by its
|
||||
declared edges alone; from its second it is placed by what was true.
|
||||
|
||||
**The kinds are three dependencies, not one.** A *code* dependency — B packages A's source — means
|
||||
B is rebuilt whenever A changes, in the same tier: B's build needs nothing of A's first. A *build*
|
||||
dependency — B stands on A's artifact, or declares it — means B is rebuilt after A is *built*, the
|
||||
next tier, and nothing need be deployed in between. A *runtime* dependency — B is built by A — means
|
||||
B is rebuilt only after A is built *and running*, the next tier with a gate on the machines' reports.
|
||||
One cycle is real and resolved by the kinds themselves: the runtime image is built by the build
|
||||
machine, and the build machine stands on the runtime image; the image comes first, built by the
|
||||
build machine that is running, which is the only one there could be — a `built-by` edge never orders
|
||||
a module after a build machine that stands on it. A provision is not a dependency of this relation:
|
||||
a consumer binds to its provider through what the push renders, and a change to the provider's image
|
||||
changes nothing in the consumer's; a consumer whose build does read a provider's source declares it.
|
||||
"A was deployed, so restart B" is the push's domain — B is replaced when what it reads changed — and
|
||||
not the plan's.
|
||||
|
||||
**2. A merge produces a plan, and the plan is a record.** The controller takes the modules the merge
|
||||
changed and everything reachable from them along `depends-on` edges, and sorts that set into tiers:
|
||||
tier 0 depends on nothing else in the set, tier 1 only on tier 0, and so on. The plan — the merge it
|
||||
answers, the tiers, and each module's state — is written to the store before any build is asked. The
|
||||
handler asks tier 0 and returns. Every build's outcome, taken in by the same handler that takes every
|
||||
outcome in, advances the plan it belongs to; a controller replaced mid-plan resumes it from the store.
|
||||
|
||||
**3. A tier is done when it is built, and when what the next tier needs from it is running.** A
|
||||
module whose roll-out policy says *roll out* is sent to its machines when it moves, as today. The next
|
||||
tier is asked only once every module in this tier is built and every rolled-out module of this tier
|
||||
that a later tier is `built-by` has been applied by the machines running it — the machines' reports
|
||||
say so. A module whose policy says *record* is built and not waited for. So a merge
|
||||
touching the build machine and the controller builds the build machine, waits until it is the build
|
||||
machine that is running, and only then asks for the controller's build.
|
||||
|
||||
**4. A plan is read where the mesh is read.** `status` lists every open plan: the merge, the tier it
|
||||
is at of how many, what it is waiting for and since when; `builds` lists the asked beside the built.
|
||||
A plan that has waited past a bound is named red there, which is the first fact of
|
||||
[issue 187](../04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md)'s list.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The merge handler returns in milliseconds; the receive loop is never held by a build again. Issue
|
||||
184's remaining cause — a handler that waits for its own work — is removed rather than worked
|
||||
around; the bus's heartbeats stop being dropped under a merge.
|
||||
- A controller roll in the middle of a plan costs nothing: the plan is in the store and the asks are
|
||||
in the queue (mesh-controller 194).
|
||||
- A release across repositories is a plan whose edges cross repositories; the order a person kept in
|
||||
a work-order file is the order the tiers give. Issue 186's third fault is answered by the plan,
|
||||
not by a separate release record.
|
||||
- The explicit `build --on <base>` stays as the way to ask for the same plan by hand.
|
||||
- A module's `build.on` remains the one place a manifest states a dependency the store cannot see.
|
||||
|
||||
## How this is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| Dependencies are one relation, each edge with its kind | an inventory test over a fixture catalogue: a runtime image, a module on it, a module packaging the controller's source, and the build machine; the four kinds come back from one call |
|
||||
| A merge's set is sorted into tiers along the three kinds: a code dependency in the same tier, a build dependency after its base is built, a runtime dependency after the build machine; the build machine's own base first | a unit test on the tiering over the mesh's real shape: the runtime image, the build machine on it, modules built by it, a plugin declared on one, the proxy packaging the controller, an unrelated module left out; a cycle is one last tier and said |
|
||||
| Only a runtime dependency gates on deployment, and only for a module that rolls out | the same test's gate cases |
|
||||
| The plan is written before any build is asked, and the handler returns | a controller test: a merge announcement produces a plan row with its tiers and one asked build per tier-0 module, and the handler is back before any outcome |
|
||||
| An outcome advances its plan; a complete tier asks the next; a tier with a rolled-out base waits for the machines' reports | store-backed tests over a two-tier plan: the first outcome marks built; the tier's roll-out gate holds until the report; the next tier is asked after |
|
||||
| A controller restarted mid-plan resumes it | a test that opens a plan, drops the handler, and advances from the store alone |
|
||||
| `status` lists open plans and names one that waits past the bound | the status JSON test with a fixture plan |
|
||||
| Live | a catalogue merge touching a base and a dependent: the plan's tiers in `status`, the base rolled before the dependent is asked |
|
||||
|
||||
## Built and proven live, 2026-10-01
|
||||
|
||||
> **Progressive insight — 2026-10-01.** The decision stands; these are the facts of its building.
|
||||
|
||||
Built in mesh-controller 197 (the relation, the plan record, the driver, `status`), 198 (`plans`),
|
||||
199 (a `built-by` edge orders and gates but never widens — the first live plan had taken the whole
|
||||
catalogue along for a controller change; `plans stop`), 200. The first merge handled by the finished
|
||||
machinery, at 18:56Z, was a controller change and produced the plan this record describes: tier 0
|
||||
the build machine; tier 1 the controller and the proxy that packages its source. The handler
|
||||
returned at once; the build machine was built, rolled, and the plan read *tier 0 built; waiting for
|
||||
builder on novox to be applied* until the machine reported; then tier 1 was asked, both built, and
|
||||
the plan read done — three minutes, read through the console with `plans`, the receive loop taking
|
||||
reports throughout. What the day between decision and proof taught is in issues
|
||||
[184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md),
|
||||
[186](../04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md) and
|
||||
[188](../04-ISSUES/188-a-refusal-inside-on-the-network-drops-a-machine-silently/00-report.md).
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0157](0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md), [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)
|
||||
- [Design 30 — The mesh updates itself on a push](../03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md)
|
||||
- Issues [184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md), [186](../04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md), [187](../04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md)
|
||||
@@ -0,0 +1,164 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-01
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
|
||||
---
|
||||
|
||||
# 163. Taking a module over is a comparison: what it compares, what it refuses, and what it carries
|
||||
|
||||
## Context
|
||||
|
||||
On an adopted machine the mesh holds what it finds until the module is taken, and taking is the
|
||||
cutover ([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)). The whole-node flip
|
||||
is previewed and confirmed by digest; the per-module cutover, the step that actually replaces a
|
||||
running service, previews nothing. `take` names the held things the next push will replace and
|
||||
where each original is kept. It does not say how the module's version of each differs from what
|
||||
runs. Ten issues from the first migrations are the same omission seen from ten sides:
|
||||
|
||||
- a port narrowed from everywhere to the private network, unannounced ([086](../04-ISSUES/086-taking-a-module-narrows-a-port-without-saying-so/00-report.md));
|
||||
- a configuration file replaced whole, dropping the one line that was the installation's own ([098](../04-ISSUES/098-taking-a-module-replaces-a-configuration-nobody-compared/00-report.md));
|
||||
- an image pin that had aged into a downgrade, discovered by three minutes of outage ([099](../04-ISSUES/099-a-modules-image-pin-ages-into-a-downgrade/00-report.md));
|
||||
- a secret minted for a service that already had one, with no way to carry the existing value in because it was a required secret and not the module's own ([100](../04-ISSUES/100-a-minted-secret-cannot-be-the-one-the-service-already-uses/00-report.md));
|
||||
- a container moved onto the module's own network, out of reach of the neighbour that called it by name ([101](../04-ISSUES/101-taking-a-service-reached-by-container-name-cuts-its-neighbours-off/00-report.md));
|
||||
- a resource whose target changed, leaving the old container running with no record naming it ([097](../04-ISSUES/097-a-resource-that-changes-target-leaves-the-old-one-behind/00-report.md));
|
||||
- a volume path that changed without the running container noticing, because the host does not compare that field ([126](../04-ISSUES/126-a-volume-path-is-not-in-the-spec-comparison/00-report.md));
|
||||
- a build that deployed at once because the module's policy said so, racing a data move ([126](../04-ISSUES/126-a-volume-path-is-not-in-the-spec-comparison/00-report.md));
|
||||
- a setting accepted where it was set and refusing the whole machine where it was read ([096](../04-ISSUES/096-a-setting-that-cannot-work-is-stored-and-stops-the-node/00-report.md));
|
||||
- a module that could not take over what genesis raised, because the two differed in name, network, data and image ([090](../04-ISSUES/090-the-forge-module-does-not-take-over-the-forge-genesis-raised/00-report.md));
|
||||
- a successor that could not stand beside its predecessor at all, answered by [ADR 0104](0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md)'s adapter ([093](../04-ISSUES/093-the-successor-proxy-cannot-serve-what-the-predecessor-still-serves/00-report.md)).
|
||||
|
||||
What the host records of a found thing is enough to compare from: a file's original, kept, with
|
||||
its digest, mode and owner; a container's id and whether it ran; whether anything changed it
|
||||
since. What it does not yet record is what a comparison needs most: the found container's image
|
||||
and when that image was made, the networks it is on and who else is on them, what it mounts, what
|
||||
it publishes. And the controller's rule that a machine is told everything or nothing turns one
|
||||
impossible statement into a machine nobody can talk to.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. A take is previewed, and the preview is a comparison.** For every held thing the module would
|
||||
replace, `take` puts what runs beside what the module declares and says the difference:
|
||||
|
||||
- a **container**: its image against the module's, with each image's creation date so older and
|
||||
newer have a meaning; its name; its networks, and the other containers on each found network
|
||||
that is not the module's; its published ports and the reach of each, found firewall and guard
|
||||
included; its mounts against the module's volumes and paths;
|
||||
- a **file**: the kept original against the declared content, as a difference, not two digests;
|
||||
- a **secret** the module takes that the mesh minted and nobody accepted, when the service's data
|
||||
was found — a service that already runs already has a value;
|
||||
- the module's **settings** on that machine, composed against its definition.
|
||||
|
||||
`take` without `--yes` prints the comparison and stops; `take --yes <digest>` cuts over exactly
|
||||
what was previewed, the way the flip is confirmed, and a preview whose account of the machine is
|
||||
older than the flip allows is refused the same way. The host supplies the facts in its report of
|
||||
what it holds: the found container's image and its creation date, its networks and their members,
|
||||
its mounts and published ports.
|
||||
|
||||
**2. Three differences refuse by default, each overridden by naming it.** An image **older** than
|
||||
the one running, by creation date — `--downgrade`, said once and recorded. A declared file that
|
||||
**differs** from the kept original — `--replace <path>`, or the module declares the file partially
|
||||
and writes into it ([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md)), which is
|
||||
the right answer wherever the file is the service's own and the format allows it. A **minted,
|
||||
unaccepted secret** for a service whose data was found — accept the value first, or `--mint
|
||||
<name>` to say the service shall take a new one. Two differences are said and not refused: a port
|
||||
whose reach **narrows**, and a found network whose other members may reach the container **by
|
||||
name**, each member named; both are the operator's to weigh, and the words are there to weigh them.
|
||||
|
||||
**3. A secret the mesh would mint may be accepted instead, own or required.** `secret accept`
|
||||
reaches a module's required secrets, not only its own: the value is a fact about the machine, and
|
||||
the mesh's job at a take is to learn it. The accepted value is sealed to the module as a minted one
|
||||
would be, and the provider that would have minted it is told it has one. Whether one accepted
|
||||
value should reach every consumer of a provider at once is [issue 165](../04-ISSUES/165-one-accepted-value-must-be-accepted-once-per-consumer/00-report.md)'s
|
||||
question and the next group's.
|
||||
|
||||
**4. A taken container may keep a found network, for a while, by a setting.** A per-machine
|
||||
setting names a found network the module's container also joins, so a neighbour that resolves it
|
||||
by name keeps resolving it. It is migration scaffolding in the sense of
|
||||
[ADR 0104](0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md): assigned only on an
|
||||
adopted machine, reported while it stands, removed when the neighbours are taken, and the preview
|
||||
names it. Taking a group of modules at once is not decided here; the setting makes the order free.
|
||||
|
||||
**5. The host compares every field it writes, and removes what it can no longer name.** A
|
||||
container is current when every field the host would write agrees with the one running — volumes
|
||||
and paths included; a field the host cannot compare recreates rather than passes. The host's
|
||||
record keeps a resource's former targets: a container or file the host **wrote** under a name or
|
||||
path the declaration no longer names is removed on the next apply and said; what was **found** is
|
||||
never removed, as ADR 0100 says. And the host answers the question nothing answered on
|
||||
2026-09-23: its report lists what runs on the machine that the mesh neither wrote nor holds —
|
||||
containers and listeners — as *strays*, so a thing left behind is seen the day it is left.
|
||||
|
||||
**6. A setting is judged where it is stored, and an impossible one costs a module, not a machine.**
|
||||
Storing a setting composes it against the module's current definition and refuses with the node,
|
||||
module, layer and key when it cannot work. A definition that later moves under a stored setting
|
||||
makes composition leave *that module* out of the machine's declaration — its held things kept, its
|
||||
containers untouched — and say the statement by name; the machine is still told everything else.
|
||||
A machine is told everything or nothing about what it *is* told; what it is not told is said.
|
||||
|
||||
**7. What genesis raises, it raises as the module that succeeds it declares** — name, network,
|
||||
data directory and image — so the module adopts it by the found rule that already exists, and a
|
||||
module meant to succeed a bootstrap service that it cannot adopt is a fault of genesis, found by a
|
||||
test that raises and then assigns. **`build` says when a policy will act on its result**, so a
|
||||
person choreographing a data move knows which module will not wait; under
|
||||
[ADR 0162](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md) the roll-out is the plan's, and
|
||||
the plan says it too.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `take` becomes the per-module twin of the flip: preview, digest, confirm. The flip's own preview
|
||||
gains the same comparisons for every module it takes.
|
||||
- The host's report of what it holds grows by the found container's image and creation date,
|
||||
networks and members, mounts and published ports; its store keeps former targets and strays.
|
||||
- Issues 086, 098, 099, 100, 101 close on rule 1 and 2; 097 and 126 on rule 5; 096 on rule 6;
|
||||
090 on rule 7; 093 is closed by ADR 0104's adapter, which runs.
|
||||
- Nothing here changes what an adopted machine keeps or when: found stays held, held is never
|
||||
removed, the original is kept before anything is written.
|
||||
|
||||
## How this is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| The host reports a found container's image and creation date, networks and their members, mounts and published ports | host unit tests over a fake runtime; the adoption bed's report |
|
||||
| `take` without `--yes` previews every held thing's difference and changes nothing; `--yes` with the digest cuts over; a stale account is refused | controller tests over a fixture report: a differing file, an older image, a narrowed port, a shared network, a minted secret |
|
||||
| An older image, a differing file and a minted secret for found data refuse without their override | the same tests |
|
||||
| A found network kept by a setting is joined, reported and named in the preview | a host test and a controller resolution test |
|
||||
| `secret accept` takes a required secret | an inventory test; the provider is told |
|
||||
| Every container field is compared; a former target the host wrote is removed and said; what was found is not | host tests: a volume path change recreates; a renamed container's predecessor is removed; a found one under the old name is kept |
|
||||
| Strays are reported | a host test over a fake runtime with a container nobody declared |
|
||||
| A setting that cannot compose is refused where stored, naming node, module, layer, key; a definition moving under one leaves that module out and says so | controller tests |
|
||||
| Genesis raises the forge as its module declares it | a genesis test that raises, assigns, and finds the module holding rather than raising a second |
|
||||
| Live | the next cutover on an adopted machine: `take` shows the comparison, refuses the downgrade if there is one, and the service keeps its configuration and its secret |
|
||||
|
||||
## Built, 2026-10-02
|
||||
|
||||
> **Progressive insight — 2026-10-02.** The decision stands; these are the facts of its building.
|
||||
|
||||
Built across mesh-host 63 and 64 and mesh-controller 201, 202 and the pull request that followed
|
||||
them. Rule 1: `take` previews every held thing's comparison and ends with a digest; `take --yes
|
||||
<digest>` acts on exactly that preview, and a changed preview or an account older than the flip
|
||||
allows is refused, as the flip's are. A published port's reach is said as the machine reported it,
|
||||
behind the found firewall whose rules are not read. Rule 2: an older image, a differing file and a
|
||||
minted, unaccepted secret for found data refuse, overridden by `--downgrade`, `--replace <path>` and
|
||||
`--mint <name>`; the secrets a module holds on a machine are read with where each came from. Rule 3:
|
||||
`secret accept --provider` reaches a required secret. Rule 4: the per-machine setting is `networks`,
|
||||
a container id to the found networks it keeps; judged for an adopted machine only, joined by the host
|
||||
after the container runs, part of the container's spec, named in the preview. Rule 5: the host's
|
||||
facts, former targets and strays. Rule 6: one judgement, run where a setting is stored and where a
|
||||
machine is composed; a module whose stored setting its definition can no longer compose is left out
|
||||
of the declaration, the envelope says so, the host keeps that module's things, and `plan` and `push`
|
||||
say it by name. A key that reaches nothing is refused where stored and said by `plan`, and never
|
||||
costs a module. Rule 7: genesis raises the forge under the module's container name, with its image
|
||||
digest and its data directory; the network is the one difference left, said by the take, because the
|
||||
bootstrap forge reaches the store on the machine's loopback.
|
||||
|
||||
**Not yet proven live.** Every machine of this mesh is converged, so the table's last row — a take
|
||||
on an adopted machine — waits for the next adoption. What is live is what the rows above it check.
|
||||
Issues 086, 098, 099, 100 and 101 stay located until that row is read.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md), [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md), [ADR 0103](0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md), [ADR 0104](0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md), [ADR 0162](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md)
|
||||
- [Design 05 — The node host](../03-DESIGN/01-to-be/05-the-node-host.md), [Design 09 — The node lifecycle](../03-DESIGN/01-to-be/09-the-node-lifecycle.md)
|
||||
- Issues 086, 090, 093, 096, 097, 098, 099, 100, 101, 126
|
||||
+43
-2
@@ -48,6 +48,31 @@ 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.
|
||||
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.
|
||||
|
||||
**Every decision is a record.** There is no ledger and no index file — if a decision is worth
|
||||
@@ -143,6 +168,15 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0132** — [A seat carries the tools its holder must serve](0132-a-seat-carries-the-tools-its-holder-must-serve.md)
|
||||
- **0134** — [The mesh says what it applied](0134-the-mesh-says-what-it-applied.md)
|
||||
- **0142** — [The mesh delivers its own components as binaries, not as container images](0142-the-mesh-delivers-its-own-components-as-binaries.md)
|
||||
- **0154** — [The mesh's own verbs are the mesh-controller seat's tools, and which verbs those are](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)
|
||||
- **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)
|
||||
- **0157** — [A build says what it does on the bus, as it happens](0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md)
|
||||
- **0158** — [A provider with one credential shares it with every consumer, and the vault remakes it for all of them at once](0158-a-provider-with-one-credential-shares-it-with-every-consumer.md)
|
||||
- **0159** — [A tool call names the machine it is for, every answer says which machine answered, and a holder's runtime serves its seat's verbs](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)
|
||||
- **0160** — [The mesh issues an assignment's subjects, and a runtime serves what it is issued](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
|
||||
- **0161** — [What deserves a seat: a role of a module is a seat, a singular fact about machines is a placement with a capacity of one, and a holder's software is the machine's](0161-what-deserves-a-seat.md)
|
||||
- **0162** — [A merge produces a tiered plan the mesh keeps, and a module's dependencies are one relation in the catalogue](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md)
|
||||
- **0163** — [Taking a module over is a comparison: what it compares, what it refuses, and what it carries](0163-taking-a-module-over-is-a-comparison.md)
|
||||
|
||||
### Its tiers, from the bottom up
|
||||
|
||||
@@ -174,6 +208,8 @@ 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)
|
||||
- **0109** — [A package registry seat is one per ecosystem, not one for all of them](0109-a-package-registry-seat-is-one-per-ecosystem.md)
|
||||
- **0126** — [A module declares its own seats; the mesh reserves its own](0126-a-module-declares-its-own-seats.md)
|
||||
- **0148** — [The mesh's names are resolved, not copied into every container](0148-the-meshs-names-are-resolved-not-copied-into-containers.md)
|
||||
- **0151** — [A route's internal name is composed under the node that serves it](0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md)
|
||||
|
||||
### What runs on them, and how it gets there
|
||||
|
||||
@@ -208,7 +244,7 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **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)
|
||||
- **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)
|
||||
- **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)*
|
||||
- **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)
|
||||
- **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)
|
||||
- **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)
|
||||
@@ -228,6 +264,9 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **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
|
||||
|
||||
@@ -239,7 +278,7 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0016** — [The lab](0016-the-lab.md)
|
||||
- **0037** — [Where a module lives](0037-where-a-module-lives.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) *(proposed)*
|
||||
- **0068** — [The lab takes requests, one at a time, and runs each from its own copy](0068-the-lab-takes-requests.md) *(superseded)*
|
||||
- **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)
|
||||
- **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)
|
||||
@@ -248,6 +287,7 @@ 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)
|
||||
- **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)
|
||||
- **0149** — [The live mesh is the test bed](0149-the-live-mesh-is-the-test-bed.md)
|
||||
|
||||
### How it is checked
|
||||
|
||||
@@ -268,5 +308,6 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0034** — [The local account owns the mesh, and a web application's login is not that](0034-the-local-account-owns-the-mesh.md)
|
||||
- **0080** — [The development cycle is checked, not trusted](0080-the-development-cycle-is-checked.md)
|
||||
- **0081** — [A decision nothing cites is not yet in the chain](0081-a-decision-nothing-cites-is-not-yet-in-the-chain.md)
|
||||
- **0153** — [The record is read by a module the mesh assigns, and the console lists it](0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)
|
||||
|
||||
<!-- index:end -->
|
||||
|
||||
@@ -1,78 +1,58 @@
|
||||
---
|
||||
layer: as-is
|
||||
status: implemented
|
||||
code: [hal]
|
||||
updated: 2026-08-23
|
||||
decisions: []
|
||||
code: [mesh-catalog modules/records, mesh-catalog modules/mesh-console]
|
||||
updated: 2026-09-30
|
||||
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
|
||||
|
||||
The mesh keeps two knowledge stores. They are not redundant, and knowing which is which is the
|
||||
difference between finding an answer in one search and rediscovering it over several hours.
|
||||
**The mesh keeps no knowledge store.** What it knows is what its modules answer, and the way a person
|
||||
or an agent asks is the console's tool list on the machine they sit at
|
||||
([13 — The console](13-the-console.md)). This document used to describe two stores; it is rewritten
|
||||
because neither exists from the mesh's side, and an as-is document that describes what is gone is a
|
||||
brochure.
|
||||
|
||||
## The operational memory
|
||||
## What was here, and where it went
|
||||
|
||||
A store of operational notes, written and read by whoever — human or agent — is working. Each
|
||||
note is a slug and a body: how something works, what went wrong, what the fix was, what
|
||||
assumption turned out to be false.
|
||||
Until the cut-over of 2026-09-28 the predecessor ran two stores: an operational memory of notes
|
||||
indexed on symptoms, and a structured archive of governed documents with a librarian approving
|
||||
promotion. Both were reached through the predecessor's tool server over the bus the mesh removed
|
||||
([issue 147](../../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md)).
|
||||
Nothing in the mesh reaches them now, and nothing in the mesh has replaced them: there is no note
|
||||
store, no archive, no librarian, and the lessons of the last days were written into this repository by
|
||||
hand. That is a gap, and it is stated here rather than papered over. What replaces a symptom-indexed
|
||||
memory, if anything does, is undecided.
|
||||
|
||||
It is indexed on **symptoms**. The entry someone needs is usually titled after the error they
|
||||
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 record
|
||||
|
||||
Its content is overwhelmingly the record of previous debugging: a large body of
|
||||
troubleshooting entries, module conventions, and standing notes about work that is open. It is
|
||||
the mesh's institutional memory of *what has already gone wrong*.
|
||||
**The design record is read where it is written.** Since 2026-09-30 a module, `records`, keeps a
|
||||
checkout of this repository from the forge — cloned from the `git` seat, reset to the origin on every
|
||||
merge the forge announces and every ten minutes — and answers over the bus: where a phrase appears as
|
||||
written, one document whole, what a folder holds, and where the checkout stands, each naming the
|
||||
commit it read. Which repository it reads is a setting on its assignment; the module names no mesh.
|
||||
|
||||
The cost of skipping it is documented in the mesh's own record: entries have been rediscovered
|
||||
from scratch, over hours, in sessions where the search was skipped because the trail felt
|
||||
confident. It fires hardest on familiar ground, not unfamiliar ground.
|
||||
It is listed by the console beside every other tool, with a description that says to search the
|
||||
literal words of a symptom before forming a hypothesis. That is what
|
||||
[ADR 0025](../../02-DECISIONS/0025-the-design-record-is-read-not-copied.md) meant by *beside
|
||||
everything else*, in a mesh with no store to be beside
|
||||
([ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)).
|
||||
|
||||
## The structured archive
|
||||
Nothing is copied. A checkout lags the source by seconds after an announced merge and by minutes
|
||||
otherwise, and says so.
|
||||
|
||||
A second store, structured rather than flat: spaces, pages, revisions, tiers, and full-text
|
||||
search. Where the operational memory is a note, this is a document with an owner and a
|
||||
lifecycle.
|
||||
## The constitution
|
||||
|
||||
Content is promoted through tiers — private, then team, then platform — with a librarian agent
|
||||
owning approval and promotion at the boundary. Proposals to edit are reviewed rather than
|
||||
applied.
|
||||
|
||||
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.
|
||||
[`00-META/how-we-build.md`](../../00-META/how-we-build.md) is the source of the mesh constitution
|
||||
([ADR 0021](../../02-DECISIONS/0021-hq-is-the-source-of-the-constitution.md)). The page it used to be
|
||||
synchronised into lived in the predecessor's archive and is unreachable; the constitution today is
|
||||
read from this repository, through the same module, and playbook 05's sync has nothing to write to.
|
||||
|
||||
## Where this repository sits
|
||||
|
||||
This repository is a third thing, and the objection was raised when it was created: a fourth
|
||||
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.
|
||||
A third thing beside two that are gone, which makes it the first: the one governed record the mesh
|
||||
has, public, read by a module the mesh assigns, and edited nowhere else.
|
||||
|
||||
@@ -82,6 +82,22 @@ to clone.
|
||||
The schema column added for this defaults to empty rather than null, because "not on a seat" is a
|
||||
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
|
||||
|
||||
**Capacity is not implemented.** The design's vocabulary has a seat with a capacity, and a
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
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,12 +15,13 @@ Where the two disagree, the implementation wins and the disagreement is stated.
|
||||
| [`04-delivery.md`](04-delivery.md) | Push to running: the three silos, levels, and what a green pipeline proves |
|
||||
| [`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 |
|
||||
| [`07-knowledge.md`](07-knowledge.md) | The two knowledge stores, and what each is for |
|
||||
| [`07-knowledge.md`](07-knowledge.md) | The mesh keeps no store: what it knows is what modules answer, and the record is read by one |
|
||||
| [`08-agents-and-work.md`](08-agents-and-work.md) | Agents as employees, tasks, workflows, and the meeting model |
|
||||
| [`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 |
|
||||
| [`11-the-lab.md`](11-the-lab.md) | The lab — the first piece of the new shape that exists, and what it does not yet do |
|
||||
| [`12-the-seats.md`](12-the-seats.md) | The seats the mesh defines, who holds one, and where a seat changes resolution |
|
||||
| [`13-the-console.md`](13-the-console.md) | The mesh's tools on the machine a person sits at, served by a module the mesh assigned there |
|
||||
|
||||
## What these documents are not
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code: [mesh-host]
|
||||
updated: 2026-09-29
|
||||
updated: 2026-10-02
|
||||
decisions:
|
||||
- 02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md
|
||||
- 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/0102-the-mesh-writes-into-a-shared-file-never-over-it.md
|
||||
@@ -153,6 +154,28 @@ checked:* unit tests hold the host to keeping a found file and container, conver
|
||||
taken, never removing a held file and reporting one that changed; the adoption bed asserts a found
|
||||
file byte for byte unchanged until its module is taken.
|
||||
|
||||
**What the host says of a found container, and what it removes** — revision, 2026-10-01
|
||||
([ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md)). Its report of a held
|
||||
container carries the image and the image's creation date, the networks it is on and the other
|
||||
containers on each, its mounts and its published ports — the facts a take compares. The host compares
|
||||
every field it writes before calling a container current, volumes and paths included; its record keeps
|
||||
a resource's former targets, removes a container or file it wrote under a name the declaration no
|
||||
longer names, never removes what was found, and reports what runs on the machine that it neither
|
||||
wrote nor holds. *How it is checked:* ADR 0163's table.
|
||||
|
||||
**What the host joins, keeps and raises for a take** — revision, 2026-10-02
|
||||
([ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 4, 6 and 7). A
|
||||
container may name networks it also joins once it runs — the found network a per-machine setting keeps
|
||||
for a taken container while a neighbour still resolves it there; joined after the run, part of the
|
||||
container's spec, refused when it cannot be joined. A declaration may name the modules the mesh left
|
||||
out of it because a stored setting cannot compose with the module's definition: the host keeps what it
|
||||
wrote and holds for a left-out module and says so, where absence used to read as removal. And genesis
|
||||
raises the bootstrap forge under the forge module's container name, with the module's image digest and
|
||||
its data directory, so the module holds it by the found rule; the network is the one difference a take
|
||||
has left to say. *How it is checked:* a host test joins a kept network and refuses one it cannot; a
|
||||
host test keeps a left-out module's record and hold and removes an absent module's; a bootstrap test
|
||||
holds the installer's constants to the module's manifest where the catalogue is checked out beside it.
|
||||
|
||||
**Found reaches every kind that can touch what the machine has**
|
||||
([ADR 0103](../../02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md)). For a module not yet taken, a directory present with no record
|
||||
keeps its mode and owner, a unit present with no record keeps its state and boot setting, a
|
||||
|
||||
@@ -7,8 +7,10 @@ code:
|
||||
- mesh-controller internal/identity/authority.go
|
||||
- mesh-host internal/identity/serving.go
|
||||
- mesh-host internal/apply (the service that reflects a rule set)
|
||||
updated: 2026-09-29
|
||||
updated: 2026-09-30
|
||||
decisions:
|
||||
- 02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md
|
||||
- 02-DECISIONS/0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md
|
||||
- 02-DECISIONS/0147-a-module-anchors-the-meshs-authority.md
|
||||
- 02-DECISIONS/0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md
|
||||
- 02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md
|
||||
@@ -289,6 +291,31 @@ 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
|
||||
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
|
||||
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.
|
||||
@@ -299,6 +326,14 @@ machine — declared or not — is what a nameserver in `resolv.conf` would be f
|
||||
service, the rest is the node — so what resolves is *anything under a node's name*, going to that
|
||||
node. What routes it once it arrives is a proxy's, and stays separate.
|
||||
|
||||
*2026-09-30.* **So the node in a route's internal name is the one whose proxy answers it**
|
||||
([ADR 0151](../../02-DECISIONS/0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md)).
|
||||
Composed from the node the module ran on, the name sent a client to a machine with nothing listening
|
||||
whenever the proxy ran elsewhere
|
||||
([issue 139](../../04-ISSUES/139-an-internal-route-name-resolves-to-the-consumers-node/00-report.md));
|
||||
composed from the serving node, the rule above holds without exception. The public name stays the
|
||||
module's node's, which is where the operator put it.
|
||||
|
||||
**The mesh writes the data and runs no daemon.** One wildcard per machine, from the same set that
|
||||
writes the hosts file. A resolver is third-party software and runs *on* the mesh rather than being
|
||||
*of* it: the mesh has no business shipping one, choosing which one, or knowing its configuration
|
||||
@@ -374,7 +409,9 @@ can reach from the outside but cannot resolve from the inside is a name it canno
|
||||
authority of its own.
|
||||
|
||||
**So a granted route is published into internal resolution as well** — the routed name to the node
|
||||
that serves it, mesh-wide, by the same mechanism that writes the node names. It is *given by the
|
||||
that serves it, mesh-wide, by the same mechanism that writes the node names — and as itself: a routed
|
||||
name has no mesh form, and the suffixed alias the roster once added beside it resolved to a refusal
|
||||
([issue 157](../../04-ISSUES/157-a-routed-names-internal-alias-is-served-by-nothing/00-report.md)). It is *given by the
|
||||
mesh, not chosen by a module*, for the same reason the node names are: a module listing the routes
|
||||
would go stale the day one changes. The mesh propagates the names it was told to serve and still
|
||||
knows nothing about what they mean
|
||||
|
||||
@@ -8,8 +8,9 @@ code:
|
||||
- mesh-host packaging/nox-mesh-host-network.sh
|
||||
- mesh-controller internal/token
|
||||
- mesh-controller internal/inventory/nodes.go
|
||||
updated: 2026-09-23
|
||||
updated: 2026-10-02
|
||||
decisions:
|
||||
- 02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md
|
||||
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
|
||||
- 02-DECISIONS/0004-a-node-and-how-it-joins.md
|
||||
- 02-DECISIONS/0005-the-node-host.md
|
||||
@@ -311,6 +312,32 @@ found firewall again and converges the openings through it; what was taken stays
|
||||
a predecessor leaves one and asserts nothing that serves changes until a module is taken or the
|
||||
node is converged, and that the flip closes exactly what the preview said.
|
||||
|
||||
**Taking a module is previewed, and the preview is a comparison** — revision, 2026-10-01
|
||||
([ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md)). For every held thing a
|
||||
module would replace, `take` puts what runs beside what the module declares: a container's image and
|
||||
its age, name, networks and their other members, published ports and their reach, mounts; a file's
|
||||
kept original against the declared content, as a difference; a secret the mesh minted for a service
|
||||
that already has one; the module's settings composed against its definition. An older image, a
|
||||
differing file and a minted secret for found data refuse unless named; a narrowed port and a shared
|
||||
network are said. `take --yes <digest>` cuts over what was previewed, as the flip does. A taken
|
||||
container may keep a found network by a per-machine setting while its neighbours are not yet taken.
|
||||
*How it is checked:* ADR 0163's table.
|
||||
|
||||
**A setting is judged where it is stored, and the take's words** — revision, 2026-10-02
|
||||
([ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1, 2, 4 and 6).
|
||||
The preview ends with a digest of what it said; `take --yes <digest>` acts on that preview and nothing
|
||||
else, and a preview that has changed since, or an account of the machine older than the flip allows, is
|
||||
refused as the flip's is. A module the machine holds nothing for has nothing to compare, and `--yes`
|
||||
suffices. The overrides are `--downgrade`, `--replace <path>` and `--mint <name>`; the per-machine
|
||||
setting that keeps a found network is `networks`, a container id to the networks it keeps, accepted
|
||||
for an adopted machine only. Storing a setting composes it against the module's current definition and
|
||||
refuses, naming node, module, layer and key, what cannot compose or reaches nothing. A definition that
|
||||
later moves under a stored setting costs that module its place in the machine's declaration, said by
|
||||
name in `plan`, `push` and the declaration itself, and the machine is told everything else; a stray
|
||||
setting no longer refuses the machine where it is read. *How it is checked:* controller tests over the
|
||||
one judgement — refused where stored, a module left out where composed, the envelope naming it — and
|
||||
over a take's digest, staleness and secrets.
|
||||
|
||||
A candidate machine is not empty. It has a package manager, probably a container runtime,
|
||||
configuration somebody chose. [ADR 0005](../../02-DECISIONS/0005-the-node-host.md)
|
||||
says the host never touches what it did not create — adoption is the deliberate act of taking
|
||||
|
||||
@@ -7,9 +7,10 @@ code:
|
||||
- mesh-controller internal/catalogue/build.go
|
||||
- mesh-controller internal/inventory/secrets.go
|
||||
- mesh-controller cmd/mesh-builder
|
||||
updated: 2026-09-12
|
||||
updated: 2026-09-30
|
||||
decisions:
|
||||
- 02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md
|
||||
- 02-DECISIONS/0037-where-a-module-lives.md
|
||||
- 02-DECISIONS/0009-modules-and-the-graph.md
|
||||
- 02-DECISIONS/0010-delivery.md
|
||||
- 02-DECISIONS/0005-the-node-host.md
|
||||
@@ -60,6 +61,32 @@ module from a repository and a path, and the root-only reading left every existi
|
||||
unbuildable — pointed at the catalogue the builder finds no manifest, pointed at a module's source
|
||||
it finds no manifest either.*
|
||||
|
||||
## A manifest is checked where it is written
|
||||
|
||||
*Added 2026-09-30, from [issue 148](../../04-ISSUES/148-a-manifest-outside-this-catalogue-has-no-check/00-report.md).*
|
||||
|
||||
The check the mesh applies at registration — the manifest parses strictly, every name in it is a
|
||||
usable one, its routes and events and seats are well formed, and no two manifests given together
|
||||
declare one seat — is a verb on the controller's binary, `module check <manifest>…`, and it needs no
|
||||
mesh. It reads the files it is given, runs the same functions registration runs, prints every problem
|
||||
in the manifest's own words, and exits non-zero if there was one. Somebody describing their own
|
||||
application in their own repository — the case [ADR 0037](../../02-DECISIONS/0037-where-a-module-lives.md)
|
||||
calls the one that matters most — runs it before pushing, and finds out there rather than when a
|
||||
running mesh refuses the registration, or later, when a machine applies something that resolved and
|
||||
should not have.
|
||||
|
||||
**What it cannot know, it says.** A seat another module declares elsewhere is unknown to a check that
|
||||
was not handed that module's manifest, and the output says so rather than refusing: pass the other
|
||||
manifest too. The mesh's own seats it knows from the binary, which is the one place that set may be
|
||||
read without a store ([ADR 0122](../../02-DECISIONS/0122-a-seat-is-data-a-rename-is-a-database-update.md)
|
||||
keeps the store authoritative, so a claim on a mesh seat is judged fully only at registration, and the
|
||||
check says that too).
|
||||
|
||||
*How it is checked:* the controller's test runs the check over the catalogue checkout beside it and
|
||||
over a manifest with a known fault, and asserts the first passes and the second names the fault; the
|
||||
test that used to be the only check, `TestEveryCatalogueManifestParses`, now stands beside a command
|
||||
anybody can run.
|
||||
|
||||
## The manifest in the repository is not the manifest the mesh holds
|
||||
|
||||
A resource names an artifact:
|
||||
|
||||
@@ -5,7 +5,7 @@ code:
|
||||
- mesh-controller internal/inventory/secrets.go
|
||||
- mesh-controller cmd/mesh-controller/rotate.go
|
||||
- mesh-controller examples/postgres-provisioner
|
||||
updated: 2026-09-21
|
||||
updated: 2026-10-01
|
||||
decisions:
|
||||
- 02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md
|
||||
- 02-DECISIONS/0009-modules-and-the-graph.md
|
||||
@@ -136,3 +136,16 @@ rotates, and is queried for who holds it, through exactly the machinery describe
|
||||
The rotation a module's own secret lacks is not a second mechanism; it is this one, pointed at a
|
||||
secret the vault provides. What this page proves for a database password holds, by construction,
|
||||
for a secret from the vault.
|
||||
|
||||
*Built 2026-10-01, the read-at-start half ([issue 180](../../04-ISSUES/180-a-modules-own-secret-cannot-be-rotated/00-report.md),
|
||||
[ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md)).* An own secret
|
||||
says how the module takes it — `taken: at-start` or `taken: applied` on its entry — and the mesh
|
||||
rotates only the first: `secret rotate <node> <module> <name>` makes it anew, seals it to the machine
|
||||
and the operator, and sends the machine, so the module starts again on it. A secret that says neither
|
||||
is refused with the word to write, because a credential rotated under software that never reads it
|
||||
again is the fault of issue 179 made deliberately; an applied one is refused until the staged form is
|
||||
built; an accepted one is refused as ADR 0113 says. `rotate` is a verb on the controller's seat with
|
||||
both shapes, so the console asks for either. A provider that shares its one credential with every
|
||||
consumer ([ADR 0158](../../02-DECISIONS/0158-a-provider-with-one-credential-shares-it-with-every-consumer.md))
|
||||
rotates the same way, with every holder's copy remade and every holding machine sent together. *How it is checked:* the tests named in issue 180, and a
|
||||
live rotation through the console of a secret a module reads at start.
|
||||
|
||||
@@ -148,6 +148,10 @@ than reproduced from a declaration — because there is nothing to reproduce it
|
||||
([ADR 0025](../../02-DECISIONS/0025-the-design-record-is-read-not-copied.md)), not by holding a
|
||||
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
|
||||
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
|
||||
|
||||
@@ -5,8 +5,10 @@ code:
|
||||
- mesh-controller cmd/mesh-builder
|
||||
- mesh-controller internal/builder
|
||||
- mesh-catalog modules/builder
|
||||
updated: 2026-09-29
|
||||
updated: 2026-10-01
|
||||
decisions:
|
||||
- 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
|
||||
- 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/0097-a-vendor-image-is-a-declared-build-input.md
|
||||
@@ -185,7 +187,8 @@ disagrees with it.
|
||||
| `grants` | credentials it must create for its consumers |
|
||||
| `filtering` | rules beyond its own ports |
|
||||
| `computed` | marks a module the controller generates rather than an author writing |
|
||||
| `build.artifacts` | what it produces |
|
||||
| `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 |
|
||||
| `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**
|
||||
([ADR 0091](../../02-DECISIONS/0091-a-mount-is-declared-three-ways.md)). A bind mount the module
|
||||
@@ -232,10 +235,10 @@ counts as a copy and what as a base.
|
||||
|
||||
| resource | is | a module may |
|
||||
|---|---|---|
|
||||
| `directory` | a directory with a mode and an owner | ✅ |
|
||||
| `file` | literal content, with `${bound:…}` and `${secret:…}` filled in | ✅ |
|
||||
| `directory` | a directory with a mode and an owner, **placed by the mesh** under the node's root: `place: "."` is the assignment's own root, `place: "mesh"` the mesh's directory for the module, a pathless one sits beneath the root by its id; a stated path is the placement for data that must stay where it is, and may itself sit beneath a placed one (`${dir:<id>}/…`). Everything else names it as `${dir:<id>}` ([issue 119](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md), [174](../../04-ISSUES/174-the-meshs-own-files-for-a-module-are-placed-by-the-definition/00-report.md)) | ✅ |
|
||||
| `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)) | ✅ |
|
||||
| `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, **named by id** and placed by the assignment (`accesses: {<id>: <path>}` on its settings); mounts say `${access:<id>}`; a path in the definition is the default an assignment replaces, tolerated while the catalogue converts ([issue 153](../../04-ISSUES/153-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md)) | ✅ |
|
||||
| `archive` | files fetched by digest and unpacked | ✅ |
|
||||
| `package` | a package that must be present | ✅ |
|
||||
| `network` | a named container network | ✅ |
|
||||
@@ -265,6 +268,31 @@ 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
|
||||
right number: they are loaded by a tool host, and a tool host is a process that stays up.
|
||||
|
||||
## A build says what it does, as it happens
|
||||
|
||||
*2026-10-01 — [ADR 0157](../../02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md).*
|
||||
|
||||
A build machine narrates every build on the bus as the role it holds: `started` when it takes the
|
||||
work, one `log.<build id>` event per line — every command it runs with its duration, every step of
|
||||
the recipe, and on failure the command's own output, line by line — and `built` for the outcome as
|
||||
before. The same lines still go to the machine's standard error, so a build machine with nobody
|
||||
listening is as readable as it was; a listener reads the same lines, live, from anywhere on the mesh.
|
||||
|
||||
One build is one subject. A reader follows it by subscribing that subject and nothing else, and the
|
||||
events stream keeps it for a week, so `builds --log <id>` — on the command line and as the
|
||||
controller's seat verb through the console — reads it back afterwards. `builds` lists every build's
|
||||
id beside it, and `build` says the id it asked with. The mesh keeps no second copy: the stream is the
|
||||
log. The console's `build` tool asks and answers at once with the id; the outcome is taken in — the
|
||||
build recorded, the module registered with its source — by whoever hears it, the waiting command or
|
||||
the daemon following the role's event, so a build nobody waited for still reaches the catalogue
|
||||
([issue 176](../../04-ISSUES/176-the-consoles-build-tool-neither-waits-nor-registers/00-report.md)). A viewer of builds, when one is built, is a subscriber over these subjects and the outcome; the
|
||||
builder needs nothing more for it.
|
||||
|
||||
*How it is checked:* the holder's grant is exactly `started`, `built` and `log.*` (broker test); a
|
||||
build's lines reach a reader of its subject in order and the stream holds them afterwards (link test
|
||||
against a real server); the seat verb with an id reads the log (controller test); and, live, a build
|
||||
after the roll-out read line by line through the console.
|
||||
|
||||
## The builder compiles the languages the mesh is written in
|
||||
|
||||
*2026-09-29 —
|
||||
|
||||
@@ -5,8 +5,9 @@ code:
|
||||
- mesh-catalog modules/showcase
|
||||
- mesh-controller internal/builder
|
||||
- mesh-sdk src
|
||||
updated: 2026-09-21
|
||||
updated: 2026-09-30
|
||||
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/0053-a-step-that-runs-on-a-schedule.md
|
||||
- 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
layer: to-be
|
||||
status: implemented
|
||||
code: [mesh-catalog, mesh-controller, mesh-host]
|
||||
updated: 2026-09-21
|
||||
updated: 2026-10-01
|
||||
decisions:
|
||||
- 02-DECISIONS/0158-a-provider-with-one-credential-shares-it-with-every-consumer.md
|
||||
- 02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md
|
||||
- 02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md
|
||||
- 02-DECISIONS/0085-a-secret-is-a-provision.md
|
||||
@@ -127,6 +128,22 @@ credential a provider grants; the export names each entry by the node and module
|
||||
the name they know it by, and says whether it is a module's own secret or a pair credential, so
|
||||
recovery addresses both alike.
|
||||
|
||||
### A provider with one credential
|
||||
|
||||
*Decided 2026-10-01 ([ADR 0158](../../02-DECISIONS/0158-a-provider-with-one-credential-shares-it-with-every-consumer.md)); the controller's half built the same day (mesh-controller PR 184): the offer's word, the need carrying the shared secret's name, the vault's one value under one generation stamp, remade for every holder on a later binding or a rotation, the rotate command sending every holder. What remains is each provider's definition saying `credential` and `taken`, with a start that applies the file — the media catalogue's work.*
|
||||
|
||||
Software that holds one credential — a download client's web password, an indexer's one API key —
|
||||
cannot give each consumer a login, so ADR 0048's form does not fit it and its values were accepted
|
||||
by hand. An offer may now say `"credential": {"own": "<secret>"}`: the provider's own secret *is* the
|
||||
credential every consumer of that provision receives, in the shape of an ordinary pair credential,
|
||||
under the provider's one user name. The vault keeps one value per provider assignment and provision,
|
||||
sealed to the provider's machine, each consumer's machine and the operator; because it holds no
|
||||
plaintext it remakes the value for every holder at once when a consumer binds or unbinds or a
|
||||
rotation is asked, and the mesh sends every holding machine together. The provider takes it as it
|
||||
says it takes its own secret (`taken`, issue 180); consumers read it at start. An accepted value is
|
||||
sealed to the consumers of the moment and not remade; a consumer that binds later waits for the next
|
||||
acceptance. *How it is checked:* the rows of ADR 0158's table; the controller's rows pass, the live row waits for the first provider.
|
||||
|
||||
## Beyond generate and hold
|
||||
|
||||
Owning a secret means owning more than its creation. The mesh being migrated onto has a working
|
||||
|
||||
@@ -7,8 +7,10 @@ code:
|
||||
- mesh-tools src/broker-amqp.ts (to be replaced)
|
||||
- mesh-catalog modules/nats (to be written)
|
||||
- mesh-sdk src (the protocol's NATS binding, step 3)
|
||||
updated: 2026-09-27
|
||||
updated: 2026-10-01
|
||||
decisions:
|
||||
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
||||
- 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
|
||||
- 02-DECISIONS/0106-the-bus-is-nats.md
|
||||
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
||||
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
|
||||
@@ -80,8 +82,18 @@ mesh.seat.<seat>.accept.<verb> work submitted to a role (JetStream: per-s
|
||||
mesh.seat.<seat>.event.<verb> a role's own event (JetStream: EVENTS)
|
||||
mesh.seat.<seat>.tool.<verb> a role's tool (core request/reply)
|
||||
mesh.ask.<node>.<command> the controller's command api (core request/reply)
|
||||
mesh.assignment.<node>.<module> an assignment's membership (JetStream: ASSIGNMENTS, last-per-subject)
|
||||
```
|
||||
|
||||
**Revised 2026-10-01** ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)): the rows above
|
||||
for a module's and a seat's tools are the shapes the controller *issues*, not rules a runtime carries.
|
||||
Every assignment is published a membership — what it serves and where, in which queue, its seat verbs,
|
||||
where its events land, what it may reach — on `mesh.assignment.<node>.<module>`, kept last per subject
|
||||
like a declaration, republished when the assignment's facts change. The runtime serves exactly that
|
||||
list; the account's grant is the same membership read the other way; the console's listing carries each
|
||||
tool's subject. The one rule a runtime keeps is the membership's own subject, from the two names in its
|
||||
credential.
|
||||
|
||||
**Revised 2026-09-27** ([ADR 0129](../../02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md)):
|
||||
**`mesh.build.request`, `mesh.control.built` and the BUILDS stream are gone.** A build is work submitted to a role, and the
|
||||
mesh already has a shape for that — a seat's `accept` subjects, on a work queue with a queue group of
|
||||
@@ -135,7 +147,7 @@ Core NATS is at-most-once. Everything the mesh must not lose lives in a JetStrea
|
||||
|---|---|---|---|
|
||||
| CONTROL | `mesh.control.>` except `alive` (a build's outcome moved to its seat, ADR 0121) | work queue, one consumer (the controller), explicit ack | the store-window guarantee ([ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)): the controller `nak`s with a delay while its store is away and the message is redelivered; nothing is dropped |
|
||||
| NODES | `mesh.node.>` | last per subject | one declaration per node, always the newest |
|
||||
| EVENTS | `mesh.mod.*.event.>` | limits (age, size), durable consumer per subscribing module | a subscriber that was down catches up; after `max-deliver` attempts the advisory feeds `mesh.events.dead` (its own small stream) |
|
||||
| EVENTS | `mesh.mod.*.event.>` and `mesh.seat.*.event.>` | limits (age, size), durable consumer per subscribing module | a subscriber that was down catches up; after `max-deliver` attempts the advisory feeds `mesh.events.dead` (its own small stream). *2026-10-01:* a build's whole log is here too, as the build-machine seat's `log.<build id>` events ([ADR 0157](../../02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md)) — one subject per build, a week of retention, read back by `builds --log <id>` with a consumer that is gone when the reading is done |
|
||||
|
||||
Tool calls and heartbeats stay on core NATS: a lost heartbeat is the next heartbeat; a lost tool
|
||||
call is a timeout the caller already handles.
|
||||
@@ -361,6 +373,13 @@ bridged. It is three things:
|
||||
|
||||
Nothing is built of this before §10's bed passes; the MCP surface is a thin adapter over (2).
|
||||
|
||||
*Built, and then made a module — 2026-09-30.* (1) and (2) exist: `operator issue` and the `mesh`
|
||||
client. What (2) describes as a program on the workstation is now the recovery path; the surface an
|
||||
operator uses is a module the mesh assigns to the machine, holding a credential the mesh minted —
|
||||
[ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md),
|
||||
[34 — The console](34-the-console.md). The tool list it asks for is no longer
|
||||
`catalog_tools`, which nothing served: each runtime answers `tools` for its own module.
|
||||
|
||||
## 8. What a module sees, and what the wire does
|
||||
|
||||
**The contract a module is written against does not change.** `publish` on an envelope becomes a
|
||||
|
||||
@@ -10,8 +10,9 @@ code:
|
||||
- mesh-controller cmd/mesh-controller/source.go
|
||||
- mesh-controller internal/inventory/migrations/0032-a-source-may-live-on-a-seat.sql
|
||||
- mesh-catalog modules/gitea/module.json
|
||||
updated: 2026-09-27
|
||||
updated: 2026-10-01
|
||||
decisions:
|
||||
- 02-DECISIONS/0161-what-deserves-a-seat.md
|
||||
- 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/0128-the-mesh-bus-is-required-not-ambient.md
|
||||
@@ -75,6 +76,17 @@ named at the wrong scope. Adding a seat is a decision, recorded, for the reason
|
||||
host's vocabulary is one: the set is what a person reads to learn what a mesh can have, and an entry
|
||||
nobody argued for is an entry nobody can explain.
|
||||
|
||||
**What deserves one** ([ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md)). A provision the
|
||||
mesh's own code dereferences by name is delivered by a mesh seat its provider claims — the store, the
|
||||
bus, the vault, the artifact store, the catalogue. A provision a module merely offers may have several
|
||||
providers, and a consumer with several is a person's choice. A fact of the shape *exactly one machine
|
||||
is X* — the hub — is not a seat, because a seat is held by a module assignment and points at it; it is
|
||||
a placement with a capacity of one, kept by the store, refused by name when a second is placed, and
|
||||
named in the listing. And a seat whose role is to speak to what the machine runs — the uplink — is
|
||||
held only by the dialect the machine runs: the machine says which in its profile, renewed with every
|
||||
report, and the holder declares the capability, so the wrong one is refused the way any missing
|
||||
capability is.
|
||||
|
||||
## The set
|
||||
|
||||
**The set is derived, and only the mesh's half is written here.** Revision, 2026-09-26
|
||||
@@ -108,8 +120,8 @@ convention, which later seats departed from.
|
||||
| `mesh-controller` | — | mesh | — | the controller |
|
||||
| `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-vault` | — | mesh | `secret`, reserved | the vault |
|
||||
| `mesh-artifact-store` | `the-artifact-store` | mesh | `artifact-store` | the artifact registry |
|
||||
| `mesh-vault` | — | mesh | `secret` | the vault (in the seed since [ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md); the earlier *reserved* named an effect no rule produced) |
|
||||
| `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-catalog` | `the-catalogue` | mesh | — | the catalogue |
|
||||
| `mesh-npm-package-registry` | `npm-package-registry` | mesh | `npm-package-registry` | the forge |
|
||||
| `mesh-git` | `git` | mesh | `git` | the forge |
|
||||
@@ -240,6 +252,8 @@ checked as their tables say:
|
||||
| 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`. |
|
||||
| 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` has one provider, the holder of `mesh-vault` | 0161: a second claimant of the seat is refused by name (`CanHold`); *correction of fact, 2026-10-01: no parser rule ever reserved the word, the seat does the work*. |
|
||||
| A singular fact about machines is a placement of capacity one, refused by name | 0161: the overlay command's test for a second hub; the store's unique index. |
|
||||
| A holder of `node-uplink` is the dialect the machine runs | 0161: the host reports `uplink-<manager>` in its profile with every report; a resolution test refuses the other holder naming the capability. |
|
||||
| Holdings are derived, and the overview lists every seat | 0118: the `seats` command test, including an unheld seat. |
|
||||
| A build source on the seat records no address; an unheld seat refuses only self-hosted builds | 0111's tests. |
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: proposed
|
||||
code: []
|
||||
updated: 2026-09-26
|
||||
status: in-progress
|
||||
code: [mesh-controller internal/catalogue]
|
||||
updated: 2026-09-30
|
||||
decisions:
|
||||
- 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/0113-the-vault-makes-every-secret.md
|
||||
- 02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md
|
||||
@@ -136,6 +137,12 @@ lost is a named volume, not a directory ([ADR 0030](../../02-DECISIONS/0030-data
|
||||
the assignment says nothing;
|
||||
- **a placement**, where the assignment puts one directory elsewhere: on a second disk, or where an
|
||||
adopted machine's data already is ([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)).
|
||||
*Built 2026-10-01 ([issue 153](../../04-ISSUES/153-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md)):*
|
||||
`places` on the assignment's settings, by directory id, with an owner where the data already has
|
||||
one; and `accesses`, by access id, for the operator's data — an access has an id and its mounts
|
||||
name it as `${access:<id>}`. Both validated as `endpoints` is: an id the definition does not
|
||||
declare is refused. *How it is checked:* the controller's placement tests, and the
|
||||
path-preservation proof extended to accesses.
|
||||
|
||||
**An operator's shared data** is an access, as before ([ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md)):
|
||||
never created, owned or removed by the mesh. The module requires read or read-write access. Where
|
||||
@@ -171,6 +178,34 @@ 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
|
||||
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 was 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)),
|
||||
the gap this design answered on 2026-09-26; built later the same day: a directory saying
|
||||
`place: "mesh"` is `<root>/mesh/<module>`, a directory beneath a placed one states its path as
|
||||
`${dir:<id>}/<rest>` and moves with it, and the same proof — both catalogues resolved and compared —
|
||||
shows forty-eight definitions naming the paths they named before.
|
||||
|
||||
*The operator's value travels only where it is asked for (2026-09-30,
|
||||
[issue 173](../../04-ISSUES/173-a-modules-settings-reach-every-fact-it-contributes/00-report.md)):*
|
||||
a setting overrides a key a contribution or a served fact declares and adds none; a file keeps taking
|
||||
any key. A provider that must tell its consumers an operator's value — a mail server's domain, an
|
||||
identity provider's issuer — declares it in what it serves as `${setting:<key>}`, and it is refused by
|
||||
name when nothing sets it. That is the contract half of this design's operator provider in the shape
|
||||
the placeholder allows: the definition says which values reach which requirement, and nothing else
|
||||
does. *How it is checked:* the unit tests named in issue 173, and the plan comparison that closed it.
|
||||
|
||||
## 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
|
||||
@@ -379,7 +414,9 @@ writes (`/var/lib/mesh/<module>`: sealed credentials, composed bindings) need a
|
||||
module-visible reservation. They do not: a module *requires* a `host-path` and receives a
|
||||
location; what the mesh writes for the module is the mesh's plumbing, placed where the mesh
|
||||
chooses and mounted in — never part of the module's contract. One reservation per
|
||||
requirement, `<root>/<module>/<name>`.
|
||||
requirement, `<root>/<module>/<name>`. *(Built 2026-09-30: the mesh's directory for a module is
|
||||
`<root>/mesh/<module>`, named in the definition as a placed directory and nowhere as a path —
|
||||
issue 174.)*
|
||||
|
||||
**Resolution happens in the controller, at declaration composition.** The node receives
|
||||
concrete paths exactly as today — the wire format and the host's apply do not change for
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
layer: to-be
|
||||
status: proposed
|
||||
code: []
|
||||
updated: 2026-09-27
|
||||
updated: 2026-10-01
|
||||
decisions:
|
||||
- 02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md
|
||||
- 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
|
||||
- 02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md
|
||||
- 02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md
|
||||
---
|
||||
@@ -114,6 +116,19 @@ automate the freeze.
|
||||
(this is how the uplink managers and the re-registrations above were done). Only image-bearing
|
||||
modules need the build machine, which narrows what the deadlock above can block.
|
||||
|
||||
## What a merge does now (2026-10-01)
|
||||
|
||||
Revision, [ADR 0162](../../02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md). The
|
||||
trigger exists: the forge announces a merge on the bus and the controller acts on it (ADR 0157 made
|
||||
the build narrate; this makes the merge a plan). A module's dependencies are one relation in the
|
||||
catalogue — `depends-on` edges of four kinds: stands-on, packages, built-by, declared. A merge takes
|
||||
what changed and everything reachable from it along those edges, sorts the set into tiers, writes the
|
||||
plan to the store, asks the first tier and returns. Each outcome advances the plan; a tier whose
|
||||
rolled-out modules a later tier is built by waits until the machines report them applied; a
|
||||
controller replaced mid-plan resumes from the store. `status` lists open plans and names one that
|
||||
has waited too long. The transition discipline for breaking changes in the list above is still
|
||||
unwritten, and still the next thing.
|
||||
|
||||
## Why now, and why not yet
|
||||
|
||||
**Why it matters:** self-update is the difference between a mesh a person maintains by typing
|
||||
|
||||
@@ -13,6 +13,7 @@ code:
|
||||
- mesh-catalog modules/mesh-catalog
|
||||
updated: 2026-09-28
|
||||
decisions:
|
||||
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.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/0128-the-mesh-bus-is-required-not-ambient.md
|
||||
@@ -120,6 +121,12 @@ and a module consuming one event from two emitters could tell them apart only by
|
||||
The subject already carries the emitter, so the key a module sees names it too — which makes a
|
||||
disagreement between a manifest and the code a typo rather than a category error.
|
||||
|
||||
*2026-10-01 ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)):* where a name lands is now
|
||||
*issued* to each assignment as a membership the controller publishes, rather than derived by a rule
|
||||
the runtime carries; a module still declares only names, and gains one fact about itself — whether its
|
||||
instances are interchangeable — which decides whether the mesh issues it the module's plain subject
|
||||
beside its machine's.
|
||||
|
||||
## 2. Three namespaces, and nothing else
|
||||
|
||||
**Its own** — `mesh.mod.<module>.>`. Its events and its tools. Nothing else may publish into it,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: designed
|
||||
code: []
|
||||
updated: 2026-09-28
|
||||
status: implemented
|
||||
code: [mesh-controller, mesh-tools]
|
||||
updated: 2026-10-01
|
||||
decisions:
|
||||
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
||||
- 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
|
||||
- 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
|
||||
@@ -78,6 +81,16 @@ machine's holder and the holders' queue group would hand the call to whichever a
|
||||
node-scoped seat's tool therefore carries the node it is asked of. Nothing about a mesh-scoped seat
|
||||
changes.
|
||||
|
||||
*2026-10-01 ([ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)):*
|
||||
the same shape now serves a **module's** tool on several machines, which had the queue-group fault
|
||||
this section describes for seats: each instance also serves `mesh.mod.<module>.tool.<tool>.<node>`,
|
||||
a caller writes `<module>.<tool>@<node>`, and every answer names the machine that gave it. And §3 is
|
||||
built for every holder, not only the controller: the credential names the seats a module claims and
|
||||
their verbs, the runtime serves each with the tool of the same name on the seat's subject, and the
|
||||
bus admits it only where the module holds the seat. *Later the same day ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)):*
|
||||
the subjects a holder serves, and a module's own, stop being derived in the runtime and are issued to
|
||||
the assignment as a membership the controller publishes; the shape stays, the deciding moves.
|
||||
|
||||
## 5. Discovery
|
||||
|
||||
**What a role answers is a read.** The seats and their protocols are records, so the list is a query
|
||||
@@ -102,6 +115,12 @@ being something a person carries and becomes something the mesh runs, on a node,
|
||||
An agent's authority can then be role-shaped: *the forge's tools*, rather than a list of
|
||||
module-specific names that changes the day the forge is replaced.
|
||||
|
||||
*Decided and designed on 2026-09-30:* the module is the console —
|
||||
[ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md),
|
||||
[34 — The console](34-the-console.md). It builds the second half of §5 now (a module's tools are
|
||||
asked of the module, through a `tools` verb every runtime answers) and lists a role's tools when the
|
||||
records carry them.
|
||||
|
||||
## 7. Versioning
|
||||
|
||||
A seat's tools are an interface and change like one. Additive within a version. A change that would
|
||||
@@ -120,6 +139,36 @@ by side until nothing is bound to the old one.
|
||||
- **Two nodes holding one node-scoped seat derive two addresses.** Checked by the same test as the rest
|
||||
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
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: implemented
|
||||
code: [mesh-catalog, mesh-tools, mesh-controller]
|
||||
updated: 2026-10-01
|
||||
decisions:
|
||||
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
||||
- 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
|
||||
- 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.
|
||||
|
||||
**Every module tool takes the machine to ask** (*2026-10-01*, [ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)):
|
||||
an optional `node` the console lists on each one, puts into the subject and never hands to the module,
|
||||
for a module that runs on several machines; without it whichever instance answers first does, and the
|
||||
console appends *answered by <machine>* to every answer. A seat's verb takes none; the seat's scope
|
||||
decides. *Later the same day ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)):* the console composes no
|
||||
subject at all; each tool's subject comes with the listing, and a stateful module on two machines is
|
||||
listed once per machine because the mesh issued it no plain subject.
|
||||
|
||||
**A tool that was not listed can still be called.** Listing is discovery; calling is the grant. An agent
|
||||
that knows a tool's name asks for it by `<module>.<tool>` and the module answers or the bus says why not.
|
||||
|
||||
**What is missing from the list, and until when.** A role's tools and the mesh's own verbs — `status`,
|
||||
`push`, `assign` — are the `mesh-controller` seat's under ADR 0132 and are not served yet; their three
|
||||
prerequisites are listed in that record. When the seat serves them, the console lists them beside the
|
||||
modules' own, and the person stops opening a shell for the mesh's own questions. Until then the console
|
||||
says so in its handshake.
|
||||
|
||||
## 4. Where it runs
|
||||
|
||||
On whichever machines an operator sits at, by assignment. It is not on the control node by default and
|
||||
does not need to be: it reaches the bus like any module, from anywhere in the mesh. A machine that is
|
||||
not a node cannot have it, which is the right refusal — the mesh reaches what it declares, and a
|
||||
workstation that wants the console joins first.
|
||||
|
||||
The person's credential and the `mesh` client (design [25](25-the-bus-on-nats.md) §7) remain the path
|
||||
for a machine that is not a node, and the path to a mesh not yet far enough along to assign anything.
|
||||
|
||||
## 5. Removing it
|
||||
|
||||
Unassigning the console from a machine revokes its bus account at the next composition and stops the
|
||||
container; nothing is left on the machine that could still connect. An agent pointed at the loopback
|
||||
address gets a refused connection, which is the truthful answer.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Check | Defends |
|
||||
|---|---|
|
||||
| a module invoking one tool may publish that subject and no other tool's; `*` may publish every one; neither may publish an event or subscribe what it did not consume | ADR 0152, the grant |
|
||||
| a module registering two tools answers three names to `tools`, with schemas; a module naming its own `tools` is refused at load | ADR 0152, discovery |
|
||||
| against a real bus: two modules up, a third held and not running — the console lists the two and names the third as not answering | ADR 0152, silence is not success |
|
||||
| a call through the console's endpoint reaches a module over the bus and the answer is the module's own, unshaped | ADR 0035, a surface decides nothing |
|
||||
| on the live mesh: the console assigned to a workstation answers `tools/list` on loopback and a call to the forge returns repositories | the exit of work-order step 3 |
|
||||
| the composed filter for a machine carrying the console opens no port for it | ADR 0144 |
|
||||
|
||||
## What shipped, 2026-09-30
|
||||
|
||||
Everything above, the same day: mesh-controller PR 164 (`invokes`, `module check`), mesh-tools PR 20
|
||||
(`mesh serve`, the `tools` verb), mesh-catalog PR 181 (`mesh-console`). Verified on the live mesh: the
|
||||
console assigned to a workstation answered `tools/list` on its loopback with 62 tools from the modules
|
||||
whose runtimes had been rebuilt to answer `tools`, named 36 modules as not answering (modules that serve
|
||||
no tools, and modules whose new runtime the mesh records rather than rolls out), and a `tools/call` of
|
||||
the forge's `gitea_list_repos` returned repositories. An agent on that machine reaches it as an HTTP
|
||||
MCP server and reports it connected. The mesh assigned the declared port unchanged, which is what a
|
||||
machine with nothing else on it does; the console binds whatever it is given.
|
||||
|
||||
Two things shipped bent, both stated in [`00-as-is/13-the-console.md`](../00-as-is/13-the-console.md):
|
||||
the person's client through the console (`--console`) exists and was exercised in the test suite, not
|
||||
on the live mesh; and a module registered from the catalogue by hand recorded its source as a URL rather
|
||||
than as a path on the git seat, because `--self` takes the forge path form — the rebuild-on-merge still
|
||||
matched it by URL.
|
||||
|
||||
## What this does not settle
|
||||
|
||||
- Narrowing a console's grant per assignment. ADR 0046 makes it a setting; nothing reads one yet.
|
||||
- The mesh's own verbs on the bus. Design 33's third family; this document only says where they appear
|
||||
once they exist.
|
||||
- A person's identity behind the console. The mesh sees the console's account; design 15 keeps the
|
||||
question open.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md) — the decision
|
||||
- [33 — The tools the mesh answers](33-the-tools-the-mesh-answers.md) — what the console lists
|
||||
- [25 — The bus on NATS](25-the-bus-on-nats.md) §7 — the person's client this makes a module of
|
||||
- [issue 147](../../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md) — the symptom
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
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)
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
status: located
|
||||
status: resolved
|
||||
opened: 2026-08-23
|
||||
located-in: [hal, hq]
|
||||
fixed-by:
|
||||
amended-design: 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
|
||||
located-in: [mesh-catalog modules/records, mesh-catalog modules/mesh-console]
|
||||
fixed-by: ADR 0153; mesh-catalog PR 183 (records), PR 185 (a phrase that wraps)
|
||||
amended-design: 03-DESIGN/01-to-be/35-reading-the-record.md
|
||||
---
|
||||
|
||||
# 006 — This repository is not indexed into the knowledge base, and the claim that it is holds up a decision
|
||||
@@ -169,3 +169,40 @@ them into. The record stays open, and its answer is no longer "index this reposi
|
||||
is whatever the mesh grows as its own knowledge surface, if it grows one. Until then the honest fix
|
||||
is the README, which should stop claiming a property nothing provides.
|
||||
|
||||
|
||||
## Where this stands, 2026-09-30
|
||||
|
||||
The README no longer claims a property nothing provides: it says the indexing never existed, that the
|
||||
store it named is unreachable since the cut-over, and that ADR 0025's answer — read, not copied, by an
|
||||
agent that consults this repository — is decided and not built. That was the honest fix the previous
|
||||
note asked for, and it is done.
|
||||
|
||||
The record stays open on ADR 0025's build, and on nothing else. The mesh's own operator surface is now
|
||||
a module ([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)); the
|
||||
reader ADR 0025 describes is the mesh session of design 15, which would answer through that surface
|
||||
like any tool. What closes this is still the check 0025 names: search for a phrase that appears only in
|
||||
a design document here, and get it back.
|
||||
|
||||
## 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,7 +1,7 @@
|
||||
---
|
||||
status: open
|
||||
status: located
|
||||
opened: 2026-09-22
|
||||
located-in: []
|
||||
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
@@ -35,3 +35,14 @@ it changes before it changes it, and for taking a module this one does not.
|
||||
included, and ask for the same kind of confirmation as the flip?
|
||||
- Or should taking refuse while a port of the module is reachable more widely than the module
|
||||
declares, until the operator either changes the module's exposure or confirms the narrowing?
|
||||
|
||||
## Decided, 2026-10-01
|
||||
|
||||
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1 and 2: the preview names a narrowing. Building follows,
|
||||
host first, then the controller's `take`.
|
||||
|
||||
## Built, 2026-10-02
|
||||
|
||||
mesh-controller 201 and the pull request after it: the preview names it, and `take --yes <digest>`
|
||||
acts on the preview that was read. Stays located until a take is read on an adopted machine — every
|
||||
machine of this mesh is converged today, so the record's live row has not been run.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
status: open
|
||||
status: resolved
|
||||
opened: 2026-09-22
|
||||
located-in: []
|
||||
fixed-by:
|
||||
located-in: [mesh-controller internal/link/protocol.go (the report field that was missing), internal/inventory, cmd/mesh-controller (node show and status)]
|
||||
fixed-by: mesh-controller 7683ba8, corrected by 1d9c102
|
||||
amended-design:
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
# 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.
|
||||
+17
-2
@@ -1,7 +1,7 @@
|
||||
---
|
||||
status: open
|
||||
status: located
|
||||
opened: 2026-09-22
|
||||
located-in: []
|
||||
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
@@ -48,3 +48,18 @@ network, or it is not a takeover.
|
||||
directory — so the module adopts it by the rule that already exists?
|
||||
- Should something refuse to call a module the successor of a bootstrap service it cannot adopt?
|
||||
- Is the forge's own address better resolved than set, which is [issue 088](../088-the-forges-own-address-names-a-port-it-may-not-have/00-report.md)?
|
||||
|
||||
## Decided, 2026-10-01
|
||||
|
||||
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rule 7: genesis raises as the module declares. Building follows,
|
||||
host first, then the controller's `take`.
|
||||
|
||||
## Built in part, 2026-10-02
|
||||
|
||||
mesh-host 64: genesis raises the forge under the module's container name (`gitea`), pinned to the
|
||||
module's image digest, with the module's data directory mounted at `/data` — so the module finds it,
|
||||
holds it, and a take compares equal images and the same data. A test holds the installer's constants
|
||||
to the module's manifest where the catalogue is checked out beside it. The network is the difference
|
||||
left: the bootstrap forge runs on the machine's network to reach the store on its loopback, the module
|
||||
runs bridged and publishes its ports, and the take says so. Closing waits for group 9's genesis test —
|
||||
a mesh raised, the module assigned, and the module found holding rather than raising a second forge.
|
||||
|
||||
+9
-2
@@ -1,8 +1,8 @@
|
||||
---
|
||||
status: located
|
||||
status: resolved
|
||||
opened: 2026-09-22
|
||||
located-in: [mesh-catalog, mesh-controller internal/catalogue]
|
||||
fixed-by:
|
||||
fixed-by: ADR 0104 — the route adapter module (mesh-catalog modules/route-adapter) writes each migrated route into the predecessor's proxy; it runs on the home server's migration
|
||||
amended-design:
|
||||
---
|
||||
|
||||
@@ -71,3 +71,10 @@ answered by an **adapter** that writes into the predecessor's own configuration.
|
||||
the predecessor's proxy keeps serving every name and keeps its certificates, while each migrated
|
||||
module's name is pointed at the mesh's container. The proxy is the last cutover again, and by then
|
||||
every route is one the mesh contributed.
|
||||
|
||||
## Resolved, 2026-10-01
|
||||
|
||||
The adapter ADR 0104 decided exists and runs: `route-adapter` provides `route` on an adopted
|
||||
machine by writing each migrated module's route where the predecessor's proxy reads it, and the
|
||||
proxy itself is the last cutover. [ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md)
|
||||
records the rest of what a take compares.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
status: open
|
||||
status: resolved
|
||||
opened: 2026-09-23
|
||||
located-in: []
|
||||
fixed-by:
|
||||
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
|
||||
fixed-by: mesh-controller (the pull request after 201: JudgeSettings, LeftOut), mesh-host 64 (left_out kept)
|
||||
amended-design:
|
||||
---
|
||||
|
||||
@@ -58,3 +58,17 @@ knowing the code.
|
||||
an operator to undo it without reading the source?
|
||||
- Is there anything a node must never be pushed without, such that sending a partial declaration is
|
||||
worse than sending none?
|
||||
|
||||
## Decided, 2026-10-01
|
||||
|
||||
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rule 6: judged where stored; an impossible statement costs a module. Building follows,
|
||||
host first, then the controller's `take`.
|
||||
|
||||
## Resolved, 2026-10-02
|
||||
|
||||
One judgement, in the catalogue, run where a setting is stored and where a machine is composed. Stored,
|
||||
a setting that cannot compose with the module's current definition is refused naming the node, the
|
||||
module, the layer and the key; a key that reaches nothing is refused there too. Composed, a definition
|
||||
that moved under a stored setting leaves that module out of the machine's declaration — the envelope
|
||||
names it, the host keeps what it holds and wrote for it, `plan` and `push` say it — and the machine is
|
||||
told everything else. A stray setting no longer refuses the whole machine where it is read.
|
||||
|
||||
+16
-3
@@ -1,8 +1,8 @@
|
||||
---
|
||||
status: open
|
||||
status: resolved
|
||||
opened: 2026-09-23
|
||||
located-in: []
|
||||
fixed-by:
|
||||
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
|
||||
fixed-by: mesh-host 63 (former targets removed, strays reported), mesh-controller 201/202 (strays shown)
|
||||
amended-design:
|
||||
---
|
||||
|
||||
@@ -75,3 +75,16 @@ found, and so would be kept for ever on purpose.
|
||||
module unassigned between the two declarations?
|
||||
- What reports this? Nothing on the machine currently answers "what is running here that the mesh
|
||||
did not ask for", which is the question that would have found this in seconds.
|
||||
|
||||
## Decided, 2026-10-01
|
||||
|
||||
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rule 5: former targets are removed and strays reported. Building follows,
|
||||
host first, then the controller's `take`.
|
||||
|
||||
## Resolved, 2026-10-02
|
||||
|
||||
mesh-host 63: the host's record keeps a resource's former targets, removes a container or file it
|
||||
wrote under a name the declaration no longer names, never what was found, and reports strays — what
|
||||
runs on the machine that the mesh neither wrote nor holds. mesh-controller 201 and 202 show strays
|
||||
on `node show` for an adopted and a converged machine alike; the live mesh reported four on the
|
||||
control node the evening it rolled.
|
||||
|
||||
+13
-2
@@ -1,7 +1,7 @@
|
||||
---
|
||||
status: open
|
||||
status: located
|
||||
opened: 2026-09-23
|
||||
located-in: []
|
||||
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
@@ -63,3 +63,14 @@ written.
|
||||
substitutes settings into content today.
|
||||
- Is the kept original enough of an answer, given nothing restores it and nothing points at it
|
||||
when the service starts behaving differently?
|
||||
|
||||
## Decided, 2026-10-01
|
||||
|
||||
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1 and 2: the difference is shown and a differing file refuses. Building follows,
|
||||
host first, then the controller's `take`.
|
||||
|
||||
## Built, 2026-10-02
|
||||
|
||||
mesh-host 63 reports the difference between the kept original and the declared content; mesh-controller
|
||||
201 shows it in the preview and refuses a differing file unless `--replace <path>` names it, or the
|
||||
module declares the file partially. Stays located until a take is read on an adopted machine.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
status: open
|
||||
status: located
|
||||
opened: 2026-09-23
|
||||
located-in: []
|
||||
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
@@ -60,3 +60,14 @@ expected rate.
|
||||
nothing answers the first.
|
||||
- Is a digest pin the right thing for a module that takes over an existing service at all, or
|
||||
should a cutover be able to say *keep what is running* and record what that was?
|
||||
|
||||
## Decided, 2026-10-01
|
||||
|
||||
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1 and 2: the images are compared by age and a downgrade refuses. Building follows,
|
||||
host first, then the controller's `take`.
|
||||
|
||||
## Built, 2026-10-02
|
||||
|
||||
mesh-host 63 reports the found image and both images' creation dates; mesh-controller 201 says
|
||||
DOWNGRADE and refuses unless `--downgrade` is said. Stays located until a take is read on an adopted
|
||||
machine.
|
||||
|
||||
+15
-2
@@ -1,7 +1,7 @@
|
||||
---
|
||||
status: open
|
||||
status: located
|
||||
opened: 2026-09-23
|
||||
located-in: []
|
||||
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
@@ -65,3 +65,16 @@ the module can only be installed fresh.
|
||||
Should it, so the dangerous case can be refused rather than discovered?
|
||||
- What is the reverse path: the mesh has minted one, the service ignored it, and the working value
|
||||
is still on the machine. Nothing reconciles those.
|
||||
|
||||
## Decided, 2026-10-01
|
||||
|
||||
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 2 and 3: a minted secret for found data refuses; secret accept reaches required secrets. Building follows,
|
||||
host first, then the controller's `take`.
|
||||
|
||||
## Built, 2026-10-02
|
||||
|
||||
`secret accept <node> <module> <name> --provider <node>` reaches a required secret (mesh-controller 201).
|
||||
The pull request after it reads every secret a module holds on a machine with its origin, and a take
|
||||
of a module whose data was found refuses a minted, unaccepted one — naming the accept that carries
|
||||
the existing value in, or `--mint <name>` to let the service take the new one. Stays located until
|
||||
a take is read on an adopted machine.
|
||||
|
||||
+14
-2
@@ -1,7 +1,7 @@
|
||||
---
|
||||
status: open
|
||||
status: located
|
||||
opened: 2026-09-23
|
||||
located-in: []
|
||||
located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
@@ -61,3 +61,15 @@ exercise.
|
||||
learn to take a group atomically? Nothing takes more than one module at a time today.
|
||||
- Does the same hole exist for anything else the predecessor's runtime resolves and the mesh's does
|
||||
not — a network alias, a `depends_on`, a name in a shared `/etc/hosts`?
|
||||
|
||||
## Decided, 2026-10-01
|
||||
|
||||
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1 and 4: the neighbours are named; a found network may be kept by a setting. Building follows,
|
||||
host first, then the controller's `take`.
|
||||
|
||||
## Built, 2026-10-02
|
||||
|
||||
The preview names every neighbour on a found network (mesh-controller 201). The pull request after it
|
||||
adds the per-machine setting `networks` — a container id to the found networks it keeps — judged for an
|
||||
adopted machine only, and mesh-host 64 has the taken container join each once it runs. Stays located
|
||||
until a take is read on an adopted machine.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
status: open
|
||||
status: resolved
|
||||
opened: 2026-09-23
|
||||
located-in: []
|
||||
fixed-by:
|
||||
amended-design:
|
||||
located-in: [mesh-controller cmd/mesh-controller/network.go (the placing command), internal/inventory/migrations/0004-the-overlay.sql (one hub)]
|
||||
fixed-by: nothing to build — ADR 0161 rule 2; the second hub was already refused by name, and the private network's seat waits for ADR 0121's server and client modules
|
||||
amended-design: [03-DESIGN/01-to-be/26-the-seats.md]
|
||||
---
|
||||
|
||||
# 105 — The hub of the private network is a placement, not a seat
|
||||
@@ -32,3 +32,16 @@ a node-scoped seat held by every node, which is true and not what was asked.
|
||||
- Does the per-node seat still say anything once the hub is a seat, or is it the interface's
|
||||
presence restated?
|
||||
- What else in the mesh is "exactly one" and recorded as a placement rather than a seat?
|
||||
|
||||
## Resolved, 2026-10-01
|
||||
|
||||
Read against the code: the store has kept one hub since the overlay's first migration (a unique
|
||||
index), and `overlay place <node> --hub` refuses a second naming the first. What this report saw as
|
||||
silent is not. [ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md), rule 2, answers the
|
||||
question that remained: a singular fact about machines is a placement with a capacity of one,
|
||||
refused by name and named in the listing — never a seat, because a seat is held by a module
|
||||
assignment and the private network is the host's own until
|
||||
[ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)'s
|
||||
server and client modules exist. That seat stands, deferred with the split it needs.
|
||||
|
||||
*How it is checked:* the overlay command's test for a second hub, and the index.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
status: open
|
||||
status: resolved
|
||||
opened: 2026-09-23
|
||||
located-in: []
|
||||
fixed-by:
|
||||
amended-design:
|
||||
located-in: [mesh-controller internal/catalogue/seats.go (the seed lacks mesh-vault), mesh-catalog modules/mesh-vault/module.json (claims nothing)]
|
||||
fixed-by: mesh-controller PR 192 (the seat row), mesh-catalog PR 205 (the claim; the vault's events renamed to its own)
|
||||
amended-design: [03-DESIGN/01-to-be/26-the-seats.md]
|
||||
---
|
||||
|
||||
# 106 — The vault claims no seat, so nothing refuses a second one
|
||||
@@ -31,3 +31,19 @@ others were missed the same way — every provider added after 0079.
|
||||
- A mesh-scoped seat `mesh-vault`, by the 0079 convention — is there any reason not to?
|
||||
- Should a provider of a mesh-scoped provision be required to claim a seat, or say explicitly that
|
||||
more than one is allowed, so the omission cannot recur?
|
||||
|
||||
## Decided, 2026-10-01
|
||||
|
||||
[ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md), rule 1: `mesh-vault` joins the mesh's
|
||||
own set, mesh-scoped, delivering `secret`, and the vault claims it; a second provider is a second
|
||||
claimant, refused by name. The record also answers the second question: a provision the mesh's own
|
||||
code dereferences by name gets a seat, every other mesh-scoped provision may have several providers.
|
||||
Design 26's *reserved* for `secret` named an effect no rule produced; corrected there.
|
||||
|
||||
## Resolved, 2026-10-01
|
||||
|
||||
`seats` on the live mesh lists `mesh-vault` at mesh scope, delivering `secret`, held by the vault on
|
||||
the control node. A second provider of `secret` is now a second claimant and refused by name
|
||||
(`CanHold`'s test). Found on the way: the vault's definition could not be rebuilt at all — it emitted
|
||||
`secret.provisioned` and the like, which the builder reads as another module's events — so the events
|
||||
are now the vault's own, `provisioned`, `rotated`, `deprovisioned`.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
status: located
|
||||
status: resolved
|
||||
opened: 2026-09-23
|
||||
located-in: [mesh-controller internal/link, mesh-host internal/link]
|
||||
fixed-by:
|
||||
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
|
||||
amended-design:
|
||||
---
|
||||
|
||||
@@ -40,3 +40,16 @@ exists there and is thrown away at the wire.
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,60 @@
|
||||
# 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: located
|
||||
status: resolved
|
||||
opened: 2026-09-24
|
||||
located-in: [mesh-host internal/apply]
|
||||
fixed-by:
|
||||
located-in: [mesh-controller internal/catalogue/declaration.go (every container was given the roster at creation)]
|
||||
fixed-by: mesh-controller PR 161 — no container is given a mesh name; it resolves through its machine's resolver (ADR 0148, landed 2026-09-30 once issue 110 did)
|
||||
amended-design:
|
||||
---
|
||||
|
||||
@@ -59,3 +59,27 @@ 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.
|
||||
- Either way: what tells an operator that a container is running with an address the node no longer
|
||||
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.
|
||||
|
||||
+23
-3
@@ -1,8 +1,9 @@
|
||||
---
|
||||
status: open
|
||||
status: resolved
|
||||
opened: 2026-09-24
|
||||
located-in: []
|
||||
fixed-by:
|
||||
located-in:
|
||||
- mesh-catalog modules/dnsmasq (the runtime was never told; the resolver answered by interface)
|
||||
fixed-by: mesh-catalog PR 175 (the runtime is reloaded and keeps its containers over a restart) and PR 176 (the resolver answers by address, so a query from a bridge is admitted) — measured 2026-09-30, 01-resolution.md
|
||||
amended-design:
|
||||
---
|
||||
|
||||
@@ -51,3 +52,22 @@ knows that is what the rule means.
|
||||
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
|
||||
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
@@ -0,0 +1,64 @@
|
||||
# 110 — resolved: a container on any network reaches the resolver, and is answered
|
||||
|
||||
*2026-09-30. Measured on the three converged machines; the adopted one holds its resolver module until it
|
||||
is taken and is not covered.*
|
||||
|
||||
## What was actually wrong
|
||||
|
||||
Not what the report predicted. The report named the filter: a container on the runtime's default
|
||||
network asks from a bridge address, and the converged filter admitted queries by source address only.
|
||||
That was true when it was written and was fixed before this issue was ever tested — the filter admits
|
||||
by the link a packet arrives on ([ADR 0144](../../02-DECISIONS/0144-anything-on-a-machine-may-call-anything-on-it.md)),
|
||||
and a container's bridge is admitted whole. Tested on every machine: the query arrives, the filter
|
||||
passes it.
|
||||
|
||||
Three other things were wrong, each hiding the next.
|
||||
|
||||
**The runtime had never been told.** The resolver module writes the runtime's `dns` key into the
|
||||
runtime's own configuration file. The runtime reads that key when it starts and not on a reload, and on
|
||||
two machines the runtime predated the file — so every container they started got a public resolver, and
|
||||
`novox.internal` came back as not existing. Nothing reported this: the file was present and current,
|
||||
the resolver ran, and a name not existing is a valid answer. Fixed in mesh-catalog PR 175: the module
|
||||
also sets `live-restore` and reloads the runtime when its file changes, so the one restart the `dns` key
|
||||
needs no longer stops every container. The restart is then the operator's, once per machine; done on
|
||||
both today, with every running container kept.
|
||||
|
||||
**The resolver dropped the query.** With the runtime corrected, a container's query reached the resolver
|
||||
— and got no answer, on every machine, including the one whose runtime had been right all along. The
|
||||
socket was bound to the private address; the filter admitted the packet; dnsmasq received it and
|
||||
discarded it without a line of log. Its configuration said `interface=mesh0`, and dnsmasq admits a
|
||||
query by the interface it arrives on when told an interface: a container's query is addressed to the
|
||||
private address but arrives on the runtime's bridge, and the bridge is not `mesh0`. Fixed in mesh-catalog
|
||||
PR 176: the resolver is told the address to answer on, not the interface that carries it, and a query to
|
||||
that address is admitted whatever bridge brings it. The bridges are the runtime's to name.
|
||||
|
||||
**The report's second half was wrong.** "Two of four machines bind the resolver to loopback only" was
|
||||
an inference from the containers' behaviour, and the behaviour had the cause above. The resolver bound
|
||||
the private address on all four; nothing had asked it there.
|
||||
|
||||
## What is verified
|
||||
|
||||
From a container on the runtime's default network, started by hand and given nothing, on each of the
|
||||
three converged machines: `novox.internal` answers with the hub's private address, through the machine's
|
||||
own resolver. That is the fourth check of
|
||||
[ADR 0148](../../02-DECISIONS/0148-the-meshs-names-are-resolved-not-copied-into-containers.md) — "on every
|
||||
network the runtime offers" — and its first step; the record's step 2 (the runtime told per machine, as
|
||||
a file) was already how the module works. Step 3 may now begin.
|
||||
|
||||
## What checks it
|
||||
|
||||
By hand, today. Nothing in the mesh asserts that a container can resolve a mesh name: the resolver's
|
||||
own tests cover what it answers, not who can ask. The check that would have caught all three faults is
|
||||
the one the report asked for and 0148 lists — a container on the default network resolving a mesh name
|
||||
— and it is not built. It belongs with the reachability check of
|
||||
[issue 145](../145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md),
|
||||
which is parked; until then this is a thing a person verifies after touching the resolver, the filter,
|
||||
or the runtime's configuration.
|
||||
|
||||
## What this cost to find
|
||||
|
||||
The three faults produced one symptom — a container that cannot resolve — and each fix revealed the
|
||||
next. The first was found by reading the runtime's own view of its configuration rather than the file;
|
||||
the second by capturing the query on the bridge and finding it arrive and go unanswered; the third only
|
||||
by admitting the first belief was wrong. A machine that had been believed to work all day had never
|
||||
worked either.
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
status: located
|
||||
status: resolved
|
||||
opened: 2026-09-25
|
||||
located-in: [hq, mesh-catalog modules/showcase, mesh-sdk src/tools/index.ts, mesh-tools]
|
||||
fixed-by:
|
||||
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
|
||||
amended-design:
|
||||
---
|
||||
|
||||
@@ -99,3 +99,27 @@ 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
|
||||
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.
|
||||
|
||||
## 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: located
|
||||
status: resolved
|
||||
opened: 2026-09-25
|
||||
located-in: [mesh-catalog modules/umami]
|
||||
fixed-by:
|
||||
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]
|
||||
fixed-by: mesh-host e82789a (issue 135's fix) — recreating the container with a current roster ended it; verified live 2026-09-30
|
||||
amended-design:
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# 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: located
|
||||
status: resolved
|
||||
opened: 2026-09-25
|
||||
located-in: [mesh-catalog modules, mesh-controller internal/catalogue]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
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
|
||||
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
|
||||
---
|
||||
|
||||
# 119 — A module definition decides where its files live on the machine
|
||||
@@ -133,3 +133,30 @@ not by any check.
|
||||
- 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
|
||||
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). *Placed since later the same day: `place: "mesh"` — issue 174 is resolved.*
|
||||
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,9 +1,9 @@
|
||||
---
|
||||
status: located
|
||||
status: resolved
|
||||
opened: 2026-09-26
|
||||
located-in: [mesh-controller internal/catalogue/declaration.go, mesh-catalog modules/keycloak, mesh-catalog modules/minio, mesh-catalog modules/nextcloud]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
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
|
||||
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
|
||||
---
|
||||
|
||||
# 122 — A module cannot ask for its own public name, so three manifests wrote this mesh's names into the catalogue
|
||||
@@ -114,3 +114,27 @@ 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?
|
||||
- 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.
|
||||
|
||||
## 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: located
|
||||
status: resolved
|
||||
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]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
fixed-by: ADR 0156; mesh-controller migration 0048 (feat/the-artifact-store-seat-is-named-for-its-scope); mesh-catalog modules/distribution
|
||||
amended-design: 03-DESIGN/01-to-be/26-the-seats.md
|
||||
---
|
||||
|
||||
# 123 — The image registry is named after a role, and *artifact* is defined as one format
|
||||
@@ -72,3 +72,15 @@ adopted, rather than on protocols.
|
||||
exercise that leaves the code disagreeing?
|
||||
- What check would keep the glossary honest — a definition tested against the kinds a definition may
|
||||
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,7 +1,9 @@
|
||||
---
|
||||
status: open
|
||||
status: resolved
|
||||
opened: 2026-09-26
|
||||
located-in: [mesh-host internal/apply, mesh-controller]
|
||||
located-in: [mesh-host internal/link (the apply report line), mesh-controller cmd/mesh-controller (status and its all-well condition)]
|
||||
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
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# 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.
|
||||
@@ -1,7 +1,9 @@
|
||||
---
|
||||
status: open
|
||||
status: resolved
|
||||
opened: 2026-09-26
|
||||
located-in: [mesh-host internal/apply]
|
||||
fixed-by: mesh-host 63 (every written field compared), mesh-controller 201 (build says the policy)
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 126 — a volume path is not in the spec comparison, and a roll-out raced a data move
|
||||
@@ -45,3 +47,14 @@ Instant renames both ways broke the circular dependency (forge needed for builds
|
||||
builds needed for the push, push needed for the forge): data back to the old path,
|
||||
old-spec forge started, artifacts rebuilt, data renamed forward, push. Nothing lost;
|
||||
the install-page junk was discarded twice.
|
||||
|
||||
## Decided, 2026-10-01
|
||||
|
||||
[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 5 and 7: every field compared; build says the policy. Building follows,
|
||||
host first, then the controller's `take`.
|
||||
|
||||
## Resolved, 2026-10-02
|
||||
|
||||
mesh-host 63: every field the host writes is compared before a container is called current, volumes
|
||||
and paths included. mesh-controller 201: `build` and the take-in say when a module's policy rolls a
|
||||
result out at once; under ADR 0162 the plan says it too.
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
---
|
||||
status: located
|
||||
status: resolved
|
||||
opened: 2026-09-26
|
||||
located-in: [mesh-catalog ca-trust]
|
||||
located-in: [mesh-catalog modules/ca-trust]
|
||||
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
|
||||
|
||||
@@ -1,45 +1,82 @@
|
||||
# Diagnosis
|
||||
# 129 — diagnosis
|
||||
|
||||
*2026-09-29.*
|
||||
*2026-09-30, from the workstation the issue was opened on.*
|
||||
|
||||
## What was ruled out
|
||||
## Still live, and reproduced exactly
|
||||
|
||||
**That something already carries the root and it is only misplaced.** It does not. The authority
|
||||
serves its root at a path beside its ACME directory, and the one thing that fetches it — the route
|
||||
proxy — puts it in a directory of its own and hands it to one program. Nothing has ever written
|
||||
into a machine's trust store. Measured on three converged machines: the anchors present are the
|
||||
predecessor's authority and a developer tool's local root, and on the machines where the
|
||||
predecessor's was deliberately removed, every internal name fails verification.
|
||||
The certificate is genuine, the authority is the mesh's, and nothing on the machine trusts it:
|
||||
|
||||
**That the private network could carry it, the way it carries the registry's trust.** That is what
|
||||
the report proposed, and it was rejected on consideration rather than on difficulty
|
||||
([ADR 0147](../../02-DECISIONS/0147-a-module-anchors-the-meshs-authority.md), option 1): being on
|
||||
the network is what makes the registry *reachable* and is therefore the right trigger there, while
|
||||
trusting an authority is a separate fact from being able to reach it. The anchor's directory and
|
||||
the command that refreshes the extracted bundles are also one operating system's difference, which
|
||||
is the host's half of the mesh and not the controller's.
|
||||
```
|
||||
$ 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)
|
||||
|
||||
**That it needs a new host resource type.** It does not, today. A file and a service say the whole
|
||||
of it, which the packet filter already proves. The primitive becomes the right answer when a second
|
||||
operating system is in play, and not before.
|
||||
$ curl https://keycloak.novox.internal/
|
||||
curl: (60) SSL certificate OpenSSL verify result: unable to get local issuer certificate (20)
|
||||
```
|
||||
|
||||
## Where it belongs
|
||||
`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.
|
||||
|
||||
A module in the catalogue: it requires `internal-acme-ca`, fetches the root over the mesh's own
|
||||
network, installs it as a trust anchor, refreshes the machine's bundles, and — because being
|
||||
unassigned stops its unit, and stopping the unit is what undoes it — takes both away again.
|
||||
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 owner is therefore `mesh-catalog`, module `ca-trust`, and nothing in the control plane.
|
||||
**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 module exists, and this stays open until a machine holds it
|
||||
## The authority serves what the module needs
|
||||
|
||||
*2026-09-29.* `ca-trust` is in the catalogue and merged
|
||||
([ADR 0147](../../02-DECISIONS/0147-a-module-anchors-the-meshs-authority.md)), and what it renders
|
||||
is checked in the control plane's own suite: the script fetches from the authority it was bound to,
|
||||
and the unit runs it both ways.
|
||||
`step-ca` is up and healthy, and publishes `roots: /roots.pem` for both `acme-ca` and
|
||||
`internal-acme-ca`. That endpoint returns PEM:
|
||||
|
||||
**No machine has been assigned it, and nothing has verified a name because of it.** The bed written
|
||||
for that cannot run ([issue 146](../146-the-foundation-cannot-be-raised-on-the-bus-the-mesh-runs-on/00-report.md)),
|
||||
and the live mesh has not been given the module. So the symptom this record opened on — every
|
||||
internal name failing verification on every machine — is still true everywhere, and the record stays
|
||||
`located` until it is not. Closing it on a module that exists would be closing it on an intention.
|
||||
```
|
||||
$ 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.
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
# 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,9 +1,9 @@
|
||||
---
|
||||
status: open
|
||||
status: resolved
|
||||
opened: 2026-09-28
|
||||
located-in: [mesh-catalog, mesh-controller internal/catalogue]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
fixed-by: mesh-controller PR 169 (the check, module check, the catalogue-wide test); mesh-catalog PR 188 (the catalogue that passes it); ADR 0155
|
||||
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
|
||||
---
|
||||
|
||||
# 134 — A definition may still name the mesh, and the check that would say so does not exist
|
||||
@@ -64,3 +64,17 @@ that.
|
||||
hostnames above are the first real cases.
|
||||
- Should a build context name a repository on the git seat rather than by URL, and if so, what does
|
||||
that mean for a context in *another* mesh's forge?
|
||||
|
||||
## Resolved, 2026-09-30
|
||||
|
||||
The check exists: `InstallationProblems`, run by `module check` and by a catalogue-wide test. Run over
|
||||
the 77 definitions it found 42 values, not 15 — the by-hand count had missed a second name one
|
||||
character after the first on the same line, which is the kind of thing a check is for. The three open
|
||||
questions: **a domain in a `why` string does not break the rule**, prose is not judged, and the eight
|
||||
were rewritten anyway because this catalogue is public; **a service's public name is the name the mesh
|
||||
composes for its route**, read through the route's binding, and an operator's own value is a setting;
|
||||
**a build context names a repository on the git seat**, `seat: git` with the path, and a context in
|
||||
another mesh's forge stays a URL, which the check reports and `names-on-purpose` would declare. The
|
||||
seven values that remain are declared with their reason — four applications built outside the mesh —
|
||||
and are the list that shrinks ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)).
|
||||
Registration does not refuse yet; it will when the list has been empty for a release. *2026-09-30, later the same day:* it refuses — the list was empty the day the check landed, and the operator asked for it (mesh-controller PR 175); `module add` and a build's result are refused in the check's words, with the way out, and the build stays recorded.
|
||||
|
||||
@@ -68,3 +68,31 @@ container runtime's shape, not a choice; the answer is to recreate, which is wha
|
||||
that would rather re-read a roster from a file can already ask for one as a fact
|
||||
([ADR 0120](../../02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md)) and restart on
|
||||
it.
|
||||
|
||||
## The container was umami, and it had its own record (2026-09-30)
|
||||
|
||||
The container in the table above is umami, and its symptom had already been filed three days earlier as
|
||||
[issue 118](../118-umamis-store-answers-the-dial-and-times-out-the-query/00-report.md) — the store
|
||||
answering the dial and timing out the query, a restart loop, and a public name answering `502`. Neither
|
||||
record noticed the other: 118 was reasoning about path-MTU and conntrack on the network between the
|
||||
container and the store, which is what a stale name looks like from inside the container.
|
||||
|
||||
118 is resolved by this record's fix and says so. Noted here because a symptom filed twice, diagnosed
|
||||
once, and closed in one place is how a repository comes to disagree with itself.
|
||||
|
||||
## What replaced this fix (2026-09-30)
|
||||
|
||||
The fix here — putting the mesh's names into the digest the host compares, so a container whose names
|
||||
moved is recreated like one whose image moved — worked, and cost more than it was worth. It made the
|
||||
roster part of every 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](../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) goes at
|
||||
the cause this record only described: **a copy taken at creation is stale the moment the roster moves,
|
||||
and detecting that is not as good as not copying.** A container resolves the mesh's names through its
|
||||
machine's resolver, at the moment it asks, so the fault this record reports cannot occur rather than
|
||||
being noticed a restart later.
|
||||
|
||||
Recorded here because this is where somebody arrives to find out why the digest carries names, and the
|
||||
answer is that it did, for two days short of a month, and stopped.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
status: open
|
||||
status: resolved
|
||||
opened: 2026-09-28
|
||||
located-in: [mesh-controller internal/catalogue, mesh-catalog]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
located-in: [mesh-host internal/profile/detectors.go (no detector for the network manager), mesh-host cmd/mesh-host (the profile is detected at enrolment only), mesh-controller internal/link (a report carries no profile), mesh-catalog modules/networkmanager, systemd-networkd, dhcpcd (declare no capability of their own)]
|
||||
fixed-by: mesh-controller PR 192 (a report carries the profile), mesh-host PR 61 (uplink-<manager> detected and reported with every apply), mesh-catalog PR 206 (each holder declares its own)
|
||||
amended-design: [03-DESIGN/01-to-be/26-the-seats.md]
|
||||
---
|
||||
|
||||
# 138 — Two modules claim one seat and are not interchangeable, and nothing says so
|
||||
@@ -54,3 +54,25 @@ able to switch the manager, which ADR 0117 refuses for a reason that has not cha
|
||||
alternative is one module that speaks whichever dialect the machine needs, chosen from the report.
|
||||
- What should happen on a machine that switches manager afterwards? The seat would then be held by the
|
||||
wrong module, and the machine is the only place that knows.
|
||||
|
||||
## Decided, 2026-10-01
|
||||
|
||||
[ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md), rule 3: the host's profile gains one
|
||||
capability per network manager found active, each holder declares its own, and the existing
|
||||
capability refusal does the rest, naming it. The profile is detected again by every apply and
|
||||
travels in the report, so a machine that switches managers is refused at its next push. The uplink
|
||||
stays one seat; the capability picks the dialect. Order of building: controller (a report may carry
|
||||
a profile), host, then the three definitions.
|
||||
|
||||
## Resolved, 2026-10-01
|
||||
|
||||
Every machine now reports which network manager it runs — the control node `uplink-systemd-networkd`,
|
||||
the laptop and the workstation `uplink-networkmanager`, the home server both `uplink-dhcpcd` and
|
||||
`uplink-networkmanager`, which is its truth — renewed with every report, and each holder declares the
|
||||
capability it needs, so the wrong holder is refused on assignment with the capability named. The uplink
|
||||
stays one seat; the capability picks the dialect. A machine that switches managers is a machine whose
|
||||
holder lacks a capability at its next push.
|
||||
|
||||
*How it is checked:* the host's detector test per manager; the controller's test that a report's
|
||||
profile replaces the enrolled one; the capability refusal's existing tests; live, `node show <machine>`
|
||||
lists `uplink-<manager>` for each.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
status: open
|
||||
status: resolved
|
||||
opened: 2026-09-28
|
||||
located-in: [mesh-controller internal/catalogue]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
located-in: [mesh-controller internal/catalogue/declaration.go (composeName took the consumer's own name as the internal domain)]
|
||||
fixed-by: mesh-controller PR 163 — the internal name composes under the node whose proxy serves the route (ADR 0151, 2026-09-30)
|
||||
amended-design: 03-DESIGN/01-to-be/08-connectivity.md
|
||||
---
|
||||
|
||||
# 139 — An internal route name resolves to the consumer's node, not the one that serves it
|
||||
@@ -49,3 +49,15 @@ resolve whether or not anything answers.
|
||||
and if so, is `route` still one mesh-wide provision or a node-scoped seat with a mesh-wide fallback?
|
||||
- What certifies the name in either case? The certificate is obtained by whoever terminates TLS, and
|
||||
that is the question above in another form.
|
||||
|
||||
## Answered (2026-09-30)
|
||||
|
||||
[ADR 0151](../../02-DECISIONS/0151-a-routes-internal-name-is-composed-under-the-node-that-serves-it.md):
|
||||
the internal name is composed under the node that serves the route — the machine the request arrives
|
||||
at — because `<x>.<node>.internal` means *goes to that node* and nothing else. The public name stays
|
||||
the consumer node's, which is where the operator put it. The first question is answered that way; the
|
||||
second, a per-node route holder, is a decision about seats and is left where it is; the third is
|
||||
unchanged, since the proxy that terminates the name is given it and certifies it.
|
||||
|
||||
mesh-controller PR 163 carries it. On this mesh every route is served beside its module, so no name
|
||||
changed; the controller's tests hold the case where it would.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
status: located
|
||||
status: resolved
|
||||
opened: 2026-09-28
|
||||
located-in:
|
||||
- mesh-controller internal/catalogue/manifest.go
|
||||
@@ -7,7 +7,7 @@ located-in:
|
||||
- mesh-controller internal/catalogue/declaration.go
|
||||
- mesh-controller examples/route-proxy
|
||||
- mesh-catalog (every routed module manifest)
|
||||
fixed-by:
|
||||
fixed-by: mesh-controller bdf965d (a module names its endpoints) and c68d3a7 (an assignment configures an endpoint as one thing) — the filter, the proxy's names and both authorities now read one statement
|
||||
amended-design: 03-DESIGN/01-to-be/08-connectivity.md
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# Resolution
|
||||
|
||||
*2026-09-29.*
|
||||
|
||||
**Built, and this record did not say so.** The issue was written on 2026-09-28 and answered the same
|
||||
week by two commits in `mesh-controller`; nothing came back to close it, so the mesh's own account of
|
||||
itself said for a day that reach was declared nowhere while the code read it in three places.
|
||||
|
||||
- `bdf965d` — *a module names its endpoints, and a route names the one it serves*. `listens[].name`
|
||||
is the endpoint; a route contribution names the endpoint rather than repeating a port.
|
||||
- `c68d3a7` — *an assignment configures an endpoint as one thing*. The `endpoints` settings key, per
|
||||
node, by endpoint name: `{"endpoints": {"ssh": {"port": 20134, "reach": "public"}}}` — port, label
|
||||
and reach in one block, which is what [ADR 0138](../../02-DECISIONS/0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md)
|
||||
asked for and what [ADR 0046](../../02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md)
|
||||
said configuration is.
|
||||
|
||||
## The three readers, which is what the issue was about
|
||||
|
||||
The complaint was that the per-node source override had exactly one caller. It now has three, and
|
||||
they are the three mechanisms reach was decided to settle at once:
|
||||
|
||||
| reader | what it does with it |
|
||||
|---|---|
|
||||
| the filter | `Reaches` turns each endpoint's reach into the rule for its machine port |
|
||||
| the proxy's names | `composeName` composes the public name, the internal name, or both — and a name nobody asked for is not composed |
|
||||
| the authorities | the proxy certifies only names it was actually given, each from its own authority, through two host policies rather than one |
|
||||
|
||||
**A routed endpoint keeps the manifest's port**, which is ADR 0138's own insight and older than it
|
||||
([ADR 0045](../../02-DECISIONS/0045-a-machine-firewall-is-the-sum-of-what-it-listens-on.md)): the
|
||||
proxy is how it is reached, so `public` there asks for a public *name*, not an open port.
|
||||
|
||||
**An endpoint that is not routed is reached and never named.** Git over ssh is that case — the one
|
||||
the issue said the model could not express — and it is now the ordinary one.
|
||||
|
||||
## How it is checked
|
||||
|
||||
`internal/catalogue/endpoints_setting_test.go`: a block says port, label and reach; a block may say
|
||||
only a reach; a name the module does not declare is refused; a reach outside the four values is
|
||||
refused; and saying the same thing twice — once in the block, once through the older per-port keys —
|
||||
is refused rather than resolved by whichever is read last. The proxy's half is `policy_test.go` and
|
||||
`authority_test.go`: a name the mesh did not send is not certified, by either authority.
|
||||
|
||||
## What is left, and it is not this
|
||||
|
||||
The older keys (`ports`, `expose`, and reach keyed by port) still work beside the block. They are
|
||||
what the block replaces, and retiring them is its own small change — not a gap in what reach can
|
||||
say.
|
||||
@@ -74,3 +74,18 @@ every future change of this shape, and it was paid today.
|
||||
argues for the former.
|
||||
- Does the same gap apply to the launcher and the units beside the binary, which are also files no
|
||||
declaration names?
|
||||
|
||||
## What now waits on this (2026-09-30)
|
||||
|
||||
[Issue 107](../107-a-declaration-carries-no-order/00-report.md) — a declaration carries no order, so a
|
||||
host cannot tell an older one from a newer. Its fix adds a field to the declaration, and a host refuses a
|
||||
declaration carrying a field it does not know, **whole**. So it is a flag day: every host upgraded, then
|
||||
the controller.
|
||||
|
||||
With nothing delivering a host, that means placing a binary by hand on every machine and unparking the
|
||||
adopted one, and a machine missed in the sequence is a machine the mesh cannot send anything to at all.
|
||||
107 is held for that reason rather than for anything in its own diagnosis.
|
||||
|
||||
**This is what makes a declaration field cost a rollout instead of an expedition**, which is a use for
|
||||
this record beyond keeping machines current: it is the thing standing between the mesh and its own
|
||||
protocol evolving.
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
# 142 — the mesh can now build and publish its own host
|
||||
|
||||
*2026-09-30. Both of ADR 0141's named gaps are closed; the host is not yet placed by the mesh.*
|
||||
|
||||
## What ADR 0141's insight named, and what each cost
|
||||
|
||||
> 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.
|
||||
|
||||
**1. Nothing could compile it.** The toolchain list was a closed set of typescript and python, and its
|
||||
own warning — every language is another implementation of the contracts modules share, so adding one
|
||||
commits to keeping N implementations in step — does not attach to Go. Go is how the host, the control
|
||||
plane and the builder are written, and none of them is a module in that sense: the host is what
|
||||
*applies* modules.
|
||||
|
||||
Closed by a Go toolchain naming a new base module, `mesh-tools-go`. Named and not pinned, so the mesh
|
||||
answers with the copy it holds and moving compiler is a build rather than an edit to the control
|
||||
plane's source ([ADR 0044](../../02-DECISIONS/0044-a-public-name-is-provisioned-like-any-capability.md),
|
||||
[0142](../../02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md)).
|
||||
|
||||
**2. A version could not reach the path.** An archive named a fixed path and nothing interpolated the
|
||||
build into it, so nothing could ask for `…/versions/<version>/`.
|
||||
|
||||
Closed by `${version}` in any value of a resource that uses an archive or a bundle. **The version is
|
||||
the artifact's digest, short, and not the commit**: two builds of one commit are meant to be the same
|
||||
bytes — the toolchains are `-trimpath` for that — so a content-addressed version means an unchanged
|
||||
build resolves to the path it already had, where a commit-named path would move for an identical binary
|
||||
and recreate everything reading it.
|
||||
|
||||
## Three more things were in the way, and none was in the record
|
||||
|
||||
Found by doing it, in the order they appeared:
|
||||
|
||||
- **`sourcesFor` turned every entrypoint into a `.ts` file.** One language's file extension, written
|
||||
into the code that serves every language. The extension is the toolchain's now.
|
||||
- **The output directory was left to the compiler.** `tsc --outDir` makes one; `go build -o` writes into
|
||||
a directory and does not create it, failing with a message about a path rather than about a build.
|
||||
Made for every toolchain, because which compilers are forgiving is not something a reader should have
|
||||
to know.
|
||||
- **A bundle was refused if it named what it is built from.** The reason — a bundle is the module's own
|
||||
directory compiled whole — holds for an interpreted language and cannot hold for a compiled one: a Go
|
||||
repository carries several commands, the host and its bootstrap among them, and "the module's own
|
||||
directory" is then not a package at all. A compiled bundle may now say which package; the refusal
|
||||
stands for every interpreted one.
|
||||
|
||||
And a fourth, in the base module itself: its first Dockerfile ran `apt-get`, and the golang image the
|
||||
mesh holds is Alpine. That is
|
||||
[issue 136](../136-a-module-may-name-a-program-the-machine-does-not-have/00-report.md) in an image
|
||||
rather than on a machine, and the build refused rather than a module failing later — which is the
|
||||
behaviour that issue wants.
|
||||
|
||||
## Measured: the mesh compiles its own host and publishes it to its own registry
|
||||
|
||||
```
|
||||
$ mesh-controller build <the host's repository>
|
||||
host-arch bundle artifact-store://mesh-host/host-arch/blobs/sha256:ad62528c…
|
||||
|
||||
mesh-host 1, built on novox from b5196e97
|
||||
```
|
||||
|
||||
Fetched from the mesh's registry and opened:
|
||||
|
||||
```
|
||||
mesh-host: ELF 64-bit LSB executable, x86-64, statically linked, stripped
|
||||
$ ./mesh-host help
|
||||
mesh-host — the node host
|
||||
```
|
||||
|
||||
Statically linked matters: what a machine holds is a file rather than a container, so a binary needing
|
||||
a libc it did not bring is a delivery that works until a machine differs.
|
||||
|
||||
**The host is a module now**, with one bundle for `arch` built from `cmd/mesh-host`. A system is
|
||||
required for a compiled artifact because a binary is pinned at link time so a host refuses to touch a
|
||||
machine it was not built for ([ADR 0005](../../02-DECISIONS/0005-the-node-host.md)); `arch` is what all
|
||||
four of this mesh's machines report themselves to be, and another system is another artifact and
|
||||
another build.
|
||||
|
||||
## What is left
|
||||
|
||||
**The host module declares no resources**, so nothing places the built bundle on a machine yet. That is
|
||||
the next piece and it is the one with the interesting question in it: the resource is an archive
|
||||
unpacked to `…/versions/${version}/`, applied by the host that is running, and the bootstrap is not
|
||||
circular because the two are different versions ([ADR 0141](../../02-DECISIONS/0141-the-host-delivers-its-own-successor.md)).
|
||||
The host half of that mechanism — versions side by side, the newest runs, the running one stands aside
|
||||
between reconciles, rollback picks a directory — is built and tested and has never had a version to
|
||||
work on.
|
||||
|
||||
**One thing worth noticing while writing it.** The systems list exists because "the difference between
|
||||
two of them is a C library, not a kernel" — and a static Go binary has no C library. So one build would
|
||||
in fact run on all three. The pin is then a policy (a host refuses a machine it was not built for)
|
||||
rather than a necessity, which is a reasonable thing to keep and is worth knowing is a choice.
|
||||
|
||||
**And a sharper one, asked as a question and worth its own record.** The artifact's system is validated
|
||||
and then read by nothing: it does not reach the compiler, no machine is matched against it, and nothing
|
||||
chooses between two artifacts by it. The host built here is x86-64 because the build machine is, not
|
||||
because anything in the declaration said so — correct for this mesh by coincidence. That is
|
||||
[issue 159](../159-an-artifacts-system-is-checked-and-then-ignored/00-report.md).
|
||||
|
||||
## Delivered, started, and one fact short (2026-09-30, later)
|
||||
|
||||
The loop closed. The launcher is delivered as a **file** resource rather than inside the archive, and
|
||||
that difference is the safety: a file is written atomically — temp file, then rename — so the running
|
||||
launcher keeps the inode it was started from, where an archive writes in place with truncate and would
|
||||
cut the script a running shell is reading. The manifest carries a second copy of the launcher and a
|
||||
test refuses any difference from `packaging/nox-mesh-host-launch`.
|
||||
|
||||
On the workstation, in order: the version landed, the launcher was replaced, the running host saw a
|
||||
delivered version and stood aside, and after one restart of the unit the launcher started
|
||||
`/usr/lib/nox-mesh-host/versions/637f65559d16/nox-mesh-host`. **A host the mesh compiled, published,
|
||||
delivered and started.**
|
||||
|
||||
It would then have refused the first declaration it was asked to apply. The host's Makefile links in
|
||||
two facts the mesh's toolchain does not, and one of them — the system it was built for — is read before
|
||||
anything is applied. That is
|
||||
[issue 161](../161-a-delivered-host-carries-none-of-its-link-time-facts/00-report.md), and the machine
|
||||
is back on its hand-placed binary until it is answered.
|
||||
|
||||
**The fallback is what made that safe**, and it was not luck: the launcher runs the pinned version, or
|
||||
the newest delivered one, or the one placed by hand — so moving the delivered versions aside restored
|
||||
the machine in one step.
|
||||
|
||||
## Self-update works (2026-09-30, end of the day)
|
||||
|
||||
```
|
||||
running: /usr/lib/nox-mesh-host/versions/093231796eb0/nox-mesh-host
|
||||
mesh-controller node show shanks
|
||||
host 093231796eb0
|
||||
```
|
||||
|
||||
One machine runs a host the mesh compiled, published, delivered and started, applying declarations and
|
||||
reporting the version it was delivered as. The two facts a delivered binary was missing are
|
||||
[issue 161](../161-a-delivered-host-carries-none-of-its-link-time-facts/01-resolution.md) and resolved:
|
||||
the system comes from the artifact, the version from where the binary sits.
|
||||
|
||||
**Three machines still run a hand-placed host**, and rolling each forward is one assignment and one
|
||||
push. The control node is worth last.
|
||||
|
||||
One thing this found on the way out: an archive cannot be undeclared, and the attempt stops the machine
|
||||
applying anything at all — [issue 162](../162-an-archive-cannot-be-undeclared/00-report.md). It is how
|
||||
undoing the first delivery froze the workstation, and it is not specific to the host.
|
||||
|
||||
## Every machine self-updates (2026-09-30, evening)
|
||||
|
||||
```
|
||||
shanks 76f4566bef3d/nox-mesh-host active
|
||||
g14 76f4566bef3d/nox-mesh-host active
|
||||
novox 76f4566bef3d/nox-mesh-host active
|
||||
ace 76f4566bef3d/nox-mesh-host active
|
||||
|
||||
mesh-controller status: (no host split)
|
||||
```
|
||||
|
||||
The last delivery was unattended on all four: the fixed host was built, pushed, each machine stood
|
||||
aside exactly once for the genuinely newer version, and the delivered launcher started it — no
|
||||
restart by hand. A following push that delivered nothing new was applied and reported by every
|
||||
machine and stood nobody aside, which is the check
|
||||
[issue 163](../163-a-delivered-host-stood-aside-on-every-push-and-reported-nothing/00-report.md) asks
|
||||
for.
|
||||
|
||||
**Two more faults on the way, both mine, both found by reading the machine rather than the success
|
||||
line.** A delivered host compared the newest delivered version against its link-time stamp rather
|
||||
than the version it was running, so it stood aside on every push and — because standing aside cancels
|
||||
the report — never reported again (163). And the adopted machine kept its found launcher as the
|
||||
adoption rule says, so the delivery there needed a `take` before the launcher moved.
|
||||
|
||||
**The crossover needs one restart of the unit per machine, once.** The launcher process that was
|
||||
running on each machine was the old script, executing from its own inode; a new file beside it
|
||||
changes nothing until the unit restarts. Every subsequent delivery is unattended.
|
||||
|
||||
**Timing, measured:** on a machine, hearing a declaration to reporting it applied is about three
|
||||
seconds. A push as the operator sees it takes 17–20 seconds, and the difference is the control plane
|
||||
composing the declaration before it sends. A `--wait` shorter than that reads as "did not report" for
|
||||
a machine that did; the three-minute default read as slowness for a machine that never would. Neither
|
||||
number is a defect being chased here, and both are worth knowing before reading a push's answer.
|
||||
|
||||
## What this leaves
|
||||
|
||||
- [Issue 162](../162-an-archive-cannot-be-undeclared/00-report.md): an archive cannot be undeclared, so
|
||||
the host module — and any module with an archive — cannot be unassigned, and trying stops the machine
|
||||
applying anything.
|
||||
- [Issue 107](../107-a-declaration-carries-no-order/00-report.md) is unblocked: a declaration field is
|
||||
now a build and a push rather than an expedition.
|
||||
- Three stale version directories on the workstation from the first attempts, moved aside under
|
||||
`/var/lib/mesh-host/versions-held-back/`, and a backup of the adopted machine's hand-placed binary
|
||||
beside its state. Both are safe to delete and are not the mesh's to delete.
|
||||
+1
-1
@@ -4,7 +4,7 @@ opened: 2026-09-29
|
||||
located-in:
|
||||
- mesh-controller internal/catalogue/filtering.go (fixed for this instance)
|
||||
- mesh-controller (what status reports, and what it does not ask)
|
||||
fixed-by:
|
||||
fixed-by: partly — mesh-controller 1da96e8 makes the report state its own scope; nothing dials a provision yet, which is ADR 0146 and is not built
|
||||
amended-design: 03-DESIGN/01-to-be/10-delivery.md
|
||||
---
|
||||
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
# 145 — partly resolved: the report says what it is not a claim about
|
||||
|
||||
*2026-09-30.*
|
||||
|
||||
## What was done
|
||||
|
||||
The sentence that was true for eleven hours now states its own scope, immediately below itself:
|
||||
|
||||
```
|
||||
4 machine(s), all doing what they were told, all heard from, running what the mesh would send
|
||||
them, and every module current with its source
|
||||
|
||||
That is the mesh and the machines agreeing. Nothing here dials a provision:
|
||||
no grant the mesh composed has been tested, so a module unable to reach what it
|
||||
requires would not appear above (04-ISSUES/145)
|
||||
```
|
||||
|
||||
That is the whole of what this change does, and it is deliberately small. It does not check anything.
|
||||
It closes the distance between *the machines are as the mesh described them* and *it works* by naming
|
||||
it, and that distance is where the eleven hours went: the report was read as the second and only ever
|
||||
meant the first.
|
||||
|
||||
Two other reports in this group now carry real information they did not
|
||||
([issue 125](../125-a-hold-is-not-a-line-in-the-apply-report/00-report.md): a hold is a line in the
|
||||
apply report and breaks the all-well sentence;
|
||||
[issue 087](../087-the-controller-cannot-tell-a-host-is-too-old/00-report.md): the mesh knows which
|
||||
host runs a machine). Neither would have caught this fault, and both were the same shape of blindness.
|
||||
|
||||
`printStatus` is now separated from the asking, so these words can be read by a test with no store, bus
|
||||
or machine — they have been acted on and been misleading twice, which makes them worth holding still.
|
||||
|
||||
## What is NOT done, and why this record stays open
|
||||
|
||||
**Nothing dials a provision.** [ADR 0146](../../02-DECISIONS/0146-connectivity-is-checked-by-name-per-hosting-form.md)
|
||||
decides how it should be done — a module on every machine serving an endpoint of its own and dialling
|
||||
every other machine's, one name per hosting form, over TLS with the certificate verified. **Nothing is
|
||||
built**, the module that existed was deleted, and the work was deferred deliberately by the operator.
|
||||
It is not this record's to start.
|
||||
|
||||
So the measured fault of this issue — that the mesh can only report on itself — is unchanged. What
|
||||
changed is that the report no longer implies otherwise.
|
||||
|
||||
## The open questions, where they stand
|
||||
|
||||
- *Should a grant be checked, and from where?* Answered by ADR 0146 and not built: from the position
|
||||
the callers are in, by a module on every machine, per hosting form.
|
||||
- *What would it cost to be wrong in the other direction?* Unanswered and important. A check that
|
||||
reports a provision broken while it works trains a reader to ignore the report, which is the failure
|
||||
this whole issue is about arriving by the other door.
|
||||
- *What should `status` say about a machine whose modules cannot reach each other?* Answered in part:
|
||||
until something checks, it says that it has not checked. What it says when a check exists is ADR
|
||||
0146's to settle.
|
||||
- *Is there a cheaper signal than a probe?* Still open. The affected module logged the failure 6,154
|
||||
times; the mesh reads no module's logs and arguably should not, but something a module could *say*
|
||||
about its own provisions would have surfaced this in minutes.
|
||||
- *Does the same blindness apply to a provider that lost a consumer's grant?* Still open, and still
|
||||
nothing checks it.
|
||||
|
||||
## One thing worth carrying forward
|
||||
|
||||
**The certificate half of ADR 0146 is now possible where it was not.** Its check requires an internal
|
||||
name fetched over TLS with the certificate verified, and until 2026-09-30 no machine trusted the mesh's
|
||||
authority at all ([issue 129](../129-nothing-makes-a-machine-trust-the-meshs-authority/02-resolution.md)).
|
||||
Three of four do now. Whoever builds 0146 no longer has to solve that first.
|
||||
+12
@@ -71,3 +71,15 @@ against a suite nobody can execute.
|
||||
- The lab rewrites a bundle's registry-prefixed third-party references to upstream ones for a
|
||||
machine with an uplink (`test/integration/harness.ts`); the new bundle's bus reference needed
|
||||
that rule added, which is done and is not this issue.
|
||||
|
||||
## What one of its fixes then did to a running mesh (2026-09-30)
|
||||
|
||||
The change that stopped the doubling — putting the stream into a push consumer's delivery subject —
|
||||
is correct on a foundation being raised and fatal on a mesh that is already running: the server will
|
||||
not move that subject while a subscriber is bound, and a node is bound to its declaration consumer
|
||||
the whole time it is up. The control plane crash-looped on the first build that carried it.
|
||||
|
||||
Recorded and fixed as [issue 156](../156-moving-a-consumers-delivery-subject-stops-the-control-plane/00-report.md).
|
||||
Noted here because this record is where somebody will arrive when reading why the subject carries the
|
||||
stream at all, and the answer is incomplete without it: **the raise path was the only one exercised,
|
||||
and it is the one path on which nothing is bound.**
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
---
|
||||
status: located
|
||||
status: resolved
|
||||
opened: 2026-09-29
|
||||
located-in: [nothing in the mesh — the surface in use is the predecessor's, installed on the workstation]
|
||||
located-in: [mesh-catalog modules/mesh-console, mesh-tools src/mesh.ts, mesh-controller internal/broker]
|
||||
fixed-by: ADR 0152; mesh-controller PR 164 (invokes); mesh-tools PR 20 (mesh serve, the tools verb); mesh-catalog PR 181 (mesh-console)
|
||||
amended-design: 03-DESIGN/01-to-be/34-the-console.md
|
||||
---
|
||||
|
||||
# 147 — the operator's tools still dial the bus that was removed
|
||||
@@ -73,3 +75,28 @@ outside the mesh.
|
||||
- It fails identically for the local machine, which rules out reachability and points at the
|
||||
transport alone.
|
||||
- The mesh's own traffic over the new bus is unaffected: nodes report, declarations apply.
|
||||
|
||||
## Answered, 2026-09-30
|
||||
|
||||
The work order's question — does the mesh grow its own operator surface, or is the surface an
|
||||
ordinary module — is answered by [ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md):
|
||||
**the console is a module.** `mesh-console` is assigned to the machine a person sits at, holds a
|
||||
credential the mesh minted, calls tools under a grant its manifest declares (`invokes`), and serves the
|
||||
mesh's tools on that machine's loopback to an agent over MCP and to a person through the same endpoint.
|
||||
Designed in [34 — The console](../../03-DESIGN/01-to-be/34-the-console.md).
|
||||
|
||||
What that leaves, said plainly so nobody reads this record as closed on the whole of its first
|
||||
paragraph: the console reaches every tool a *module* serves. The mesh's own questions — what a node
|
||||
runs, what is assigned — are the `mesh-controller` seat's tools under ADR 0132 and are not on the bus
|
||||
yet; for those a shell is still the way, and design 33 is where that closes.
|
||||
|
||||
The predecessor's program on the workstation is not replaced by the mesh; it is left where it is and
|
||||
the assistant is pointed at the console beside it. The `hal` entry in the assistant's configuration
|
||||
still names things that are not the mesh's.
|
||||
|
||||
**Verified live, 2026-09-30 evening.** The four pull requests merged; the console was registered
|
||||
(checked first with `module check`), built, assigned to a workstation, issued a bus account, and pushed.
|
||||
On that machine `tools/list` answered on loopback with 62 tools and named 36 modules as not answering,
|
||||
and a call to the forge's `gitea_list_repos` returned repositories. The assistant on that machine now
|
||||
lists the console as a connected MCP server beside the predecessor's program, which was left where it
|
||||
is. As-is: [`13-the-console.md`](../../03-DESIGN/00-as-is/13-the-console.md).
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
---
|
||||
status: open
|
||||
status: resolved
|
||||
opened: 2026-09-29
|
||||
located-in: [mesh-controller cmd/mesh-controller]
|
||||
located-in: [mesh-controller cmd/mesh-controller, mesh-controller internal/catalogue]
|
||||
fixed-by: mesh-controller PR 164 (module check)
|
||||
amended-design: 03-DESIGN/01-to-be/12-a-module-repository.md
|
||||
---
|
||||
|
||||
# 148 — a manifest outside this catalogue has no check
|
||||
@@ -35,3 +37,14 @@ Nothing prevents this; it was noticed and left. ADR 0037 named it on 2026-09-01
|
||||
take a path from `MESH_CATALOG`, so the mechanism is already path-driven and not repository-bound.
|
||||
- `mesh-controller module add` refuses a bad manifest at registration, which is the same check far
|
||||
too late: by then it is in a running mesh's records.
|
||||
|
||||
## Built, 2026-09-30
|
||||
|
||||
`mesh-controller module check <manifest>…` runs what registration runs — the strict parse, the
|
||||
per-manifest problems, and the cross-manifest rules over every manifest given — with no store and no
|
||||
mesh, prints every problem in the manifest's words, and exits non-zero on any. What it cannot judge
|
||||
without a store it says: a claim on one of the mesh's own seats is judged fully only at registration
|
||||
([ADR 0122](../../02-DECISIONS/0122-a-seat-is-data-a-rename-is-a-database-update.md)), and a seat
|
||||
declared by a module whose manifest was not passed reads as unknown. Written into
|
||||
[12 — A module repository](../../03-DESIGN/01-to-be/12-a-module-repository.md) as the section *a
|
||||
manifest is checked where it is written*. The console's own manifest was the first checked with it.
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-09-29
|
||||
located-in:
|
||||
- mesh-controller internal/catalogue/declaration.go (contributions are emitted for an assigned module, taken or not)
|
||||
fixed-by:
|
||||
---
|
||||
|
||||
# 150 — A route is contributed before its module is taken
|
||||
|
||||
## What was observed
|
||||
|
||||
ace is adopted and runs `route-adapter` beside the predecessor's traefik (ADR 0104). The first web
|
||||
module migrated there was searxng. `assign ace searxng` + `push ace` — the step that is supposed to
|
||||
change nothing on an adopted machine (ADR 0100, hq 125: assign holds, take replaces) — took
|
||||
`searxng.zurag.be` down:
|
||||
|
||||
```
|
||||
https://searxng.zurag.be/ → 502
|
||||
```
|
||||
|
||||
for about five minutes, until the module was unassigned again.
|
||||
|
||||
## Why
|
||||
|
||||
Assign held everything it found on the machine — the predecessor's `searxng` container, its
|
||||
directories — exactly as designed. But the module's **route contribution** is not a resource on the
|
||||
machine, so nothing held it: it reached `route-adapter` at once, which wrote
|
||||
`mesh-searxng.zurag.be.yml` into traefik's file provider pointing at the mesh's assigned machine port
|
||||
(`http://ace.internal:20000`) — where nothing listened, because the container that would was held.
|
||||
traefik's file router for the name then won over the predecessor's docker-label router for the same
|
||||
name, and the name served a dead backend.
|
||||
|
||||
On this occasion the window was lengthened by an unrelated failure (the machine's docker address
|
||||
pools were exhausted, so the module's network could not be created), but the fault does not depend on
|
||||
it: **between assign and take, every routed module's public name points at a backend the mesh has
|
||||
deliberately not started.** The runbook's §6 step 2 ("check the preparation — HAL's service still
|
||||
serves") is false for every routed module on a node running route-adapter.
|
||||
|
||||
## What the operator did
|
||||
|
||||
Rolled back per runbook (unassign before take), then re-ran as assign → push → wait for the node to
|
||||
report its holds → take → push, keeping the window to the ~10 s between the two pushes. A workaround,
|
||||
not a fix: a module whose data must move between assign and take (runbook §6 step 3) cannot shrink
|
||||
its window this way.
|
||||
|
||||
## What would be right
|
||||
|
||||
A contribution from a module that is assigned but **not taken** on an adopted node should be held like
|
||||
the module's resources are — withheld from the provider until take — or the provider should be told
|
||||
the contributor is held and leave the predecessor's route alone. Either keeps "assign changes nothing"
|
||||
true for routed modules.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user