--- 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.