Say what the host process is: a root service, installed as a package
The design described what the host does and never what it is at runtime. The words daemon, long-running, interval, poll and heartbeat appeared nowhere in it or in the relevant decisions. What exists is a command that runs and exits; what the design needs is a process holding a link. Nobody had written down that those differ, so several questions had no answer. 0057 settles them. It runs on every node -- the host is what makes a machine managed, so a machine without one is not a node. Root, because no useful subset of the job is unprivileged. A systemd unit, because something must survive a reboot to hold the link. It never manages its own unit. The temptation is obvious and it ends with a host stopping itself half way through an apply, leaving a machine with nothing running to fix it. The installation owns the host; the host owns everything else. Installed as a package, with a tarball as the floor. The package carries the unit file, the state directory and an upgrade path, which a bare binary does not. But the mesh's package repository is hosted on the mesh, so any route that needs the mesh to install the thing that joins the mesh is a circle -- the tarball is the path that must never acquire a dependency. Reconciles on start, on a declaration, on a timer and on reconnect. The timer is the one easy to leave out, and without it `owned` reports what the host applied rather than what is there -- ADR 0035 violated by omission. The records checker caught this commit on its first attempt: 05 listed 0057 in its frontmatter while 0057 is still proposed, and a to-be document may not rest on an unaccepted record. The section now says so in the body instead.
This commit is contained in:
@@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
status: proposed
|
||||||
|
date: 2026-08-27
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 0041-the-host-depends-on-nothing.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 57. The host is a root service, installed as a package, that never manages itself
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[`05-the-node-host.md`](../03-DESIGN/01-to-be/05-the-node-host.md) describes what the host *does*
|
||||||
|
and never says what it *is* at runtime. Searched: the words *daemon*, *long-running*, *interval*,
|
||||||
|
*poll* and *heartbeat* appear in none of it, nor in
|
||||||
|
[ADR 0038](0038-a-node-joins-by-linking-first.md) or
|
||||||
|
[ADR 0039](0039-the-link-is-the-security-boundary.md).
|
||||||
|
|
||||||
|
What exists today is a command that runs and exits — `mesh-host apply FILE`. What the design
|
||||||
|
requires is a process holding an outbound link to the control plane. Nobody wrote down that
|
||||||
|
these are different things, so several questions have no answer: does it reconcile on a timer,
|
||||||
|
what happens when the link drops, and who installs the unit that starts it — given that the host
|
||||||
|
is the thing that installs units.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### It runs on every node, and that is what a node is
|
||||||
|
|
||||||
|
[ADR 0036](0036-a-node-is-a-managed-machine.md) defines a node as a managed machine. **The host
|
||||||
|
is what makes it managed**, so a machine without one is not a node with a missing component; it
|
||||||
|
is not a node. There is no partial mode, no agentless node, and no second way in.
|
||||||
|
|
||||||
|
### It is a root service
|
||||||
|
|
||||||
|
**Root**, because there is no useful subset of its job that is not privileged: it writes under
|
||||||
|
`/etc`, installs packages, manages units, and runs containers. A host that dropped privilege
|
||||||
|
could apply almost nothing, and the almost is where the confusion would live.
|
||||||
|
|
||||||
|
**A service rather than a command**, because it holds the link, and something must survive a
|
||||||
|
reboot to hold it. The command-line entry points remain — they are how a person inspects and
|
||||||
|
rescues a machine — but the ordinary case is a unit that is always up.
|
||||||
|
|
||||||
|
### It never manages its own unit
|
||||||
|
|
||||||
|
**The host's own service file is not a resource the host applies.** The temptation is obvious —
|
||||||
|
it manages units, and its own unit is a unit — and it ends with a host stopping itself half way
|
||||||
|
through an apply, leaving a machine in a state nothing is running to fix.
|
||||||
|
|
||||||
|
So the boundary is: **the installation owns the host; the host owns everything else.** A
|
||||||
|
declaration that names the host's own unit is refused rather than obeyed.
|
||||||
|
|
||||||
|
### It is installed as a package, and a tarball is the floor
|
||||||
|
|
||||||
|
Two mechanisms, and the second is not a fallback for the first failing — it is what makes the
|
||||||
|
first possible.
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **package** — `pacman -S nox-mesh-host` | the ordinary path. Carries the binary, the unit file, the state directory, and an upgrade path |
|
||||||
|
| **tarball** — `curl … \| tar -xz` | the floor. One static binary, no repository, no distribution assumed |
|
||||||
|
|
||||||
|
**Why a package rather than only a binary.** [ADR 0041](0041-the-host-depends-on-nothing.md) says
|
||||||
|
copying the binary onto a machine is the whole installation, and that remains true of the
|
||||||
|
*binary*. But a unit file, a state directory and an upgrade path are real, and something has to
|
||||||
|
own them. A package that installs one statically linked binary plus a unit file adds no runtime
|
||||||
|
dependency — 0041 is about what must already be present for the host to work, not about how the
|
||||||
|
bytes arrived.
|
||||||
|
|
||||||
|
**Why the tarball must keep working.** The package lives in a repository, and the mesh's own
|
||||||
|
repository is hosted on the mesh. A first node cannot fetch from a mesh that does not exist yet,
|
||||||
|
and neither can a node whose mesh is down — which is exactly when somebody is trying to fix it.
|
||||||
|
**Any path that requires the mesh to install the thing that joins the mesh is a circle**, so the
|
||||||
|
tarball is the path that is never allowed to acquire a dependency.
|
||||||
|
|
||||||
|
### The mesh does not upgrade the host
|
||||||
|
|
||||||
|
A running process replacing its own binary and restarting mid-apply is the self-management
|
||||||
|
problem wearing a different hat. **Upgrading the host is an act on the machine**, by the package
|
||||||
|
manager, not a declaration the host applies to itself.
|
||||||
|
|
||||||
|
Recorded as a limit rather than a plan: it means a fleet-wide host upgrade is not currently a
|
||||||
|
mesh operation, and that will be felt.
|
||||||
|
|
||||||
|
### It reconciles on four triggers
|
||||||
|
|
||||||
|
| Trigger | Why |
|
||||||
|
|---|---|
|
||||||
|
| **start** | the machine may have changed while nothing was running |
|
||||||
|
| **a declaration arrives** | the ordinary path |
|
||||||
|
| **a timer** | drift. Something other than the host changed the machine — a person, a package upgrade replacing a config file |
|
||||||
|
| **reconnect** | it may have missed declarations while disconnected ([ADR 0036](0036-a-node-is-a-managed-machine.md)) |
|
||||||
|
|
||||||
|
**The timer is what makes the store's claim true.** Without it a machine that drifted stays
|
||||||
|
drifted until somebody changes a declaration, and `owned` reports what the host *applied* rather
|
||||||
|
than what is *there* — which is
|
||||||
|
[ADR 0035](0035-a-picture-is-read-from-what-runs.md) violated by omission.
|
||||||
|
|
||||||
|
**Ten minutes**, configurable. Short enough that drift is bounded by something a person would
|
||||||
|
notice anyway, long enough that a fleet is not doing constant work. The reconcile is cheap: it
|
||||||
|
asks the package database, the service manager and the container runtime about resources the
|
||||||
|
host already knows it owns.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Adoption becomes two concrete steps**, which is the point of writing this down:
|
||||||
|
install the package, then hand it a token. Nothing else.
|
||||||
|
- **The host gains a mode it does not have**, and it is the larger half of stage 3. Today every
|
||||||
|
entry point runs and exits.
|
||||||
|
- **Refusing to manage its own unit needs enforcing, not just stating.** A declaration naming
|
||||||
|
the host's unit must be refused by name, and that refusal is a test.
|
||||||
|
- **A host that cannot reach the mesh keeps reconciling from its store**, which is
|
||||||
|
[ADR 0036](0036-a-node-is-a-managed-machine.md) made operational rather than aspirational: a
|
||||||
|
disconnected node is not merely tolerated, it is actively holding its machine in the last
|
||||||
|
state it was told to hold.
|
||||||
|
- **The timer makes drift visible and also makes it loud.** A resource the host cannot apply
|
||||||
|
will now fail every ten minutes rather than once. That is correct and it needs somewhere to go
|
||||||
|
other than a log nobody reads — which is `observability`'s, and it does not exist yet.
|
||||||
|
- **Host upgrades are outside the mesh**, so a fleet-wide upgrade is currently manual. Nothing
|
||||||
|
here solves it and it should not be solved by giving the host a self-upgrade path.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0041](0041-the-host-depends-on-nothing.md) — the property the package must not break.
|
||||||
|
- [ADR 0036](0036-a-node-is-a-managed-machine.md) — what a node is, which this makes operational.
|
||||||
|
- [ADR 0039](0039-the-link-is-the-security-boundary.md) — the link the process exists to hold.
|
||||||
|
- [ADR 0051](0051-the-enrolment-token-carries-the-mesh.md) — the second of the two adoption steps.
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: in-progress
|
status: in-progress
|
||||||
code: [mesh-host]
|
code: [mesh-host]
|
||||||
updated: 2026-08-26
|
updated: 2026-08-27
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0030-the-repository-structure.md
|
- 02-DECISIONS/0030-the-repository-structure.md
|
||||||
- 02-DECISIONS/0036-a-node-is-a-managed-machine.md
|
- 02-DECISIONS/0036-a-node-is-a-managed-machine.md
|
||||||
@@ -113,6 +113,99 @@ work: a node is a node, and what varies between them is here rather than in the
|
|||||||
What this machine *is* — its identity, what it holds, what it has applied. Reported upward over
|
What this machine *is* — its identity, what it holds, what it has applied. Reported upward over
|
||||||
the link; never asked downward.
|
the link; never asked downward.
|
||||||
|
|
||||||
|
## The process
|
||||||
|
|
||||||
|
> **Proposed, not yet decided** —
|
||||||
|
> [ADR 0057](../../02-DECISIONS/0057-the-host-is-a-root-service-installed-as-a-package.md) is
|
||||||
|
> awaiting review. This section is written against it and moves into the document's `decisions:`
|
||||||
|
> when the record is accepted.
|
||||||
|
|
||||||
|
**One `root` service on every node, plus command-line entry points for a person.** A machine
|
||||||
|
without one is not a node ([ADR 0036](../../02-DECISIONS/0036-a-node-is-a-managed-machine.md)) —
|
||||||
|
the host is what makes a machine managed, so there is no agentless node and no partial mode.
|
||||||
|
|
||||||
|
Root because there is no useful unprivileged subset: it writes under `/etc`, installs packages,
|
||||||
|
manages units and runs containers.
|
||||||
|
|
||||||
|
### Installing it
|
||||||
|
|
||||||
|
Two steps, and there is nothing else:
|
||||||
|
|
||||||
|
```
|
||||||
|
# 1 — put the host on the machine
|
||||||
|
pacman -S nox-mesh-host
|
||||||
|
systemctl enable --now nox-mesh-host
|
||||||
|
|
||||||
|
# 2 — hand it the mesh
|
||||||
|
nox-mesh-host enrol --token <one-time token>
|
||||||
|
```
|
||||||
|
|
||||||
|
The token carries the broker's address, the fingerprint to expect, and the right to join
|
||||||
|
([ADR 0051](../../02-DECISIONS/0051-the-enrolment-token-carries-the-mesh.md)). After that the
|
||||||
|
node is in the mesh and takes declarations like every other one.
|
||||||
|
|
||||||
|
**On a machine with no package repository, or no mesh to serve one:**
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -fsSL https://<release>/mesh-host-<version>-x86_64.tar.gz | tar -xz -C /usr/local/bin
|
||||||
|
```
|
||||||
|
|
||||||
|
That path must never acquire a dependency. **The mesh's package repository is hosted on the
|
||||||
|
mesh**, so any installation route that needs the mesh is a circle — a first node cannot use it,
|
||||||
|
and neither can anyone repairing a mesh that is down.
|
||||||
|
|
||||||
|
### The unit
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=Novox Mesh node host
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=notify
|
||||||
|
ExecStart=/usr/bin/nox-mesh-host run
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5s
|
||||||
|
StateDirectory=mesh-host
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
**The package owns this file. The host never does.** It manages `service` resources, and its own
|
||||||
|
unit is a service — the temptation is obvious and it ends with a host stopping itself half way
|
||||||
|
through an apply, leaving a machine with nothing running to fix it. A declaration naming the
|
||||||
|
host's own unit is **refused**, and that refusal is a test rather than a convention.
|
||||||
|
|
||||||
|
The line to hold: **the installation owns the host; the host owns everything else.**
|
||||||
|
|
||||||
|
### What it does while running
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| holds the **link** | one outbound connection, node-initiated, nothing listening ([ADR 0039](../../02-DECISIONS/0039-the-link-is-the-security-boundary.md)) |
|
||||||
|
| **applies** what arrives | declarations of known shape, in the order given |
|
||||||
|
| **reads back and reports** | what it did, and what it now owns |
|
||||||
|
| **reconciles** | on start, on a declaration, on a timer, and on reconnect |
|
||||||
|
|
||||||
|
**The timer is the one that is easy to leave out.** Without it, a machine that drifted — a
|
||||||
|
person edited a file, a package upgrade replaced a config — stays drifted until somebody happens
|
||||||
|
to change a declaration. `owned` would then report what the host *applied* rather than what is
|
||||||
|
*there*, which is [ADR 0035](../../02-DECISIONS/0035-a-picture-is-read-from-what-runs.md)
|
||||||
|
violated by omission. **Ten minutes, configurable.**
|
||||||
|
|
||||||
|
**Disconnected is a working state, not a degraded one.** The host keeps reconciling against its
|
||||||
|
own store, so a laptop shut for a week comes back and reconciles rather than coming back and
|
||||||
|
asking what it is. That is
|
||||||
|
[ADR 0036](../../02-DECISIONS/0036-a-node-is-a-managed-machine.md) made operational.
|
||||||
|
|
||||||
|
### Upgrading it
|
||||||
|
|
||||||
|
**By the package manager, not by the mesh.** A running process that replaces its own binary and
|
||||||
|
restarts part-way through an apply is the self-management problem again. Recorded as a limit:
|
||||||
|
a fleet-wide host upgrade is not currently a mesh operation.
|
||||||
|
|
||||||
## Where a declaration comes from
|
## Where a declaration comes from
|
||||||
|
|
||||||
One behaviour, two sources
|
One behaviour, two sources
|
||||||
|
|||||||
Reference in New Issue
Block a user