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.
321 lines
15 KiB
Markdown
321 lines
15 KiB
Markdown
---
|
|
layer: to-be
|
|
status: in-progress
|
|
code: [mesh-host]
|
|
updated: 2026-08-27
|
|
decisions:
|
|
- 02-DECISIONS/0030-the-repository-structure.md
|
|
- 02-DECISIONS/0036-a-node-is-a-managed-machine.md
|
|
- 02-DECISIONS/0037-the-host-applies-it-does-not-decide.md
|
|
- 02-DECISIONS/0038-a-node-joins-by-linking-first.md
|
|
- 02-DECISIONS/0039-the-link-is-the-security-boundary.md
|
|
- 02-DECISIONS/0008-a-failed-step-fails-the-job.md
|
|
- 02-DECISIONS/0041-the-host-depends-on-nothing.md
|
|
- 02-DECISIONS/0043-a-declaration-is-an-ordered-list-of-owned-resources.md
|
|
---
|
|
|
|
# The node host
|
|
|
|
Tier 0. The one thing ever installed by hand, and the only thing that changes a machine.
|
|
|
|
## What it is
|
|
|
|
A **statically linked binary that requires nothing to be present** — copy it onto a machine and
|
|
run it, and that is the whole installation
|
|
([ADR 0041](../../02-DECISIONS/0041-the-host-depends-on-nothing.md)). Written in Go, because the
|
|
job is system-level and because the host shares no code with any other tier.
|
|
|
|
A single binary with one job: **apply declared state on this machine**
|
|
([ADR 0037](../../02-DECISIONS/0037-the-host-applies-it-does-not-decide.md)). Overlay
|
|
membership, packet filtering, packages, services, containers and filesystems are not six
|
|
concerns it carries; they are six instances of the one.
|
|
|
|
It replaces three things that exist today
|
|
([`00-as-is/05`](../00-as-is/05-runtime-and-installation.md)): the three hand-run bootstrap
|
|
scripts — first node, joining, rescue — and the synchronisers that rewrite managed files
|
|
([`00-as-is/06`](../00-as-is/06-configuration-and-secrets.md)).
|
|
|
|
**What it is not:** it does not decide anything that needs another node, it never queries the
|
|
mesh database, and it has no listening surface.
|
|
|
|
## The parts
|
|
|
|
| Part | Owns |
|
|
|---|---|
|
|
| `apply` | reconciling declared state on this machine |
|
|
| `store` | local state, authoritative while disconnected |
|
|
| `link` | the single outbound connection to the control plane |
|
|
| `profile` | what this machine can be asked to do |
|
|
| `inventory` | what this machine is and has |
|
|
| `substrate.lock` | the pinned tier-1 descriptor, appliable with no mesh present |
|
|
|
|
### apply
|
|
|
|
Takes a **declaration** and makes the machine match it. Idempotent: applying the same
|
|
declaration twice changes nothing the second time, and applying it to a drifted machine
|
|
returns it.
|
|
|
|
Three properties, each following a recorded decision:
|
|
|
|
**A failed step fails the apply.** Not "logs and continues"
|
|
([ADR 0008](../../02-DECISIONS/0008-a-failed-step-fails-the-job.md)). A partial apply that
|
|
reports success is the mesh's most expensive shape.
|
|
|
|
**Each applier reads back.** Setting a value is not evidence the value took. The firewall is
|
|
asked whether the rule loaded; conntrack is asked what timeout it holds. This is `how-we-build`
|
|
§5 as a component requirement rather than a review habit.
|
|
|
|
**What was applied is recorded after it works, never before**
|
|
([ADR 0035](../../02-DECISIONS/0035-a-picture-is-read-from-what-runs.md)). A failed apply leaves
|
|
the machine in whatever state it reached, and nothing must claim otherwise.
|
|
|
|
### store
|
|
|
|
Local, and **authoritative while disconnected**. Not a cache of the control plane — the record
|
|
of what this node has applied and what it currently holds.
|
|
|
|
This is structural rather than convenient: if disconnection is an ordinary situation rather
|
|
than an exception ([ADR 0036](../../02-DECISIONS/0036-a-node-is-a-managed-machine.md)), the
|
|
store is what makes it ordinary. A laptop shut for a week comes back and reconciles; it does
|
|
not come back and ask what it is.
|
|
|
|
### link
|
|
|
|
The node's one connection to the control plane, and its security boundary
|
|
([ADR 0039](../../02-DECISIONS/0039-the-link-is-the-security-boundary.md)).
|
|
|
|
It is the broker connection that already exists
|
|
([ADR 0001](../../02-DECISIONS/0001-nodes-communicate-over-a-broker.md)) — outbound,
|
|
node-initiated, per-node addressed — carrying **per-node identity instead of a shared
|
|
credential**. The node owns no password. It owns an identity, and that identity is what it
|
|
presents.
|
|
|
|
What arrives is bounded by form: **declarations of known shape, never a command to run.**
|
|
|
|
**It must fail legibly.** A node that cannot link says which side refused and on what grounds.
|
|
A boundary that refuses without saying why is worse than the password it replaced.
|
|
|
|
### profile
|
|
|
|
What this machine can be asked to do — a graphical session, a container runtime, an
|
|
architecture, a network position.
|
|
|
|
**Detected, never assumed.** A package being installed does not mean a capability is present
|
|
([04-ISSUES/007](../../04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md)): a
|
|
capability is real when it is present, running and working, and the difference is the whole
|
|
point of detecting it.
|
|
|
|
The profile is what makes [ADR 0036](../../02-DECISIONS/0036-a-node-is-a-managed-machine.md)
|
|
work: a node is a node, and what varies between them is here rather than in the definition.
|
|
|
|
### inventory
|
|
|
|
What this machine *is* — its identity, what it holds, what it has applied. Reported upward over
|
|
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
|
|
|
|
One behaviour, two sources
|
|
([ADR 0038](../../02-DECISIONS/0038-a-node-joins-by-linking-first.md)):
|
|
|
|
| Situation | Source |
|
|
|---|---|
|
|
| no mesh reachable | `substrate.lock` — the pinned bundle the host carries |
|
|
| mesh reachable | the control plane, over the link |
|
|
|
|
**The first node is not a different kind of node.** It is a node whose mesh is not up yet. It
|
|
applies the bundle it carries, the control plane comes up on top of it, and from that moment it
|
|
takes declarations like every other node. Its specialness is temporary and self-erasing.
|
|
|
|
**A joining node does the minimum to be reachable and nothing else** — an identity, an address,
|
|
one peer — and then stops deciding. It does not compute the overlay; it needs one peer to reach
|
|
the mesh, and the full peer set arrives derived.
|
|
|
|
## What a declaration is
|
|
|
|
Settled by [ADR 0043](../../02-DECISIONS/0043-a-declaration-is-an-ordered-list-of-owned-resources.md).
|
|
|
|
**JSON**, because the host has no dependencies to spend and the standard library carries no
|
|
YAML. **An ordered list of typed resources**, each with a stable identity — the order is stated
|
|
rather than derived, because deriving it would be the host deciding the thing most likely to
|
|
differ from what the control plane intended.
|
|
|
|
**Unknown is refused, never skipped.** An unknown version, type or field refuses the whole
|
|
declaration. A host that skipped what it did not understand would apply most of it and report
|
|
success.
|
|
|
|
**Complete for what the host owns, and only that.** It removes what it previously applied and
|
|
is no longer declared — a fact it holds, from the store, rather than an inference — and never
|
|
removes anything it did not create.
|
|
|
|
**Addressed.** A host with an identity refuses a declaration addressed elsewhere; a host
|
|
without one, applying the bundle it carries, has nothing to check against.
|
|
|
|
## Build order
|
|
|
|
Staged so each stage is verifiable in the lab before the next exists.
|
|
|
|
**1 — profile and inventory.** The host runs on a machine, detects what it can do, and reports
|
|
what it is. No control plane, no declarations, no network. Verifiable immediately: the lab's
|
|
`place:` gains its first implementation, and a raised scenario finally contains something.
|
|
|
|
**2 — apply, from the bundle.** The host applies `substrate.lock` with no mesh present. This is
|
|
the first node's path, and it is the claim the skeleton's Move 1 rests on and has never proved:
|
|
that one host can raise the substrate alone.
|
|
|
|
Raising the substrate needs six shapes in the host's vocabulary, and **all six are built**:
|
|
|
|
| | | |
|
|
|---|---|---|
|
|
| `directory`, `file`, `service` | **built** | the first three |
|
|
| `package` | **built** | present, never upgraded, and **never uninstalled** — the host cannot know what else needs it, so dropping one is *forgotten*, not *removed* |
|
|
| `container` | **built** | pinned by digest ([ADR 0046](../../02-DECISIONS/0046-the-installer-fetches-what-it-pins.md)); identified by a label carrying a digest of the declaration that made it, because a runtime normalises what it is given and that is indistinguishable from drift |
|
|
| `action` | **built** | bundle-only ([ADR 0047](../../02-DECISIONS/0047-the-bundle-may-carry-actions-the-link-may-not.md)); verify is mandatory and is the idempotency check as well as the read-back |
|
|
|
|
**The parser enforces the boundary rather than the caller remembering it.** `Parse` refuses an
|
|
action and is what the link uses; `ParseTrusted` permits one and is what the bundle uses. The
|
|
safe path is the default and the permissive one has to be named.
|
|
|
|
**The lab still cannot exercise the last three**, and that is now the only thing in the way: a
|
|
scenario is a closed address space, so nothing can be fetched there, and its machines carry no
|
|
container runtime. All three were instead verified against a real machine — a container created,
|
|
labelled, replaced when its declaration changed, exec'd into and removed; an action that exits
|
|
zero and satisfies nothing failing the apply. That is lab-installation work
|
|
([`04-lab-installation.md`](04-lab-installation.md)) rather than a constraint on the design, but
|
|
until it is done the substrate bootstrap has no end-to-end test.
|
|
|
|
**3 — link and store.** The node connects, receives declarations, and holds what it applied.
|
|
|
|
**4 — enrolment.** The one genuinely new mechanism in
|
|
[ADR 0039](../../02-DECISIONS/0039-the-link-is-the-security-boundary.md); everything else there
|
|
is configuration of what already runs.
|
|
|
|
Stage 1 is deliberately the smallest useful thing. The lab currently raises **empty machines**
|
|
([`00-as-is/11`](../00-as-is/11-the-lab.md)) because `place:` has nothing to place; stage 1 ends
|
|
that, and every later stage is tested by a lab that already works.
|
|
|
|
## How it is verified
|
|
|
|
**The lab is the harness.** A scenario places a host on a machine and asserts what it did —
|
|
against a real hypervisor, with the boundary never mocked
|
|
([ADR 0034](../../02-DECISIONS/0034-a-test-defends-a-decision.md)).
|
|
|
|
Each decision above owes a test:
|
|
|
|
| Decision | What asserts it |
|
|
|---|---|
|
|
| 0037 — the host never queries the mesh database | no database client in the dependency tree; a dependency-direction lint failing on an upward import |
|
|
| 0038 — one behaviour, two sources | the same code path raises a first node and joins a second |
|
|
| 0039 — a node holds no shared credential | a raised node's store contains no credential to any service |
|
|
| 0036 — disconnection is a situation | a node cut off and returned reconciles without being re-adopted |
|
|
| 0008 — a failed step fails the apply | an apply with a failing step reports failure |
|
|
|
|
## Open
|
|
|
|
- **Whether one host can raise the substrate alone.** Move 1 assumes it. Stage 2 tests it, and
|
|
if it is false the tier boundary moves.
|
|
- **What the host carries versus what it finds.** It manages `wg`, `nft`, `pacman`, `docker`;
|
|
it does not contain them, and how it obtains one it lacks is undecided —
|
|
[04-ISSUES/007](../../04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md).
|
|
- **Six vocabularies.** Zero dependencies, but the host must still know what a peer, a rule, a
|
|
package, a unit, a container and a dataset *are*. Nothing has measured that surface, and it is
|
|
the residue of the question [`host-size.md`](../../01-RESEARCH/006-mesh-from-scratch/host-size.md)
|
|
answered.
|
|
- **Rescue.** [ADR 0038](../../02-DECISIONS/0038-a-node-joins-by-linking-first.md) suggests it is
|
|
a node whose local state is discarded so the mesh re-derives it, and does not decide it.
|
|
- **What may expire.** An identity needing refresh to stay valid would make a laptop fail for
|
|
being a laptop ([ADR 0036](../../02-DECISIONS/0036-a-node-is-a-managed-machine.md)).
|