0060 named the gap and did not close it: everywhere else an init runs the launcher at boot, and Android grants neither an init to register with nor anything worth supervising, because a supervisor would be killed alongside what it supervises. Closed by narrowing what is required rather than building something. A host is resident or episodic, and both are hosts. Being killed by the platform is disconnection, which 0036 already made ordinary -- and every mechanism an episodic host needs already exists because it was built for laptops that close. A partial host can join a mesh and cannot be the first node, since every bootstrap step is a shape it refuses. Its bundle says so. Two consequences that are easy to miss: last-heard-from means much less on an episodic host, so a healthy phone reads as a dead server unless the reader knows which kind it is; and a declaration may take a long time to land, which makes 0058's outstanding-versus-failed distinction load-bearing. Still open, and in that order: what an Android node is FOR, and only then how it is started.
145 lines
8.1 KiB
Markdown
145 lines
8.1 KiB
Markdown
---
|
|
status: accepted
|
|
date: 2026-08-28
|
|
deciders: jochen
|
|
reconstructed: false
|
|
extends: 0057-the-host-is-a-root-service-installed-as-a-package.md
|
|
---
|
|
|
|
# 60. The host is built per operating system
|
|
|
|
## Context
|
|
|
|
Three of the host's six shapes need something from the machine: `service` needs a service
|
|
manager, `package` needs a package manager, `container` needs a container runtime. The other
|
|
three — `file`, `directory`, `action` — need only a filesystem and the ability to run something.
|
|
|
|
The host names those capabilities generically and implements them specifically. The detector
|
|
reports `container-runtime`, `package-manager`, `service-manager`; the appliers call `docker`,
|
|
`pacman` and `systemctl`. **So the design says capability and the code says Arch**, and nothing
|
|
records which of those is the intent.
|
|
|
|
The question that surfaced it: what happens on a machine that has podman, or one that does not
|
|
run systemd? And underneath it, a live contradiction —
|
|
[research 012](../01-RESEARCH/012-the-minimum-viable-node/00-overview.md) decides that on
|
|
conflict during adoption *the machine's configuration is kept*, so a machine with podman keeps
|
|
podman, and then the container applier calls `docker` and fails.
|
|
|
|
## Considered options
|
|
|
|
1. **Abstract each capability behind an interface.** One host, adapters per service manager and
|
|
package manager. Rejected, and the reason is correctness rather than effort: the service
|
|
applier reads `LoadState` to tell *not installed* apart from *stopped*, which is what stops it
|
|
reporting absence as success. An interface spanning systemd and OpenRC degrades to what both
|
|
can express, and **the lowest common denominator is exactly where that fault lives**.
|
|
2. **Support one operating system and say so.** Honest, and it makes every other machine
|
|
permanently out of scope rather than not-yet.
|
|
3. **A host per operating system.** Chosen.
|
|
|
|
## Decision
|
|
|
|
**The host is built for an operating system family, and `systemd` and `pacman` are the Arch
|
|
host's implementation rather than abstractions the mesh has to grow.**
|
|
|
|
```
|
|
mesh-host-arch-x86_64 pacman · systemctl · docker
|
|
mesh-host-debian-x86_64 apt · systemctl · docker (when there is a machine)
|
|
mesh-host-alpine-x86_64 apk · rc-service · podman (when there is a machine)
|
|
```
|
|
|
|
**These are not independent choices and treating them as such was the error.** A machine has
|
|
pacman *because* it is Arch. The package manager, the service manager and the packaging format
|
|
arrive together, as one decision somebody made when they installed the operating system.
|
|
|
|
### Almost all of it is shared
|
|
|
|
Not a rewrite per operating system. The declaration vocabulary, the store, the apply loop, the
|
|
read-back discipline, the refusal model and the link are all portable. **What differs is two
|
|
appliers**, and the rest is compiled around them.
|
|
|
|
**The bundle is the exception, and an earlier version of this record wrongly listed it as
|
|
portable.** Its *mechanism* is — one embedded declaration, applied with no mesh present. Its
|
|
*contents* are not: package names, unit names and service names all differ, so an Arch host
|
|
embeds an Arch bundle and an Alpine host an Alpine one. That is the same thing this record says
|
|
about package names one section down, and missing it here is what made the distinction hard to
|
|
see.
|
|
|
|
### The control plane names the package, because the host does not decide
|
|
|
|
A container runtime is `docker` on Arch and `docker.io` on Debian. Mapping *this node needs a
|
|
container runtime* to a package name is **deciding**, which
|
|
[ADR 0037](0037-the-host-applies-it-does-not-decide.md) puts outside the host.
|
|
|
|
It needs no new mechanism: the profile already reports what the machine is, so the declaration a
|
|
node receives is already tailored to that node. The host receives a package name and installs it.
|
|
|
|
### A host that cannot implement a shape refuses it
|
|
|
|
The interesting case is not Debian, it is **Android** — no service manager it will lend us, no
|
|
package installation, usually no root. Such a host implements `file`, `directory` and `action`,
|
|
and nothing else.
|
|
|
|
That needs no new mechanism either. A host already refuses a type it does not know; *this host
|
|
does not implement `package`* is the same refusal with a different reason, and the profile
|
|
reports which shapes it implements so the control plane never sends one it cannot do.
|
|
|
|
**`file`, `directory` and `action` are the portable floor.** They work anywhere there is a
|
|
filesystem and a way to run something, which makes a partial host a real thing rather than a
|
|
broken one.
|
|
|
|
**A partial host can join a mesh and cannot be the first node.** Every step of raising a
|
|
substrate is a `package`, a `container`, or an `action` against one, so the shapes it refuses
|
|
are exactly the ones a bootstrap needs. Its bundle says so rather than being an empty
|
|
placeholder.
|
|
|
|
**How such a host is started was left open here and is closed by
|
|
[ADR 0062](0062-a-host-may-be-episodic.md)** — by narrowing what is required rather than
|
|
building something. A host may be *episodic* rather than resident, and being killed by the
|
|
platform is disconnection, which is already ordinary.
|
|
|
|
## Consequences
|
|
|
|
- **Each implementation stays as sharp as its operating system allows.** The `LoadState`
|
|
distinction survives because the Arch host knows it is systemd. Nothing is degraded to fit an
|
|
interface spanning systems we do not run.
|
|
- **Delivery already worked this way**, which is the strongest sign this is the right seam: the
|
|
host ships as a package from the mesh's own repository
|
|
([ADR 0058](0058-delivery-ends-in-a-declaration.md)), and a `.pkg.tar.zst` is an Arch artifact.
|
|
A per-OS binary is consistent with what was already decided rather than an addition to it.
|
|
- **The container runtime is a choice within a host, not an OS split** — Arch runs docker or
|
|
podman — and it is **detected, not declared**, because adoption keeps what the machine already
|
|
has. Two are supported.
|
|
|
|
This is the opposite answer to the one above, for a reason rather than by preference.
|
|
Abstracting service managers is *lossy*: systemd and OpenRC are different models, and
|
|
`LoadState` has no equivalent. Container runtimes deliberately converged on one CLI, so almost
|
|
nothing is lost — checked against podman 6.1.0, `run`, `rm -f` and docker's own template
|
|
syntax for reading state and labels all work unchanged. **Only the probe differs**
|
|
(`{{.ServerVersion}}` against `{{.Version.Version}}`), which makes it a two-entry lookup
|
|
rather than an interface.
|
|
|
|
**One difference is not in the CLI and would have shipped silently.** Podman accepts
|
|
`--restart unless-stopped` and records it, and has no daemon to act on it: containers do not
|
|
come back after a reboot unless `podman-restart.service` is enabled, which it is not by
|
|
default. Every command reports success and the effect does not happen. That belongs in the
|
|
**declaration** — a node using podman is told to enable that unit — rather than in the host,
|
|
which keeps the host dumb and puts the difference where a person can read it.
|
|
- **A second operating system is now additive rather than a redesign** — write two appliers, ship
|
|
a package. And it will be designed against a real machine rather than a guess, which is the
|
|
point of not building the abstraction now.
|
|
- **The profile has to report the operating system**, and today it reports capabilities without
|
|
saying which system they belong to. Small, and needed before the control plane can tailor a
|
|
package name.
|
|
- **Nothing states the machine requirements yet.** A machine missing a capability fails at apply
|
|
time rather than being refused up front, even though the host already detects it. That is a
|
|
gap this record makes visible and does not close.
|
|
|
|
## References
|
|
|
|
- [ADR 0037](0037-the-host-applies-it-does-not-decide.md) — why the host is handed a package name.
|
|
- [ADR 0041](0041-the-host-depends-on-nothing.md) — one static binary, now per system as well as
|
|
per architecture.
|
|
- [ADR 0058](0058-delivery-ends-in-a-declaration.md) — delivery, which was already per-OS.
|
|
- [Research 012](../01-RESEARCH/012-the-minimum-viable-node/00-overview.md) — adoption keeping
|
|
the machine's configuration, which hardcoding a runtime contradicts.
|