ADRs 0036, 0037, 0038 — what a node is, what the host does, how one joins
0036 (accepted): a node is a managed machine, and disconnection is a situation. The open question posed a class distinction — full nodes and lesser presences. There is none. Reachability is state, not kind, which promotes the host's local store from a component to a requirement: it is what makes disconnection ordinary rather than exceptional. The reduced contract the question reached for is real but it is capability, and that belongs in the profile. 0037 (accepted): the host applies, it does not decide. Measured rather than argued — the absorption is smaller than the machinery that already applies state, and eight of ten adapters carry no dependency to move. The two that do open a Postgres connection to the control plane, which inside tier 0 is the one thing the tier rule exists to forbid. So each concern splits: deciding needs every other node and stays in tier 2; applying needs root and locality and goes to tier 0. The host carries ONE concern, of which the six are instances. 0038 (proposed): a node joins by linking first. The operator's two-modes proposal, adopted as intent and corrected as structure. Two modes is two code paths where the first runs once per mesh and rots — and the mesh already has that fault in its worst form, as three hand-run shell scripts. Instead: one behaviour, two sources of declaration. The first node is not a different kind of node, it is a node whose mesh is not up yet, and its specialness is temporary and self-erasing. 0038 also shrinks the migration 0037 called expensive: a joining node never needs mesh-wide state, because the hard part of the overlay is only needed to compute the WHOLE mesh. It needs one peer. The rest arrives. Left open and said so: what may be pushed over the link and how a joining node proves it is entitled to join, and whether one host can raise the substrate alone.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
---
|
||||
status: proposed
|
||||
date: 2026-08-25
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 0037-the-host-applies-it-does-not-decide.md
|
||||
---
|
||||
|
||||
# 38. A node joins by linking first, and the mesh finishes the job
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0037](0037-the-host-applies-it-does-not-decide.md) settles that the host applies and the
|
||||
control plane decides. That leaves the case where there is no control plane to decide: the
|
||||
first node, which must raise a mesh from nothing, and the second, which must join one.
|
||||
|
||||
Raised by the operator: *"shouldn't the host have two modes — one for the initial node, setting
|
||||
up the mesh, so we know the full state; then when adopting a second node, we enter the mesh
|
||||
early and let our first node take over the mesh-related work? The host should only set up the
|
||||
bare minimum for the other nodes in the mesh to complete adoption."*
|
||||
|
||||
The instinct is right and it is the resolution of the gap ADR 0037 leaves open. The framing
|
||||
needs one correction, and the correction comes from what the mesh already does.
|
||||
|
||||
**Today there are three hand-run shell paths**: `install.d/adopt.sh` (183 lines),
|
||||
a separate first-node bootstrap, and `install.d/rescue.sh`. The skeleton names the cause —
|
||||
*"the first node is raised by a special script that exists only because of the circularity"* —
|
||||
and Move 1 exists to remove it. Three paths that do nearly the same thing, maintained
|
||||
separately, run by hand, outside anything that checks them.
|
||||
|
||||
**That is the two-mode problem, already at its worst.** A decision that gives the host two
|
||||
modes risks rebuilding `adopt.sh` and `bootstrap.sh` inside the binary, where they will drift
|
||||
in exactly the same way and be harder to see.
|
||||
|
||||
## Considered options
|
||||
|
||||
1. **Two modes — genesis and join.** What was proposed. Rejected as a *structure* while adopted
|
||||
as an *intent*: two modes is two code paths, the first is exercised once per mesh and the
|
||||
second constantly, so the rarely-run one rots. The current three scripts are the evidence.
|
||||
2. **One path, and the first node is special-cased inside it.** The conditional moves rather
|
||||
than disappearing, and now it is scattered instead of named.
|
||||
3. **One behaviour, two sources of declaration.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
**The host has one behaviour: apply the declaration it has.** What differs between the first
|
||||
node and the fiftieth is not what the host *does* but **where the declaration comes from** —
|
||||
and, exactly as in [ADR 0036](0036-a-node-is-a-managed-machine.md), that is a situation rather
|
||||
than a class.
|
||||
|
||||
| Situation | Declaration comes from |
|
||||
|---|---|
|
||||
| no mesh reachable | the pinned bundle the host carries (`substrate.lock`) |
|
||||
| mesh reachable | the control plane, over the link |
|
||||
|
||||
**The first node is not a different kind of node.** It is a node whose mesh is not up *yet*. It
|
||||
applies the bundle it carries, the control plane comes up on top of it, and from that moment it
|
||||
takes declarations like everything else. Its specialness is temporary and self-erasing, which
|
||||
is the property `adopt.sh` and the bootstrap script do not have.
|
||||
|
||||
**A joining node does the minimum to be reachable, and nothing else.** It establishes identity
|
||||
and a route to the control plane — the `link` — and then stops deciding. Everything after that
|
||||
arrives as declarations.
|
||||
|
||||
**The minimum is deliberately small:** an identity, an address, and one peer to reach. A
|
||||
joining node does **not** compute the overlay. It needs a single peer to reach the mesh; the
|
||||
full peer set is derived centrally and pushed down, like everything else.
|
||||
|
||||
## Why this resolves what 0037 left open
|
||||
|
||||
ADR 0037 records that `wireguard` and `traefik` are the two modules that must be split before
|
||||
they can be absorbed, and that they are the hardest because they need mesh-wide state.
|
||||
|
||||
**A joining node never needs that state.** The hard part of the overlay — every node's key,
|
||||
address, site and endpoint reachability — is only needed to compute the *whole* mesh, which is
|
||||
the control plane's job. The node needs one peer. The rest arrives.
|
||||
|
||||
So the migration ADR 0037 calls expensive is smaller than it looked, and this record is what
|
||||
makes it smaller.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Adoption stops being a script.** The three hand-run paths collapse into the host: joining
|
||||
is establishing a link, and rescue is a node whose local state is discarded so the mesh can
|
||||
re-derive it. Whether rescue is fully covered by this is not decided here.
|
||||
- **The bundle is a fallback, not a mode.** It is what a host applies when nothing better is
|
||||
available, which also covers a node that has been disconnected for a long time — ADR 0036's
|
||||
ordinary situation.
|
||||
- **The rarely-run path is now the common one.** The first node exercises the same code every
|
||||
other node exercises constantly. That is the whole reason for choosing this over two modes.
|
||||
- **The link becomes the security boundary.** Everything a node applies arrives through it, so
|
||||
what may be pushed, and how a joining node proves it is entitled to join, is now a question
|
||||
worth its own record. **Not decided here, and it is a gap.**
|
||||
- **The bundle must be able to raise the substrate alone.** Whether one host can bring up the
|
||||
four pinned services with no mesh present is Move 1 of the skeleton and remains unproven.
|
||||
This record depends on it and does not establish it.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0037](0037-the-host-applies-it-does-not-decide.md) — the split this completes.
|
||||
- [ADR 0036](0036-a-node-is-a-managed-machine.md) — situation rather than class, applied here
|
||||
to the first node.
|
||||
- [Research 006, Move 1](../01-RESEARCH/006-mesh-from-scratch/skeleton.md) — the pinned bundle,
|
||||
and the special script it exists to remove.
|
||||
- [`00-as-is/05-runtime-and-installation.md`](../03-DESIGN/00-as-is/05-runtime-and-installation.md)
|
||||
— how a node comes into being today.
|
||||
Reference in New Issue
Block a user