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.
98 lines
5.1 KiB
Markdown
98 lines
5.1 KiB
Markdown
---
|
|
status: accepted
|
|
date: 2026-08-25
|
|
deciders: jochen
|
|
reconstructed: false
|
|
---
|
|
|
|
# 37. The host applies; it does not decide
|
|
|
|
## Context
|
|
|
|
The skeleton absorbs overlay membership, packet filtering, package management, service
|
|
supervision, the container runtime and filesystem management into tier 0, and
|
|
[research 006](../01-RESEARCH/006-mesh-from-scratch/00-overview.md) called this *"the
|
|
skeleton's biggest unproven claim. A binary whose whole argument is that it has no dependencies
|
|
now carries six concerns."*
|
|
|
|
That claim has now been measured against the monorepo's `main`:
|
|
[`host-size.md`](../01-RESEARCH/006-mesh-from-scratch/host-size.md).
|
|
|
|
The measurement says the question asked about the wrong axis.
|
|
|
|
## Considered options
|
|
|
|
1. **Absorb the six concerns as they are.** What the skeleton literally proposes. Rejected on
|
|
evidence: two of the ten modules implementing them open a direct connection to the control
|
|
plane's database and compute their own configuration. Absorbing those unchanged puts a
|
|
Postgres client and knowledge of the mesh schema inside tier 0 — an upward dependency, and
|
|
the tier rule is the whole of the bootstrap argument.
|
|
2. **Leave them as modules.** Keeps the tier rule trivially, and keeps the fault that prompted
|
|
the skeleton: four modules constituting *how a node is reachable* with no relationship the
|
|
mesh can see, so one intent is expressed four times
|
|
([research 005](../01-RESEARCH/005-domain-grouping/analysis.md) finding 4 measures this and
|
|
finds it is the only place in the catalogue where the shape genuinely occurs).
|
|
3. **Split each concern: decide centrally, apply locally.** Chosen.
|
|
|
|
## Decision
|
|
|
|
**The host has one concern: apply declared state on this machine.** The six are not six
|
|
concerns it carries; they are instances of the one.
|
|
|
|
Each divides:
|
|
|
|
- **Deciding** — what this node's overlay, names, exposure, filtering, packages and services
|
|
*should be*. This needs every other node, and belongs to the control plane.
|
|
- **Applying** — putting that on the machine. This needs root and locality, and belongs to the
|
|
host.
|
|
|
|
**The host never queries the mesh database.** A host that reads the control plane's schema is
|
|
tier 0 depending on tier 2, and the tiers stop being a bootstrap answer the moment that is
|
|
permitted once.
|
|
|
|
## Why the evidence supports it
|
|
|
|
**Size was the wrong worry.** The ten modules total 2 755 lines. The machinery that already
|
|
applies state on a node — `meshware`, `env-sync`, `config-sync` — is 3 059. Everything being
|
|
absorbed is smaller than what already exists to apply it. The host is not a new large thing; it
|
|
already exists, spread across three core modules and unnamed.
|
|
|
|
**Eight of the ten are already pure appliers.** They receive derived state and put it on the
|
|
machine. Absorbing them moves code that has no dependency to move.
|
|
|
|
**The split has already been happening, unnamed.** `dnsmasq-app` needs the same mesh-wide data
|
|
as `wireguard` and does not query for it. Its own comments record why: the values were
|
|
*"duplicated by hand on all four nodes"* until someone derived them centrally, after a rename
|
|
meant editing four override rows nobody knew about. That is this decision, reached once by
|
|
fixing a bug.
|
|
|
|
## Consequences
|
|
|
|
- **Two modules must be split before they can be absorbed**, and they are the two hardest.
|
|
`wireguard` needs every node's key, address, site and endpoint reachability; `traefik` needs
|
|
certificates and every node's exposed names. The measurement says the design is right; it
|
|
does not say the migration is cheap, and this record does not claim it is.
|
|
- **The overlay and firewall modules stop existing** as the skeleton says — but the reason is
|
|
now sharper than "they are host concerns". The host holds membership and applies filtering;
|
|
the control plane decides policy; swappable backends stay modules.
|
|
- **Six vocabularies remain.** Zero dependencies, but the host must still know what a WireGuard
|
|
peer, an nftables rule, a package, a unit, a container and a dataset *are*. That surface is
|
|
the residue of the original worry and is not measured by anything here.
|
|
- **A dependency-direction lint is now load-bearing**, not a nicety. This record is a rule
|
|
about direction, and per this repository's own standard a rule states how it is checked: an
|
|
upward import fails the build. A tier rule enforced by intention is the same as no tier rule.
|
|
- **What the host carries versus what it finds is still open.**
|
|
[Issue 007](../04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md) — the
|
|
host manages `wg`, `nft`, `pacman`, `docker`; it does not contain them, and *installed* is
|
|
not the same as *usable*.
|
|
|
|
## References
|
|
|
|
- [`host-size.md`](../01-RESEARCH/006-mesh-from-scratch/host-size.md) — the measurement.
|
|
- [Research 005](../01-RESEARCH/005-domain-grouping/analysis.md) — reachability as the only
|
|
measured co-change cluster in the catalogue.
|
|
- [ADR 0003](0003-the-mesh-database-is-the-source-of-truth.md) — what the control plane decides
|
|
from.
|
|
- [ADR 0030](0030-the-repository-structure.md) — `mesh-host` as tier 0.
|
|
- [ADR 0008](0008-a-failed-step-fails-the-job.md) — the standard the direction lint is held to.
|