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.
107 lines
5.7 KiB
Markdown
107 lines
5.7 KiB
Markdown
---
|
|
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.
|