Files
hq/02-DECISIONS/0037-the-host-applies-it-does-not-decide.md
T
jschoubben 72b22830f3 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.
2026-08-25 10:34:45 +02:00

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.