# Is the host too large? The skeleton absorbs overlay membership, packet filtering, package management, service supervision, the container runtime and filesystem management into tier 0, and [`00-overview.md`](00-overview.md) calls this *"the skeleton's biggest unproven claim. A binary whose whole argument is that it has no dependencies now carries six concerns."* This is that claim, measured. ## Method Against `origin/main` of the monorepo at 2026-08-25 — read through git refs rather than a checkout, which sits on a feature branch 1031 commits behind with uncommitted work. For each module that implements one of the six concerns: how much code it is, and **what it depends on to do its job**. The second question turned out to be the one that matters. ## Finding 1 — by size, the concern is misplaced | Absorbed into the host | Lines | |---|---| | `dnsmasq-app` | 706 | | `traefik` | 426 | | `ufw` | 385 | | `mesh-ca` | 353 | | `wireguard` | 313 | | `incus` | 285 | | `package-manager` | 118 | | `zfs` | 67 | | `fail2ban` | 60 | | `docker-app` | 42 | | **total** | **2 755** | | Machinery that already applies state on a node | Lines | |---|---| | `hal/meshware` | 1 829 | | `hal/env-sync` | 626 | | `hal/config-sync` | 604 | | **total** | **3 059** | **Everything being absorbed is smaller than the machinery that already exists to apply it.** The six concerns are not six subsystems; they are ten thin adapters averaging 275 lines, most of which is rendering a config file and running a command. The host is not a new large thing. It already exists, spread across three core modules, and the absorption adds less code than those three already contain. Size is therefore the wrong axis, and the open question asked about the wrong thing. ## Finding 2 — the real risk is direction, and it is narrow What each adapter needs in order to decide what to write: | Module | Gets its inputs from | Applier only? | |---|---|---| | `ufw` | `~/.hal/modules/*/module.yml` on the local disk | yes | | `dnsmasq-app` | `process.env.DNSMASQ_*`, derived centrally by `env-sync` | yes | | `fail2ban`, `mesh-ca`, `package-manager`, `docker-app`, `incus`, `zfs` | local state and declared env | yes | | `wireguard` | **direct `pg` connection** — `nodes`, `node_accessors`, `node_wg_keys`, `module_env` | **no** | | `traefik` | **direct `pg` connection** — `nodes`, `mesh_ca` | **no** | **Eight of ten are already pure appliers.** They receive derived state and put it on the machine. Absorbing those into tier 0 moves no dependency at all — it moves code that already has none. **Two reach upward.** `wireguard` and `traefik` open a connection to the control plane's database and compute their own configuration from it. Absorbing them *as they are* would put a Postgres client and knowledge of the mesh schema inside tier 0 — an upward dependency, which is precisely what [the tier rule](skeleton.md) forbids and what the entire bootstrap argument rests on. So the danger in the absorption is real, and it is two modules wide rather than six concerns wide. ## Finding 3 — the split has already been happening, unnamed `dnsmasq-app` is the same *kind* of module as `wireguard`: it needs every node's addresses and names. It does not query for them. Its own comments record why: > *"the mesh DB already holds [this] in `node_accessors` and the WireGuard address, duplicated > by hand on all four nodes. Renaming the namespace then meant editing four override rows > nobody [knew about]."* and > *"now derived from `node_accessors`"* That is one module having already made the move the skeleton proposes — deciding centrally, applying locally — for the ordinary reason that hand-duplicated state went wrong. Nobody named it as an architectural direction; it was reached by fixing a bug. Eight of ten adapters are on the far side of that migration. Two are not. ## What this means for the open question **The absorption is not a move. It is a split, and it is mostly already done.** The question "does the host become too large" assumed six concerns would arrive whole. They do not. Each divides: - **deciding** — what this node's overlay, names, exposure and filtering should be, which needs every other node and therefore belongs in tier 2; - **applying** — putting that on the machine, which needs root and locality and therefore belongs in tier 0. Tier 0 absorbs the applying. That is ~2 755 lines today, most of it already dependency-free, against 3 059 lines of apply machinery the host needs regardless. **The claim survives, with its scope corrected.** The host does not carry six concerns; it carries one — *apply declared state on this machine* — of which the six are instances. That is the skeleton's own tier-0 test (*"does it apply state on a machine?"*) applied to itself. ## What remains open - **The two unsplit modules are the two hardest.** Overlay needs every node's key, address, site and endpoint reachability; the proxy needs certificates and every node's exposed names. These are where "derive centrally, apply locally" is most work, and neither has been done. The measurement says the design is right; it does not say the migration is cheap. - **Six concerns is still six vocabularies.** Absorbing them adds no dependencies but does add surface: the host must know what a WireGuard peer, an nftables rule, a package, a unit, a container and a dataset *are*. Nothing here measures that cost, and it is the residue of the original worry. - **What the host must carry versus what it must find.** The host manages `wg`, `nft`, `pacman`, `docker`; it does not contain them. [Issue 007](../../04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md) is exactly this question and is unresolved.