Renumber the records 1 to 23

The consolidation left a sparse sequence -- 1, 4, 6, 7, 9, 10, 12, 15, 16, 18,
19, 25, 34, 35, 36, 37, 40, 42, 44, 45, 48, 49, 58 -- where the gaps were only
the archaeology of what used to be there.

Renumbered contiguously. Renames run in ascending order, so every target number
is already free and no two files ever collide.

The reference rewrite is one simultaneous pass rather than a sequence of
replacements. Numbers moved into slots other numbers were vacating -- the node
host went 37 to 16 while the lab went 16 to 9 -- so replacing one at a time
would have cascaded and silently pointed things at the wrong record.

Seven plain-text references survived the merges as prose rather than links,
naming records that no longer existed: the enrolment token, the link boundary,
what a declaration is, reachability, the repository structure. Each mapped to
the consolidated record that now holds it.

Verified rather than assumed: every [ADR NNNN](path) link now has matching text
and target, checked across the whole repository, and the checker passes.

Frontmatter `consolidates:` lists dropped -- they named records that are gone,
and each consolidated record already says in prose what it absorbed.
This commit is contained in:
2026-08-28 23:28:34 +02:00
parent 77f3a4cea7
commit e1febe8e0f
84 changed files with 441 additions and 449 deletions
+2 -2
View File
@@ -3,12 +3,12 @@ layer: to-be
status: designed
code: [hal]
updated: 2026-08-23
decisions: [02-DECISIONS/0015-mesh-brokers-nodes-host-agents-think.md]
decisions: [02-DECISIONS/0008-mesh-brokers-nodes-host-agents-think.md]
---
# Work breakdown — the decomposition
How [ADR 0015](../../02-DECISIONS/0015-mesh-brokers-nodes-host-agents-think.md) gets built, in what order, and where a human must look.
How [ADR 0008](../../02-DECISIONS/0008-mesh-brokers-nodes-host-agents-think.md) gets built, in what order, and where a human must look.
Ordering is not preference. Each phase removes a constraint the next one needs gone.
+5 -5
View File
@@ -4,9 +4,9 @@ status: in-progress
code: [mesh-lab]
updated: 2026-08-23
decisions:
- 02-DECISIONS/0016-the-lab.md
- 02-DECISIONS/0016-the-lab.md
- 02-DECISIONS/0019-how-this-repository-works.md
- 02-DECISIONS/0009-the-lab.md
- 02-DECISIONS/0009-the-lab.md
- 02-DECISIONS/0011-how-this-repository-works.md
---
# End-to-end testing
@@ -37,7 +37,7 @@ today, that is a gap in the vocabulary rather than a reason to privilege that sh
The design below describes a scenario as a complete mesh — forge (Gitea), coordinator,
delivery cascade — because what it tests is a module. **That is the larger of two classes, and
not the first one built** ([ADR 0016](../../02-DECISIONS/0016-the-lab.md)).
not the first one built** ([ADR 0009](../../02-DECISIONS/0009-the-lab.md)).
| | **Bootstrap scenario** | **Full scenario** |
|---|---|---|
@@ -127,7 +127,7 @@ drifts.
a mesh named by the request instead.
- **Scenarios must be concurrent and cheap.** Several agents working means several scenarios
at once, each needing its own network and nodes. A lab node is a virtual machine
([ADR 0016](../../02-DECISIONS/0016-the-lab.md)), and snapshots are
([ADR 0009](../../02-DECISIONS/0009-the-lab.md)), and snapshots are
what make repetition cheap — restoring a scenario costs far less than building one. The
earlier argument here, that only system containers made this affordable, was superseded: the
scale it assumed was invented rather than required.
+10 -10
View File
@@ -4,9 +4,9 @@ status: in-progress
code: [mesh-lab]
updated: 2026-08-25
decisions:
- 02-DECISIONS/0016-the-lab.md
- 02-DECISIONS/0016-the-lab.md
- 02-DECISIONS/0016-the-lab.md
- 02-DECISIONS/0009-the-lab.md
- 02-DECISIONS/0009-the-lab.md
- 02-DECISIONS/0009-the-lab.md
---
# The scenario declaration
@@ -15,7 +15,7 @@ A scenario is a **declaration of an underlay**, plus what to put on it. It is th
everything in the lab hangs off, so it is worth getting small.
It states what a hosting provider and a home router would provide, and nothing the mesh is
responsible for ([ADR 0016](../../02-DECISIONS/0016-the-lab.md)).
responsible for ([ADR 0009](../../02-DECISIONS/0009-the-lab.md)).
## Public networks are unrelated, and routed rather than bridged
@@ -258,7 +258,7 @@ otherwise explicit declaration, and it exists because NAT has to run somewhere.
It is a **container, not a virtual machine** — a router is scenery rather than something under
test, so the fidelity argument that makes a node a virtual machine does not reach it
([ADR 0016](../../02-DECISIONS/0016-the-lab.md)). What a router must
([ADR 0009](../../02-DECISIONS/0009-the-lab.md)). What a router must
reproduce is kernel behaviour, and a container has the same kernel.
**`machines[].at`** — segment and addresses, or a **list** of them for a machine on several
@@ -299,7 +299,7 @@ belongs to a router it does not control, and asleep.
Whether the overlay survives that, re-forms, and is noticed to have changed endpoint is
**observed**, never arranged
([ADR 0016](../../02-DECISIONS/0016-the-lab.md)).
([ADR 0009](../../02-DECISIONS/0009-the-lab.md)).
## Why the addresses are load-bearing
@@ -320,13 +320,13 @@ it must be.
The format should make getting this wrong hard rather than merely documented: a segment without
a `gateway:` is a public segment, and an address in it — including a gateway's `address:` — that
is not documentation space is a declaration error, refused before anything is raised. That is
[ADR 0058](../../02-DECISIONS/0058-delivery.md) applied to a configuration
[ADR 0023](../../02-DECISIONS/0023-delivery.md) applied to a configuration
file: the failure it prevents is silent, so the check has to be loud.
## The same declaration serves both classes
The bootstrap and full scenarios differ **only in `place:`**
([ADR 0016](../../02-DECISIONS/0016-the-lab.md)). Everything
([ADR 0009](../../02-DECISIONS/0009-the-lab.md)). Everything
about the underlay is identical, which is what makes one a strict subset of the other rather
than a fork.
@@ -354,7 +354,7 @@ not first.
## What a scenario deliberately cannot say
- **Overlay addresses, the hub, peer configuration.** Outcomes, not inputs
([ADR 0016](../../02-DECISIONS/0016-the-lab.md)).
([ADR 0009](../../02-DECISIONS/0009-the-lab.md)).
- **What a machine is in mesh terms** — server or workstation, its site, its names. Mesh
configuration, established by the mesh.
- **A host's capability profile.** Detected, never declared.
@@ -543,7 +543,7 @@ cannot yet express.
Nothing here mentions overlay addresses, which node is the hub, who peers with whom, any name,
or any certificate. Research 004 recorded all of those for this topology, and **a scenario must
not state them** ([ADR 0016](../../02-DECISIONS/0016-the-lab.md)): they
not state them** ([ADR 0009](../../02-DECISIONS/0009-the-lab.md)): they
are what the mesh does, and a scenario that supplied them would be certifying its own work.
The absence is the point. Given the declaration above, whether a hub is elected, whether the
+7 -7
View File
@@ -4,16 +4,16 @@ status: in-progress
code: [mesh-lab]
updated: 2026-08-25
decisions:
- 02-DECISIONS/0016-the-lab.md
- 02-DECISIONS/0016-the-lab.md
- 02-DECISIONS/0016-the-lab.md
- 02-DECISIONS/0009-the-lab.md
- 02-DECISIONS/0009-the-lab.md
- 02-DECISIONS/0009-the-lab.md
---
# Scenario lifecycle
The first thing the lab must do, and the only thing it must do before anything else can be
written: **materialise a mesh, return it to a known state, and destroy it**
([ADR 0016](../../02-DECISIONS/0016-the-lab.md)).
([ADR 0009](../../02-DECISIONS/0009-the-lab.md)).
A [declaration](02-scenario-declaration.md) describes a scenario. This describes what happens
to one.
@@ -39,7 +39,7 @@ The order is not arbitrary — each step needs the one before it to exist:
1. **Segments.** Isolated links, one per declared segment, belonging to this instance and
joined to nothing outside it
([ADR 0016](../../02-DECISIONS/0016-the-lab.md)).
([ADR 0009](../../02-DECISIONS/0009-the-lab.md)).
2. **Gateways.** Derived, never declared as machines: a gateway is materialised for each
distinct `gateway:` declaration, sitting on both its segment and its parent, carrying the
translation, forwarding and mapping-expiry the declaration asked for.
@@ -60,7 +60,7 @@ habit.
## A failed raise leaves the wreckage
A step that fails stops the raise
([ADR 0058](../../02-DECISIONS/0058-delivery.md)) — and **does not tear
([ADR 0023](../../02-DECISIONS/0023-delivery.md)) — and **does not tear
down**.
Tearing down on failure destroys the only evidence of what went wrong, which is precisely
@@ -100,7 +100,7 @@ made after it, and returning undoes it like any other change.
## Reaching in
Everything the lab does to a machine goes through the virtualisation layer, never over IP
([ADR 0016](../../02-DECISIONS/0016-the-lab.md)). `exec` runs a
([ADR 0009](../../02-DECISIONS/0009-the-lab.md)). `exec` runs a
command on a machine and returns its output.
This has one consequence worth stating plainly: **a reachability question is asked from inside**.
+4 -4
View File
@@ -4,15 +4,15 @@ status: designed
code: [mesh-lab]
updated: 2026-08-24
decisions:
- 02-DECISIONS/0016-the-lab.md
- 02-DECISIONS/0058-delivery.md
- 02-DECISIONS/0009-the-lab.md
- 02-DECISIONS/0023-delivery.md
---
# Installing the lab on a clean machine
The lab has prerequisites — a virtualisation daemon, copy-on-write storage, a pool, an identity
permitted to talk to it — and it cannot get them from the mesh, because it is where the mesh is
built ([ADR 0016](../../02-DECISIONS/0016-the-lab.md)).
built ([ADR 0009](../../02-DECISIONS/0009-the-lab.md)).
So the lab needs an install path of its own. This describes it, and the shape it has to take is
determined by two failures observed while measuring
@@ -55,7 +55,7 @@ and unbounded at worst.
**The lab refuses to run degraded.** It does not warn and continue: a warning about a slow inner
loop is read once and ignored forever, and the loop stays slow. This is
[ADR 0058](../../02-DECISIONS/0058-delivery.md) applied where the failure is
[ADR 0023](../../02-DECISIONS/0023-delivery.md) applied where the failure is
performance rather than an error.
## Two ways the prerequisites arrive
+26 -26
View File
@@ -4,17 +4,17 @@ status: in-progress
code: [mesh-host]
updated: 2026-08-27
decisions:
- 02-DECISIONS/0019-how-this-repository-works.md
- 02-DECISIONS/0036-a-node-and-how-it-joins.md
- 02-DECISIONS/0037-the-node-host.md
- 02-DECISIONS/0036-a-node-and-how-it-joins.md
- 02-DECISIONS/0036-a-node-and-how-it-joins.md
- 02-DECISIONS/0058-delivery.md
- 02-DECISIONS/0037-the-node-host.md
- 02-DECISIONS/0037-the-node-host.md
- 02-DECISIONS/0037-the-node-host.md
- 02-DECISIONS/0037-the-node-host.md
- 02-DECISIONS/0037-the-node-host.md
- 02-DECISIONS/0011-how-this-repository-works.md
- 02-DECISIONS/0015-a-node-and-how-it-joins.md
- 02-DECISIONS/0016-the-node-host.md
- 02-DECISIONS/0015-a-node-and-how-it-joins.md
- 02-DECISIONS/0015-a-node-and-how-it-joins.md
- 02-DECISIONS/0023-delivery.md
- 02-DECISIONS/0016-the-node-host.md
- 02-DECISIONS/0016-the-node-host.md
- 02-DECISIONS/0016-the-node-host.md
- 02-DECISIONS/0016-the-node-host.md
- 02-DECISIONS/0016-the-node-host.md
---
# The node host
@@ -25,11 +25,11 @@ Tier 0. The one thing ever installed by hand, and the only thing that changes a
A **statically linked binary that requires nothing to be present** — copy it onto a machine and
run it, and that is the whole installation
([ADR 0037](../../02-DECISIONS/0037-the-node-host.md)). Written in Go, because the
([ADR 0016](../../02-DECISIONS/0016-the-node-host.md)). Written in Go, because the
job is system-level and because the host shares no code with any other tier.
A single binary with one job: **apply declared state on this machine**
([ADR 0037](../../02-DECISIONS/0037-the-node-host.md)). Overlay
([ADR 0016](../../02-DECISIONS/0016-the-node-host.md)). Overlay
membership, packet filtering, packages, services, containers and filesystems are not six
concerns it carries; they are six instances of the one.
@@ -61,7 +61,7 @@ returns it.
Three properties, each following a recorded decision:
**A failed step fails the apply.** Not "logs and continues"
([ADR 0058](../../02-DECISIONS/0058-delivery.md)). A partial apply that
([ADR 0023](../../02-DECISIONS/0023-delivery.md)). A partial apply that
reports success is the mesh's most expensive shape.
**Each applier reads back.** Setting a value is not evidence the value took. The firewall is
@@ -69,7 +69,7 @@ asked whether the rule loaded; conntrack is asked what timeout it holds. This is
§5 as a component requirement rather than a review habit.
**What was applied is recorded after it works, never before**
([ADR 0035](../../02-DECISIONS/0035-a-picture-is-read-from-what-runs.md)). A failed apply leaves
([ADR 0014](../../02-DECISIONS/0014-a-picture-is-read-from-what-runs.md)). A failed apply leaves
the machine in whatever state it reached, and nothing must claim otherwise.
### store
@@ -78,14 +78,14 @@ Local, and **authoritative while disconnected**. Not a cache of the control plan
of what this node has applied and what it currently holds.
This is structural rather than convenient: if disconnection is an ordinary situation rather
than an exception ([ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md)), the
than an exception ([ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md)), the
store is what makes it ordinary. A laptop shut for a week comes back and reconciles; it does
not come back and ask what it is.
### link
The node's one connection to the control plane, and its security boundary
([ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md)).
([ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md)).
It is the broker connection that already exists
([ADR 0001](../../02-DECISIONS/0001-nodes-communicate-over-a-broker.md)) — outbound,
@@ -108,7 +108,7 @@ architecture, a network position.
capability is real when it is present, running and working, and the difference is the whole
point of detecting it.
The profile is what makes [ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md)
The profile is what makes [ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md)
work: a node is a node, and what varies between them is here rather than in the definition.
### inventory
@@ -133,7 +133,7 @@ is the component; that one is what happens to it.
## Where a declaration comes from
One behaviour, two sources
([ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md)):
([ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md)):
| Situation | Source |
|---|---|
@@ -150,7 +150,7 @@ the mesh, and the full peer set arrives derived.
## What a declaration is
Settled by [ADR 0037](../../02-DECISIONS/0037-the-node-host.md).
Settled by [ADR 0016](../../02-DECISIONS/0016-the-node-host.md).
**JSON**, because the host has no dependencies to spend and the standard library carries no
YAML. **An ordered list of typed resources**, each with a stable identity — the order is stated
@@ -187,8 +187,8 @@ Raising the substrate needs six shapes in the host's vocabulary, and **all six a
| `directory`, `file` | **built** | no machine dependency at all |
| `service` | **built** | running/stopped **and** enabled/disabled at boot — a unit started but not enabled stops being true at the next reboot |
| `package` | **built** | present, never upgraded, and **never uninstalled** — the host cannot know what else needs it, so dropping one is *forgotten*, not *removed* |
| `container` | **built** | pinned by digest ([ADR 0048](../../02-DECISIONS/0048-the-substrate-and-the-control-plane.md)); identified by a label carrying a digest of the declaration that made it, because a runtime normalises what it is given and that is indistinguishable from drift |
| `action` | **built** | bundle-only ([ADR 0037](../../02-DECISIONS/0037-the-node-host.md)); verify is mandatory and is the idempotency check as well as the read-back |
| `container` | **built** | pinned by digest ([ADR 0021](../../02-DECISIONS/0021-the-substrate-and-the-control-plane.md)); identified by a label carrying a digest of the declaration that made it, because a runtime normalises what it is given and that is indistinguishable from drift |
| `action` | **built** | bundle-only ([ADR 0016](../../02-DECISIONS/0016-the-node-host.md)); verify is mandatory and is the idempotency check as well as the read-back |
**The parser enforces the boundary rather than the caller remembering it.** `Parse` refuses an
action and is what the link uses; `ParseTrusted` permits one and is what the bundle uses. The
@@ -205,7 +205,7 @@ until it is done the substrate bootstrap has no end-to-end test.
**3 — link and store.** The node connects, receives declarations, and holds what it applied.
**4 — enrolment.** The one genuinely new mechanism in
[ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md); everything else there
[ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md); everything else there
is configuration of what already runs.
Stage 1 is deliberately the smallest useful thing. The lab currently raises **empty machines**
@@ -216,7 +216,7 @@ that, and every later stage is tested by a lab that already works.
**The lab is the harness.** A scenario places a host on a machine and asserts what it did —
against a real hypervisor, with the boundary never mocked
([ADR 0034](../../02-DECISIONS/0034-a-test-defends-a-decision.md)).
([ADR 0013](../../02-DECISIONS/0013-a-test-defends-a-decision.md)).
Each decision above owes a test:
@@ -239,7 +239,7 @@ Each decision above owes a test:
package, a unit, a container and a dataset *are*. Nothing has measured that surface, and it is
the residue of the question [`host-size.md`](../../01-RESEARCH/006-mesh-from-scratch/host-size.md)
answered.
- **Rescue.** [ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md) suggests it is
- **Rescue.** [ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md) suggests it is
a node whose local state is discarded so the mesh re-derives it, and does not decide it.
- **What may expire.** An identity needing refresh to stay valid would make a laptop fail for
being a laptop ([ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md)).
being a laptop ([ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md)).
+25 -25
View File
@@ -4,11 +4,11 @@ status: designed
code: []
updated: 2026-08-27
decisions:
- 02-DECISIONS/0037-the-node-host.md
- 02-DECISIONS/0048-the-substrate-and-the-control-plane.md
- 02-DECISIONS/0019-how-this-repository-works.md
- 02-DECISIONS/0015-mesh-brokers-nodes-host-agents-think.md
- 02-DECISIONS/0048-the-substrate-and-the-control-plane.md
- 02-DECISIONS/0016-the-node-host.md
- 02-DECISIONS/0021-the-substrate-and-the-control-plane.md
- 02-DECISIONS/0011-how-this-repository-works.md
- 02-DECISIONS/0008-mesh-brokers-nodes-host-agents-think.md
- 02-DECISIONS/0021-the-substrate-and-the-control-plane.md
---
# The control plane
@@ -24,7 +24,7 @@ This document defines it. It does **not** design the contexts inside it; those a
> **The control plane is everything that needs to know about more than one node.**
That is the whole test, and it is not arbitrary — it follows from
[ADR 0037](../../02-DECISIONS/0037-the-node-host.md). The host applies and
[ADR 0016](../../02-DECISIONS/0016-the-node-host.md). The host applies and
does not decide *because deciding needs knowledge the machine does not have*. So the line falls
exactly there:
@@ -44,7 +44,7 @@ catch it because the dependency direction is still correct.
## What is inside it
**Seven contexts and one interface**
([ADR 0048](../../02-DECISIONS/0048-the-substrate-and-the-control-plane.md)) —
([ADR 0021](../../02-DECISIONS/0021-the-substrate-and-the-control-plane.md)) —
each one earning its place by the test above rather than by being ours:
| | | needs to know about more than one node because |
@@ -64,7 +64,7 @@ anything else does. A task does not need to know a node exists, and *being ours
something infrastructure*. `ai` is folded into `config`: a provider licence is an ordinary grant.
**`record` is an open question rather than an eighth entry.** Contexts integrate through it
([ADR 0045](../../02-DECISIONS/0045-a-context-owns-its-store.md)), which makes it load-bearing,
([ADR 0020](../../02-DECISIONS/0020-a-context-owns-its-store.md)), which makes it load-bearing,
and [research 006](../../01-RESEARCH/006-mesh-from-scratch/skeleton.md) leaves *where it lives*
unresolved — putting it in the substrate risks recreating the circularity the tier design just
removed. Listing it here would settle by naming what has not been settled by arguing.
@@ -88,10 +88,10 @@ because each one alone reads like a detail:
| | |
|---|---|
| [ADR 0037](../../02-DECISIONS/0037-the-node-host.md) | the host never queries the mesh database |
| [ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md) | a node holds its own identity **and nothing else** — the shared database credential every node carries today is the exposure this exists to remove |
| [ADR 0045](../../02-DECISIONS/0045-a-context-owns-its-store.md) | a context is granted only what it **exclusively** owns: no shared writes, no read-only roles |
| [ADR 0048](../../02-DECISIONS/0048-the-substrate-and-the-control-plane.md) | there is no single mesh database, and nothing reads one |
| [ADR 0016](../../02-DECISIONS/0016-the-node-host.md) | the host never queries the mesh database |
| [ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md) | a node holds its own identity **and nothing else** — the shared database credential every node carries today is the exposure this exists to remove |
| [ADR 0020](../../02-DECISIONS/0020-a-context-owns-its-store.md) | a context is granted only what it **exclusively** owns: no shared writes, no read-only roles |
| [ADR 0021](../../02-DECISIONS/0021-the-substrate-and-the-control-plane.md) | there is no single mesh database, and nothing reads one |
### So how does anything get in
@@ -123,17 +123,17 @@ node ──► broker ──► the control plane, consuming
Seven contexts, **one deployable** — they are not separate services, so this is one process
consuming and dispatching internally, not seven consumers racing. Each context then writes only
the store it exclusively owns
([ADR 0045](../../02-DECISIONS/0045-a-context-owns-its-store.md)).
([ADR 0020](../../02-DECISIONS/0020-a-context-owns-its-store.md)).
**One consumer is a property worth having**, not just a consequence of
[ADR 0048](../../02-DECISIONS/0048-the-substrate-and-the-control-plane.md). The as-is records that
[ADR 0021](../../02-DECISIONS/0021-the-substrate-and-the-control-plane.md). The as-is records that
*two consumers accidentally sharing one queue silently split the traffic between them, each
receiving half of what it expects* — which has happened, between a module's daemon and its
capability server. With one consumer that class of fault cannot arise.
**And the broker is the buffer while the control plane is down.** Nodes go on publishing;
messages queue; the control plane drains them when it returns. That is what makes
[ADR 0048](../../02-DECISIONS/0048-the-substrate-and-the-control-plane.md)'s single control plane
[ADR 0021](../../02-DECISIONS/0021-the-substrate-and-the-control-plane.md)'s single control plane
tolerable — an outage delays the mesh's *knowledge* rather than losing it.
**With one consequence that must be bounded before it is discovered:** a queue with no limit
@@ -149,12 +149,12 @@ before designing for throughput.** The registry is `inventory`'s store: nodes, m
assignments, versions. Those change when somebody changes something.
**Logs, metrics and health checks belong to `observability`**, which owns a different store
([ADR 0045](../../02-DECISIONS/0045-a-context-owns-its-store.md)). Sending them to the registry
([ADR 0020](../../02-DECISIONS/0020-a-context-owns-its-store.md)). Sending them to the registry
would be exactly the shared-schema mistake 0045 exists to stop, arriving through the back door
marked *performance*.
That leaves one genuine funnel: every context's writes go through the process that owns it, and
[ADR 0048](../../02-DECISIONS/0048-the-substrate-and-the-control-plane.md) says there is one of it.
[ADR 0021](../../02-DECISIONS/0021-the-substrate-and-the-control-plane.md) says there is one of it.
For a mesh of a handful of machines this is not a scaling problem, and **it should not be solved
by giving nodes database credentials** — that trades a bounded problem for an unbounded one. If
it ever binds, the answers are at the consumer: batch, apply backpressure, or move the highest
@@ -170,10 +170,10 @@ volume genuinely argues against a relational store.
- **Not a surface.** Tier 3 is how people and agents reach it. It has one interface; the
surfaces are what speak to that interface.
- **Not the substrate.** It *runs on* tier 1 — PostgreSQL, LavinMQ, MinIO, an OCI registry
([ADR 0048](../../02-DECISIONS/0048-the-substrate-and-the-control-plane.md)) — and cannot start without
([ADR 0021](../../02-DECISIONS/0021-the-substrate-and-the-control-plane.md)) — and cannot start without
them, which is what makes them a lower tier.
- **Not privileged on a node.** It has no more access to a machine than the declaration
vocabulary allows ([ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md)).
vocabulary allows ([ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md)).
## It is also a consumer
@@ -184,7 +184,7 @@ module needs, granted the same way.
That is the circularity the tiers exist to resolve rather than hide: the control plane cannot
provision its own database, because it is not running yet. So its **store** is raised from the
bundle the host carries, before there is a control plane to ask
([ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md),
([ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md),
[research 011](../../01-RESEARCH/011-the-module-graph/worked-provider.md)).
Its virtual host and its bucket are **not** in the bundle — by the time they are wanted there is
@@ -198,15 +198,15 @@ it.
hosts, assigned to nodes by the same mechanism as everything else.
**One node runs it, and nothing takes over**
([ADR 0048](../../02-DECISIONS/0048-the-substrate-and-the-control-plane.md)). The node is assigned,
([ADR 0021](../../02-DECISIONS/0021-the-substrate-and-the-control-plane.md)). The node is assigned,
never elected — no promotion, no quorum, no split brain.
That is sound rather than merely cheap, because the design already tolerates the control plane
being absent by construction: a node reconciles from **its own** store
([ADR 0037](../../02-DECISIONS/0037-the-node-host.md)) and
([ADR 0016](../../02-DECISIONS/0016-the-node-host.md)) and
never needed to ask anybody to hold the state it was last given. So the control plane being down
is not a new failure mode — it is
[ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md)'s ordinary disconnected
[ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md)'s ordinary disconnected
situation, happening to every node at once. **What is lost is change, not operation.**
The honest half: this node is a single point of failure, recovery is restore rather than
@@ -216,14 +216,14 @@ every public name.
## Open
- ~~**The contexts themselves.**~~ **Decided** — seven, by
[ADR 0048](../../02-DECISIONS/0048-the-substrate-and-the-control-plane.md).
[ADR 0021](../../02-DECISIONS/0021-the-substrate-and-the-control-plane.md).
What remains open is narrower and named there: **where the record lives**, which research 006
leaves unresolved because the substrate is the one place it must not go.
- **How far it may be split.** One deployable today. Splitting a context out costs the single
interface a surface depends on
([research 011](../../01-RESEARCH/011-the-module-graph/00-overview.md)).
- ~~**How many run, and what a node does without one.**~~ **Resolved** by
[ADR 0048](../../02-DECISIONS/0048-the-substrate-and-the-control-plane.md). What remains is
[ADR 0021](../../02-DECISIONS/0021-the-substrate-and-the-control-plane.md). What remains is
measurement: nothing reports how long the control plane has been unreachable, or how close a
certificate is to expiry — both needed for restore-not-failover to be a plan rather than a
hope.
+18 -18
View File
@@ -4,14 +4,14 @@ status: designed
code: []
updated: 2026-08-27
decisions:
- 02-DECISIONS/0019-how-this-repository-works.md
- 02-DECISIONS/0037-the-node-host.md
- 02-DECISIONS/0036-a-node-and-how-it-joins.md
- 02-DECISIONS/0048-the-substrate-and-the-control-plane.md
- 02-DECISIONS/0037-the-node-host.md
- 02-DECISIONS/0048-the-substrate-and-the-control-plane.md
- 02-DECISIONS/0049-connectivity.md
- 02-DECISIONS/0037-the-node-host.md
- 02-DECISIONS/0011-how-this-repository-works.md
- 02-DECISIONS/0016-the-node-host.md
- 02-DECISIONS/0015-a-node-and-how-it-joins.md
- 02-DECISIONS/0021-the-substrate-and-the-control-plane.md
- 02-DECISIONS/0016-the-node-host.md
- 02-DECISIONS/0021-the-substrate-and-the-control-plane.md
- 02-DECISIONS/0022-connectivity.md
- 02-DECISIONS/0016-the-node-host.md
---
# The substrate
@@ -27,7 +27,7 @@ Every module that needs a database asks the control plane's provisioning for one
plane needs a database too — and it cannot ask itself, because it is not running yet. That
circularity is not an awkwardness to work around; it *is* the definition. Anything on the wrong
side of it must be raised some other way, and the other way is the bundle the host carries
([ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md)).
([ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md)).
The test, applied:
@@ -38,11 +38,11 @@ The test, applied:
| an object store — **MinIO** | artifacts and blobs it delivers | no — it needs a bucket to hold them | **substrate** |
| an image registry — **the OCI registry** | images it delivers to nodes | no — it needs a repository | **substrate** |
| an identity provider | only if it delegates authentication | — | **conditional, below** |
| ingress — **Traefik** | not to start; only to be reached by name | — it grants itself one afterwards | **not substrate** ([ADR 0049](../../02-DECISIONS/0049-connectivity.md)) |
| ingress — **Traefik** | not to start; only to be reached by name | — it grants itself one afterwards | **not substrate** ([ADR 0022](../../02-DECISIONS/0022-connectivity.md)) |
| anything else the mesh hosts | no | — | not substrate |
**The role and the product are both written**, here and everywhere
([ADR 0048](../../02-DECISIONS/0048-the-substrate-and-the-control-plane.md)). The role is what the argument
([ADR 0021](../../02-DECISIONS/0021-the-substrate-and-the-control-plane.md)). The role is what the argument
turns on — the test above works on roles, and would give the same answers for a different store.
The product is what actually gets installed and pinned, and a design that names only the role
does not record that the choice was ever made.
@@ -96,19 +96,19 @@ Being substrate and being in the bundle are two different questions:
| PostgreSQL | yes — the control plane's own state lives in it | **yes** — there is nowhere to put that state otherwise |
| LavinMQ | yes — it cannot grant itself a virtual host | **not established** — see below |
| MinIO | yes — it cannot grant itself a bucket | no — nothing is delivered before the mesh exists |
| the OCI registry | yes — it cannot grant itself a repository | no — the first node fetches upstream ([ADR 0048](../../02-DECISIONS/0048-the-substrate-and-the-control-plane.md)) |
| the OCI registry | yes — it cannot grant itself a repository | no — the first node fetches upstream ([ADR 0021](../../02-DECISIONS/0021-the-substrate-and-the-control-plane.md)) |
The three on the bottom rows are **substrate by role and ordinary by delivery**: by the time
they are wanted there is a control plane, and it provisions them the way it provisions anything.
That keeps the bundle to roughly one image rather than four, which is what makes it small enough
for the review [ADR 0037](../../02-DECISIONS/0037-the-node-host.md)
for the review [ADR 0016](../../02-DECISIONS/0016-the-node-host.md)
requires.
**Why pinned:** the bundle is applied when no mesh exists, so nothing can resolve a version, ask
a registry, or check a constraint. What the host carries must already be exact.
**Why references and not payload:** the bundle names images by **digest** and the host fetches
them ([ADR 0048](../../02-DECISIONS/0048-the-substrate-and-the-control-plane.md)). A first node is
them ([ADR 0021](../../02-DECISIONS/0021-the-substrate-and-the-control-plane.md)). A first node is
a real machine with a network; the sealed case is the lab, and the lab places images itself.
Reproducibility comes from pinning the identity of a thing rather than carrying its bytes, which
@@ -136,7 +136,7 @@ container, so a container runtime must be working before anything else happens
is a *package*, not a container.
**Which runtime is detected, not chosen**
([ADR 0037](../../02-DECISIONS/0037-the-node-host.md)): a machine that
([ADR 0016](../../02-DECISIONS/0016-the-node-host.md)): a machine that
already has one keeps it. On a machine with none, the control plane names the package, because
what it is called differs per system. It is:
@@ -145,7 +145,7 @@ what it is called differs per system. It is:
- **adopted rather than installed** when the machine already has one with configuration somebody
chose ([research 012](../../01-RESEARCH/012-the-minimum-viable-node/00-overview.md));
- a package, which needs the machine's own package manager and a network — both permitted by
[ADR 0048](../../02-DECISIONS/0048-the-substrate-and-the-control-plane.md).
[ADR 0021](../../02-DECISIONS/0021-the-substrate-and-the-control-plane.md).
So the host's bootstrap vocabulary is six shapes: **package**, **container**, **file**,
**directory**, **service**, and **action**. **All six are built**
@@ -155,7 +155,7 @@ blocked on the host any longer.
**Steps 2 and 3 happen before there is a mesh to do them**, which is why provisioning is part of
the bootstrap rather than a service consumers use later. They are **actions** the bundle
declares and the host runs
([ADR 0037](../../02-DECISIONS/0037-the-node-host.md)) — so the
([ADR 0016](../../02-DECISIONS/0016-the-node-host.md)) — so the
host's vocabulary grows by one shape rather than by one resource type per substrate service.
## Open
@@ -170,7 +170,7 @@ host's vocabulary grows by one shape rather than by one resource type per substr
question about the control plane's internal shape, not about the substrate**, which is why it is
not answered here.
- ~~**Whether the host can do step 2.**~~ **Resolved** by
[ADR 0037](../../02-DECISIONS/0037-the-node-host.md). A service
[ADR 0016](../../02-DECISIONS/0016-the-node-host.md). A service
running on this machine is part of this machine, so the scope was never in question — the real
question was whether the host must learn what a database is, and it must not. The bundle
declares an **action**; the host runs it and verifies it, and what a database means stays with
+24 -24
View File
@@ -4,13 +4,13 @@ status: designed
code: []
updated: 2026-08-27
decisions:
- 02-DECISIONS/0037-the-node-host.md
- 02-DECISIONS/0036-a-node-and-how-it-joins.md
- 02-DECISIONS/0049-connectivity.md
- 02-DECISIONS/0049-connectivity.md
- 02-DECISIONS/0036-a-node-and-how-it-joins.md
- 02-DECISIONS/0049-connectivity.md
- 02-DECISIONS/0048-the-substrate-and-the-control-plane.md
- 02-DECISIONS/0016-the-node-host.md
- 02-DECISIONS/0015-a-node-and-how-it-joins.md
- 02-DECISIONS/0022-connectivity.md
- 02-DECISIONS/0022-connectivity.md
- 02-DECISIONS/0015-a-node-and-how-it-joins.md
- 02-DECISIONS/0022-connectivity.md
- 02-DECISIONS/0021-the-substrate-and-the-control-plane.md
---
# Connectivity
@@ -31,7 +31,7 @@ node* — to each responsibility:
|---|---|---|
| **overlay** — who peers with whom, at what address | **every node**, and which of them can be dialled | control plane |
| **resolution** — which name is which node | **every node** | control plane |
| **exposure** — which public name reaches which container | **which node is publicly reachable** ([ADR 0049](../../02-DECISIONS/0049-connectivity.md)) | control plane |
| **exposure** — which public name reaches which container | **which node is publicly reachable** ([ADR 0022](../../02-DECISIONS/0022-connectivity.md)) | control plane |
| **filtering** — which port is open, to whom | what is assigned here, and the overlay's shape | control plane decides, host applies |
| **certificates** — who may present which name | which name belongs to which node | control plane |
@@ -53,7 +53,7 @@ It is also what removes the last two upward dependencies.
[Research 006](../../01-RESEARCH/006-mesh-from-scratch/host-size.md) counted exactly two modules
opening a direct connection to the control plane's database — `wireguard` and `traefik` — and
they are the reason every node permanently holds a credential to it
([ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md)). Both are connectivity
([ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md)). Both are connectivity
modules. **Closing this context closes that set.**
## The order it comes up in
@@ -63,7 +63,7 @@ The one thing to get right, because everything else depends on it:
```
0 the node has an underlay address the machine's own — DHCP, or a provider gave it one
1 the node dials the mesh OVER THE UNDERLAY, at the address in its token
2 it proves itself, and is proved to the link exists (ADR 0039, ADR 0051)
2 it proves itself, and is proved to the link exists (ADR 0015, ADR 0015)
3 the mesh grants it an identity and an overlay address
4 the overlay comes up peer graph delivered as files
5 names resolve resolver config delivered as files
@@ -77,7 +77,7 @@ never be established on a new node. The link stays on the underlay permanently
outbound-only and carries its own identity, so it needs nothing the overlay provides.
**Nothing before step 3 can resolve a mesh name**, which is why the token carries an *address*
([ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md)). Today this is
([ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md)). Today this is
patched with an `/etc/hosts` floor written underneath the resolver; under this design there is
nothing to patch.
@@ -89,7 +89,7 @@ which of those it may dial, and which must dial it.
**Inputs, all declared:**
- **reachability** — an endpoint, or none
([ADR 0049](../../02-DECISIONS/0049-connectivity.md)). Not
([ADR 0022](../../02-DECISIONS/0022-connectivity.md)). Not
inferred from the address shape, which is wrong for carrier-grade NAT, wrong for IPv6, and
wrong for a routable address behind a closed firewall.
- **site** — where the machine physically is, or nothing if it roams.
@@ -98,7 +98,7 @@ which of those it may dial, and which must dial it.
**Keys.** Each node generates its own keypair. **The private key never leaves the machine**; the
public key is published to the mesh. This is already true and it is already right — it is
[ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md)'s *a node holds its own
[ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md)'s *a node holds its own
identity* applied to the overlay, and it means the control plane computes a graph it cannot
itself impersonate.
@@ -141,17 +141,17 @@ expensively enough to be worth restating:
name and overlay address.
**What goes away:** the `/etc/hosts` floor. It exists because a node had to reach the mesh
database before its own DNS existed; with [ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md)
database before its own DNS existed; with [ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md)
nothing needs a name before the link, and a fallback nothing needs is a path nothing tests.
## 3 — Exposure
Settled by [ADR 0049](../../02-DECISIONS/0049-connectivity.md); summarised here because
Settled by [ADR 0022](../../02-DECISIONS/0022-connectivity.md); summarised here because
this is where it belongs.
**A route is a grant.** A module that must be reachable declares it needs one; the proxy provides
it and hands back the public name. Ordinary
[ADR 0044](../../02-DECISIONS/0044-modules-and-the-graph.md)
[ADR 0019](../../02-DECISIONS/0019-modules-and-the-graph.md)
vocabulary — the mirror of a database grant, where the consumer supplies a target and receives a
name rather than supplying nothing and receiving credentials.
@@ -164,14 +164,14 @@ the case is a mesh-level fact, which is the fourth reason exposure is control-pl
consequence of what runs on it and who must reach it, not an independent declaration to keep in
step by hand.
**A rule names its source** ([ADR 0049](../../02-DECISIONS/0049-connectivity.md)).
**A rule names its source** ([ADR 0022](../../02-DECISIONS/0022-connectivity.md)).
A rule with no source is open, and must say so rather than appear to restrict something. `scope:`
is removed rather than implemented: five manifests carry it today, it is referenced by no code,
and it is the clearest instance in the repository of *an unenforced rule is indistinguishable
from a wrong one, and costs more, because people believe it.*
**Unknown keys are refused** — the discipline the host's declaration parser already has
([ADR 0037](../../02-DECISIONS/0037-the-node-host.md)), and
([ADR 0016](../../02-DECISIONS/0016-the-node-host.md)), and
the one manifests lack. `scope:` survived because nothing rejected it.
## 5 — Certificates
@@ -197,7 +197,7 @@ worse than the lab problem that found it — every certificate experiment on a r
production issuance quota, and a retry loop can exhaust it for a week.
**The mesh CA is not a bootstrap concern.** A joining node verifies the control plane against the
fingerprint in its token ([ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md)),
fingerprint in its token ([ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md)),
so nothing needs the CA before membership. It certifies internal names afterwards, and that is
all it does.
@@ -208,7 +208,7 @@ The list is worth having in one place, because it is most of the argument:
- **The last two direct database connections from nodes** — `wireguard` and `traefik`, the only
two, both connectivity.
- **Therefore the database credential on every node**, and the object-store credential beside it.
[ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md)'s central claim becomes
[ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md)'s central claim becomes
true rather than aspirational.
- **The `/etc/hosts` floor**, and the bootstrap circularity it patched.
- **Hub election by address prefix**, and the silent no-hub failure when nobody knew the
@@ -219,7 +219,7 @@ The list is worth having in one place, because it is most of the argument:
## Open
- ~~**What happens when the hub is down.**~~ **Resolved** by
[ADR 0048](../../02-DECISIONS/0048-the-substrate-and-the-control-plane.md), together with `06`'s
[ADR 0021](../../02-DECISIONS/0021-the-substrate-and-the-control-plane.md), together with `06`'s
matching question — they were one question. Nothing takes over. WireGuard has no failover, the
hub is declared rather than elected, and non-co-located paths stop while co-located direct peers
and every already-assigned workload keep running. The recovery path is restore, and its deadline
@@ -227,9 +227,9 @@ The list is worth having in one place, because it is most of the argument:
- **Renumbering the overlay.** Made *possible* by declaring the hub rather than inferring it from
an address, but no procedure exists, and a graph delivered node by node has an ordering problem
while it is half-applied.
- **Revoking a route** when a module is unassigned ([ADR 0049](../../02-DECISIONS/0049-connectivity.md)).
- **Revoking a route** when a module is unassigned ([ADR 0022](../../02-DECISIONS/0022-connectivity.md)).
A stale public name pointing at nothing fails more visibly than a stale grant.
- **IPv6.** [ADR 0049](../../02-DECISIONS/0049-connectivity.md) makes
- **IPv6.** [ADR 0022](../../02-DECISIONS/0022-connectivity.md) makes
it expressible; nothing here says the overlay or the resolver handle it.
- **Reporting declared-versus-observed.** ADR 0050 makes the disagreement detectable and does not
- **Reporting declared-versus-observed.** ADR 0022 makes the disagreement detectable and does not
say who looks or what they are told.
+42 -42
View File
@@ -4,17 +4,17 @@ status: designed
code: []
updated: 2026-08-27
decisions:
- 02-DECISIONS/0036-a-node-and-how-it-joins.md
- 02-DECISIONS/0037-the-node-host.md
- 02-DECISIONS/0036-a-node-and-how-it-joins.md
- 02-DECISIONS/0036-a-node-and-how-it-joins.md
- 02-DECISIONS/0037-the-node-host.md
- 02-DECISIONS/0036-a-node-and-how-it-joins.md
- 02-DECISIONS/0037-the-node-host.md
- 02-DECISIONS/0058-delivery.md
- 02-DECISIONS/0037-the-node-host.md
- 02-DECISIONS/0037-the-node-host.md
- 02-DECISIONS/0037-the-node-host.md
- 02-DECISIONS/0015-a-node-and-how-it-joins.md
- 02-DECISIONS/0016-the-node-host.md
- 02-DECISIONS/0015-a-node-and-how-it-joins.md
- 02-DECISIONS/0015-a-node-and-how-it-joins.md
- 02-DECISIONS/0016-the-node-host.md
- 02-DECISIONS/0015-a-node-and-how-it-joins.md
- 02-DECISIONS/0016-the-node-host.md
- 02-DECISIONS/0023-delivery.md
- 02-DECISIONS/0016-the-node-host.md
- 02-DECISIONS/0016-the-node-host.md
- 02-DECISIONS/0016-the-node-host.md
---
# The node lifecycle
@@ -42,11 +42,11 @@ questions that were not being asked live.
**Only `enrolled` and `disconnected` are nodes**, and they are the same node in two situations
rather than two kinds of thing
([ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md)). **`hosted` is not a
([ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md)). **`hosted` is not a
node** — it is a machine with a program on it that has not been told which mesh it belongs to.
There is no state for *the first node*. That is the point of
[ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md): the first node walks the
[ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md): the first node walks the
same path, in an unusual order.
---
@@ -54,7 +54,7 @@ same path, in an unusual order.
## unmanaged → hosted: installing
In the machine's own idiom, because the package manager and the init file are the system's
([ADR 0037](../../02-DECISIONS/0037-the-node-host.md)):
([ADR 0016](../../02-DECISIONS/0016-the-node-host.md)):
```
# Alpine — the intended first node
@@ -67,7 +67,7 @@ systemctl enable --now nox-mesh-host
```
Two lines each, and the init file behind them is four
([ADR 0037](../../02-DECISIONS/0037-the-node-host.md)) — it says
([ADR 0016](../../02-DECISIONS/0016-the-node-host.md)) — it says
*run the launcher at boot* and nothing else, so a third system is transcription rather than a
port.
@@ -103,7 +103,7 @@ WantedBy=multi-user.target
```
**Two lines of policy, and that is deliberate**
([ADR 0037](../../02-DECISIONS/0037-the-node-host.md)). The init is
([ADR 0016](../../02-DECISIONS/0016-the-node-host.md)). The init is
asked to *start this at boot* and *start it again if it exits*, and nothing else. Both are
expressible in OpenRC, runit, s6 and an Android `init.rc`, so porting this file is transcription
rather than design.
@@ -135,7 +135,7 @@ nox-mesh-host enrol --token <one-time token>
```
The token carries three things and is carried by a person
([ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md)): the broker's
([ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md)): the broker's
address, the fingerprint to expect, and the right to join once.
What happens, in order:
@@ -153,7 +153,7 @@ a container runtime, an architecture. The profile is not a diagnostic; it is the
**The node computes nothing about the mesh.** It needs one peer to reach; the whole overlay is
derived centrally and pushed down
([ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md),
([ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md),
[`08-connectivity.md`](08-connectivity.md)).
### The first declaration is the overlay, and nothing else
@@ -172,7 +172,7 @@ Three reasons, and the third is the one that matters when something goes wrong:
address and peer set are *assigned* — it generates a keypair, publishes the public half, and
receives the rest ([`08-connectivity.md`](08-connectivity.md)). So the overlay is the first
thing the mesh can give it, and it should be.
- **It is what [ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md) already
- **It is what [ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md) already
says:** *a joining node does the minimum to be reachable, and nothing else.*
- **It is the way back in.** Once the overlay is up, the node is reachable over it — by SSH, by
anything. If a later declaration breaks the machine, there is a route to it that does not
@@ -188,9 +188,9 @@ Worth stating plainly, because the two rules read as a contradiction and are not
|---|---|
| **every node reaches every other node** | over the overlay — SSH, services, ordinary traffic. This is the point of having one |
| **every node consumes from the broker** | its own queue, over its own outbound connection ([ADR 0001](../../02-DECISIONS/0001-nodes-communicate-over-a-broker.md)) |
| **nothing dials a node to control it** | the host has no inbound control surface ([ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md)) |
| **nothing dials a node to control it** | the host has no inbound control surface ([ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md)) |
**[ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md) is about the control
**[ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md) is about the control
channel, not about network reachability.** What it forbids is a listening thing that accepts
instructions and changes the machine. A node being reachable on the overlay — the whole purpose
of the overlay — is untouched by it, and so is a person opening a shell on it.
@@ -232,7 +232,7 @@ used months later on node two.
## Two kinds of host
Everything above assumes a machine with an init that runs the host at boot. Not every machine
has one ([ADR 0037](../../02-DECISIONS/0037-the-node-host.md)).
has one ([ADR 0016](../../02-DECISIONS/0016-the-node-host.md)).
| | **resident** | **episodic** |
|---|---|---|
@@ -245,7 +245,7 @@ has one ([ADR 0037](../../02-DECISIONS/0037-the-node-host.md)).
| can be the first node | yes | **no** |
**An episodic host being killed is disconnection, not failure.** That is
[ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md) doing the work it was written
[ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md) doing the work it was written
for: reachability is state, not class. Everything the design already does for a laptop that
closes — an authoritative local store, reconcile on start, *last heard from* reported without an
alarm — is what an episodic host needs, at a shorter period.
@@ -258,7 +258,7 @@ empty placeholder waiting to be filled in.
**Two things this changes for anything reading the mesh.** *Last heard from* is a much weaker
signal on an episodic host — a healthy phone looks like a dead server — so a reader has to know
which kind it is looking at. And a declaration may take a long time to land, which makes
[ADR 0058](../../02-DECISIONS/0058-delivery.md)'s separation of
[ADR 0023](../../02-DECISIONS/0023-delivery.md)'s separation of
*outstanding* from *failed* load-bearing rather than tidy.
**Still open:** how an episodic host is started in practice — an APK with a foreground service,
@@ -272,7 +272,7 @@ Adoption is not a state. It is what the **first apply** does when it is told to
machine already has ([research 012](../../01-RESEARCH/012-the-minimum-viable-node/00-overview.md)).
A candidate machine is not empty. It has a package manager, probably a container runtime,
configuration somebody chose. [ADR 0037](../../02-DECISIONS/0037-the-node-host.md)
configuration somebody chose. [ADR 0016](../../02-DECISIONS/0016-the-node-host.md)
says the host never touches what it did not create — adoption is the deliberate act of taking
ownership of exactly that, so it is a companion to that rule rather than an exception:
@@ -299,7 +299,7 @@ outcome **derived** from the worst line rather than stated alongside it.
**Changes are pushed, not polled.** A declaration arrives as a message on the link and the host
applies it then. The link is already open and outbound
([ADR 0001](../../02-DECISIONS/0001-nodes-communicate-over-a-broker.md),
[ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md)) — asking it
[ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md)) — asking it
repeatedly whether anything has changed would be slower to land *and* constant traffic to learn
nothing.
@@ -326,11 +326,11 @@ So the two periodic things do different jobs and should not be conflated:
without a heartbeat that is indistinguishable from a node that stopped. With one, *last heard
from* is a fact beside every node — which is what
[how long disconnected](#how-long-disconnected-and-who-is-told) reports and what
[ADR 0037](../../02-DECISIONS/0037-the-node-host.md) exists
[ADR 0016](../../02-DECISIONS/0016-the-node-host.md) exists
because a stuck node cannot send.
**Rebooting mid-apply is safe by construction.** The store records each resource *after* it
worked ([ADR 0035](../../02-DECISIONS/0035-a-picture-is-read-from-what-runs.md)), so a host that
worked ([ADR 0014](../../02-DECISIONS/0014-a-picture-is-read-from-what-runs.md)), so a host that
dies half way through comes back, finds the completed ones already matching, and applies the
rest. The rule that exists to stop the host lying about what it did also makes it crash-safe.
@@ -357,7 +357,7 @@ runtime because a declaration changed would stop every container on the node.
## enrolled ⇄ disconnected
Not a failure. Not degraded. A situation
([ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md)).
([ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md)).
A disconnected node **keeps reconciling against its own store**, so it goes on holding its
machine in the last state it was told to hold. A laptop shut for a week comes back and
@@ -365,7 +365,7 @@ reconciles; it does not come back and ask what it is.
What it cannot do: receive new declarations, be granted anything new, or have its certificates
renewed — which is the clock on the whole arrangement
([ADR 0048](../../02-DECISIONS/0048-the-substrate-and-the-control-plane.md)).
([ADR 0021](../../02-DECISIONS/0021-the-substrate-and-the-control-plane.md)).
**How long it has been disconnected is a fact the mesh must hold**, and nothing holds it today.
Without it, a node running last month's assignments looks exactly like one that is current.
@@ -384,7 +384,7 @@ nox-mesh-host profile # what can this machine actually do?
`apply FILE` accepts actions, because someone who can write that file and run this binary as
root can already do anything it can. The bound in
[ADR 0037](../../02-DECISIONS/0037-the-node-host.md) is on what a
[ADR 0016](../../02-DECISIONS/0016-the-node-host.md) is on what a
**remote** party may push, not on what a person at the machine may do.
This replaces the three hand-run scripts that exist today — first node, joining, rescue — with
@@ -401,14 +401,14 @@ what it owns by the table above, reports, and drops its identity. The machine ke
installed and is back to `hosted`. Nothing is left behind that anybody has to remember.
**The node is gone.** Stolen, dead, or simply unreachable. The mesh cannot tell it anything, and
by [ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md) it will go on reconciling
by [ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md) it will go on reconciling
its last declaration **forever**.
That is the honest consequence of making disconnection ordinary, and the answer is not to make
the host expire. It is that **the node holds nothing that outlives revocation**: its identity is
its own, and every grant it holds is a per-node credential at the provider
([ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md),
[ADR 0045](../../02-DECISIONS/0045-a-context-owns-its-store.md)). Revoking is done at the
([ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md),
[ADR 0020](../../02-DECISIONS/0020-a-context-owns-its-store.md)). Revoking is done at the
database, the broker, the object store — not on the machine.
So a lost node keeps *running* and stops being able to *reach* anything. That is the best
@@ -438,7 +438,7 @@ remains locally authoritative for *operating*; the copy exists only for this.
## Upgrading the host
The host is delivered like anything else
([ADR 0058](../../02-DECISIONS/0058-delivery.md)), and this is worth
([ADR 0023](../../02-DECISIONS/0023-delivery.md)), and this is worth
walking through because tier 0 looks like it should be special and is not.
```
@@ -471,7 +471,7 @@ the test of whether this is really uniform.
3 it finishes the apply and reports never mid-way
4 it exits 0 having finished, not having been stopped
5 the launcher starts it again on the new binary — it supervises the host
rather than exec'ing it (ADR 0061), so this
rather than exec'ing it (ADR 0016), so this
needs nothing from the init
6 the new host reconciles on start trigger 1, confirming the machine still matches
```
@@ -489,7 +489,7 @@ own apply completes. A node must therefore report the version it is **running**,
installed — otherwise the mesh believes an upgrade landed at step 1.
**A version that crashes on start rolls itself back**
([ADR 0037](../../02-DECISIONS/0037-the-node-host.md)).
([ADR 0016](../../02-DECISIONS/0016-the-node-host.md)).
What the init starts is not the host but a **launcher**, and the launcher is where the policy
lives:
@@ -551,7 +551,7 @@ credentials still valid — the case
**The host reports what it owns, and the mesh keeps the last report.**
The store stays locally authoritative — a node operates from its own copy and needs nothing to
do so ([ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md)). What changes is that
do so ([ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md)). What changes is that
the mesh holds a **copy for recovery**, refreshed on every apply report.
So a node that loses its state file re-enrols, receives both the declaration *and* the record of
@@ -616,12 +616,12 @@ keeps cataloguing.
### Where the enrolment token comes from
**`mesh-control token issue` prints it once**, to the person running it. Single-use, and it
expires whether used or not ([ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md)).
expires whether used or not ([ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md)).
It is carried by hand — read off a screen, pasted into a terminal. That is the design rather than
a gap in it: its authenticity comes from the channel it travelled, which is what
lets a node verify a mesh it has never spoken to
([ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md)). A token emailed,
([ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md)). A token emailed,
committed, or dropped in shared storage has lost the only property that makes it worth carrying.
**On the first node it comes from the control plane that was raised two commands ago**, which is
@@ -632,10 +632,10 @@ the same command against a mesh that is one machine old.
## Still open
- ~~**Automatic rollback of a bad host version.**~~ **Resolved** by
[ADR 0037](../../02-DECISIONS/0037-the-node-host.md): a launcher
[ADR 0016](../../02-DECISIONS/0016-the-node-host.md): a launcher
counts failed starts and rolls back — shipped by the package, not the host binary, because a
binary that will not start cannot recover itself. It rolls back once; a second failure means
the machine is the problem, not the binary.
- **How a previous declaration is retained and chosen**, which is what rollback of anything else
would use ([ADR 0058](../../02-DECISIONS/0058-delivery.md)).
would use ([ADR 0023](../../02-DECISIONS/0023-delivery.md)).
- **A node returning after months** applies a very large jump in one go. Correct, and untested.
+7 -7
View File
@@ -4,13 +4,13 @@ status: designed
code: []
updated: 2026-08-28
decisions:
- 02-DECISIONS/0058-delivery.md
- 02-DECISIONS/0044-modules-and-the-graph.md
- 02-DECISIONS/0044-modules-and-the-graph.md
- 02-DECISIONS/0058-delivery.md
- 02-DECISIONS/0058-delivery.md
- 02-DECISIONS/0044-modules-and-the-graph.md
- 02-DECISIONS/0044-modules-and-the-graph.md
- 02-DECISIONS/0023-delivery.md
- 02-DECISIONS/0019-modules-and-the-graph.md
- 02-DECISIONS/0019-modules-and-the-graph.md
- 02-DECISIONS/0023-delivery.md
- 02-DECISIONS/0023-delivery.md
- 02-DECISIONS/0019-modules-and-the-graph.md
- 02-DECISIONS/0019-modules-and-the-graph.md
---
# Modules and delivery
+14 -14
View File
@@ -9,27 +9,27 @@ document is written and this one's status becomes `implemented`.
| Document | Covers | Rests on |
|---|---|---|
| [`00-work-breakdown.md`](00-work-breakdown.md) | How the decomposition gets built, in what order, and where a human must look | [ADR 0015](../../02-DECISIONS/0015-mesh-brokers-nodes-host-agents-think.md) |
| [`01-end-to-end-testing.md`](01-end-to-end-testing.md) | The lab: a real mesh a change can be run against before it reaches nodes | [ADR 0016](../../02-DECISIONS/0016-the-lab.md), [0029](../../02-DECISIONS/0016-the-lab.md) |
| [`02-scenario-declaration.md`](02-scenario-declaration.md) | What a scenario declares — the underlay, and what to place on it | [ADR 0016](../../02-DECISIONS/0016-the-lab.md) |
| [`03-scenario-lifecycle.md`](03-scenario-lifecycle.md) | What happens to a scenario — raise, snapshot, restore, move, destroy | [ADR 0016](../../02-DECISIONS/0016-the-lab.md) |
| [`04-lab-installation.md`](04-lab-installation.md) | Getting the lab onto a clean machine, and why it verifies capability rather than installation | [ADR 0058](../../02-DECISIONS/0058-delivery.md) |
| [`05-the-node-host.md`](05-the-node-host.md) | Tier 0 — the one thing installed by hand, and the only thing that changes a machine | [ADR 0037](../../02-DECISIONS/0037-the-node-host.md) |
| [`06-the-control-plane.md`](06-the-control-plane.md) | Tier 2 — what the term means, and the test for what belongs in it | [ADR 0037](../../02-DECISIONS/0037-the-node-host.md) |
| [`07-the-substrate.md`](07-the-substrate.md) | Tier 1 — what the control plane consumes and cannot grant itself | [ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md), [0048](../../02-DECISIONS/0048-the-substrate-and-the-control-plane.md) |
| [`08-connectivity.md`](08-connectivity.md) | One context in full — overlay, resolution, exposure, filtering, certificates | [ADR 0049](../../02-DECISIONS/0049-connectivity.md), [0050](../../02-DECISIONS/0049-connectivity.md), [0051](../../02-DECISIONS/0036-a-node-and-how-it-joins.md), [0055](../../02-DECISIONS/0048-the-substrate-and-the-control-plane.md) |
| [`09-the-node-lifecycle.md`](09-the-node-lifecycle.md) | How a machine becomes a node, stays one, and stops being one | [ADR 0036](../../02-DECISIONS/0036-a-node-and-how-it-joins.md), [0051](../../02-DECISIONS/0036-a-node-and-how-it-joins.md) |
| [`10-delivery.md`](10-delivery.md) | Modules, the three edges, and how a change becomes a running thing | [ADR 0058](../../02-DECISIONS/0058-delivery.md), [0064](../../02-DECISIONS/0044-modules-and-the-graph.md), [0065](../../02-DECISIONS/0044-modules-and-the-graph.md) |
| [`00-work-breakdown.md`](00-work-breakdown.md) | How the decomposition gets built, in what order, and where a human must look | [ADR 0008](../../02-DECISIONS/0008-mesh-brokers-nodes-host-agents-think.md) |
| [`01-end-to-end-testing.md`](01-end-to-end-testing.md) | The lab: a real mesh a change can be run against before it reaches nodes | [ADR 0009](../../02-DECISIONS/0009-the-lab.md), [0029](../../02-DECISIONS/0009-the-lab.md) |
| [`02-scenario-declaration.md`](02-scenario-declaration.md) | What a scenario declares — the underlay, and what to place on it | [ADR 0009](../../02-DECISIONS/0009-the-lab.md) |
| [`03-scenario-lifecycle.md`](03-scenario-lifecycle.md) | What happens to a scenario — raise, snapshot, restore, move, destroy | [ADR 0009](../../02-DECISIONS/0009-the-lab.md) |
| [`04-lab-installation.md`](04-lab-installation.md) | Getting the lab onto a clean machine, and why it verifies capability rather than installation | [ADR 0023](../../02-DECISIONS/0023-delivery.md) |
| [`05-the-node-host.md`](05-the-node-host.md) | Tier 0 — the one thing installed by hand, and the only thing that changes a machine | [ADR 0016](../../02-DECISIONS/0016-the-node-host.md) |
| [`06-the-control-plane.md`](06-the-control-plane.md) | Tier 2 — what the term means, and the test for what belongs in it | [ADR 0016](../../02-DECISIONS/0016-the-node-host.md) |
| [`07-the-substrate.md`](07-the-substrate.md) | Tier 1 — what the control plane consumes and cannot grant itself | [ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md), [0048](../../02-DECISIONS/0021-the-substrate-and-the-control-plane.md) |
| [`08-connectivity.md`](08-connectivity.md) | One context in full — overlay, resolution, exposure, filtering, certificates | [ADR 0022](../../02-DECISIONS/0022-connectivity.md), [0050](../../02-DECISIONS/0022-connectivity.md), [0051](../../02-DECISIONS/0015-a-node-and-how-it-joins.md), [0055](../../02-DECISIONS/0021-the-substrate-and-the-control-plane.md) |
| [`09-the-node-lifecycle.md`](09-the-node-lifecycle.md) | How a machine becomes a node, stays one, and stops being one | [ADR 0015](../../02-DECISIONS/0015-a-node-and-how-it-joins.md), [0051](../../02-DECISIONS/0015-a-node-and-how-it-joins.md) |
| [`10-delivery.md`](10-delivery.md) | Modules, the three edges, and how a change becomes a running thing | [ADR 0023](../../02-DECISIONS/0023-delivery.md), [0064](../../02-DECISIONS/0019-modules-and-the-graph.md), [0065](../../02-DECISIONS/0019-modules-and-the-graph.md) |
## Not yet written
- **The remaining six contexts.**
[ADR 0048](../../02-DECISIONS/0048-the-substrate-and-the-control-plane.md)
[ADR 0021](../../02-DECISIONS/0021-the-substrate-and-the-control-plane.md)
settles the list at seven; `connectivity` is the first written in full
([`08`](08-connectivity.md)) and the other six do not exist yet. The work breakdown says in
what order they are needed.
- ~~**Domain grouping outside the core.**~~ **Not needed.**
[ADR 0044](../../02-DECISIONS/0044-modules-and-the-graph.md) is
superseded by [ADR 0044](../../02-DECISIONS/0044-modules-and-the-graph.md):
[ADR 0019](../../02-DECISIONS/0019-modules-and-the-graph.md) is
superseded by [ADR 0019](../../02-DECISIONS/0019-modules-and-the-graph.md):
there is no domain module to group into, so there is no domain list to settle. Relationships
are edges, and grouping is a tag and a query.