Files
hq/02-DECISIONS/0037-the-host-applies-it-does-not-decide.md
T
jschoubben 5e83ac2c22 Consolidate: 65 decision records to 52
Jochen: a normal application has 3-5 ADRs, maybe 10 for a large one, and we are
at 65. Fair, and the cause is mine -- I recorded every FINDING as a decision
rather than every fork in the road.

Two merges, both cases where one decision had been split across many records
because it was taken over several days rather than at once.

0019 absorbs ten records about how this repository works: what it is and that
it is public, the folder flow, the two design layers, the issue front door,
status in frontmatter, playbooks, the naming rule, the product name. Those were
never ten decisions -- they were one, seen from ten angles as the repository
took shape.

0016 absorbs the five about the lab: a node is a virtual machine, a router is
scenery, a scenario declares the underlay, a scenario is a closed address
space, and the two scenario classes. Same pattern -- one design, split by the
order it was worked out in.

The consolidated 0019 also raises the bar for what earns a record, since that
is what produced 65: a record is warranted when there is a genuine fork -- a
direction reversed, an alternative that will be proposed again, something
contested. A finding is not a decision, and a bug is certainly not. Everything
else belongs in the design document where the reasoning is actually read.

The checker earned its place here. Deleting nine records left 13 dangling links
across the repository and it named every one, including in AGENTS.md. Nothing
was found by reading.

Remaining clusters worth the same treatment: the host (8 records), delivery
(5), modules (6), connectivity (4), substrate and control plane (4). That would
be 52 down to roughly 30.
2026-08-28 18:53:19 +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 0019](0019-how-this-repository-works.md) — `mesh-host` as tier 0.
- [ADR 0008](0008-a-failed-step-fails-the-job.md) — the standard the direction lint is held to.