Files
mesh-host/README.md
T
jschoubben 9d8239afe8 Stage 2 — the host applies a declaration
A declaration is JSON, versioned, and an ordered list of resources with stable
identities (novox/hq ADR 0043). The vocabulary is directory, file and service,
and anything outside it — an unknown version, type or field — refuses the WHOLE
declaration. A host that skipped what it did not understand would apply most of
what it was sent and report success.

It converges rather than executes: applying twice changes nothing the second
time, and applying to a drifted machine returns it. A mode is maintained rather
than set, because a permission applied at creation is not a permission held —
this repository has paid for that once already.

It owns a footprint and only that. What it applied and is no longer declared is
removed; what it did not create is never touched. Removal runs FIRST, because a
resource leaving a declaration while another arrives at the same path is an
ordinary rename, and removing afterwards would delete the file just written.

The store arrives here rather than at stage 3, as ADR 0043 predicted: nothing
can be removed without knowing what was applied. It is written atomically,
refuses to start empty when it exists and cannot be read — believing it owns
nothing would leave everything behind forever — and is saved even when an apply
fails, because what was applied before the failure is on the machine either way.

Three faults found by running inside a raised machine rather than by reasoning:

A unit that DOES NOT EXIST reads as `inactive` from `systemctl is-active`,
exactly as a stopped one does. So declaring a unit stopped reported success for
a unit the host cannot manage at all — absence read as satisfaction, which is
04-ISSUES/007 wearing a different hat. LoadState separates them.

Removing an orphaned service whose unit has since been uninstalled failed the
whole apply, and a host holding such a record could then apply NOTHING, ever,
with no way out but editing its state by hand. Removal is now idempotent for the
same reason os.RemoveAll is.

And the flag parser was wrong in the same way twice: fixing `mesh-host inventory
--json` by taking the subcommand off the front left `mesh-host apply decl.json
--dry-run` broken identically, because the standard library stops at the first
non-flag argument wherever that argument is. Parsed in a loop now.

30 new tests, 55 in total.
2026-08-26 02:14:25 +02:00

136 lines
6.3 KiB
Markdown

# mesh-host
Tier 0 of the Novox Mesh. The one thing ever installed by hand, and the only thing that changes
a machine.
```
scp mesh-host root@machine:/usr/local/bin/
mesh-host profile
```
That is the whole installation. One statically linked binary, nothing else present, no runtime
to install first ([`novox/hq` ADR 0041](https://git.novox.be/novox/hq)).
## What it is for
**Apply declared state on this machine.** Overlay membership, packet filtering, packages,
services, containers and filesystems are not six concerns it carries; they are six instances of
the one.
**It does not decide.** Anything needing knowledge of another node is the control plane's, and
the host never queries the mesh database. It receives declarations and applies them.
## What exists today
**Stages 1 and 2.** It reports what a machine is, and it applies a declaration to one. It
connects to nothing and listens on nothing — what it applies comes from a file.
```
mesh-host profile what this machine can be asked to do
mesh-host inventory what this machine is, and what it holds
mesh-host apply FILE make this machine match a declaration
mesh-host owned what this host has applied and still owns
--json machine-readable
--state where this node keeps what it knows
--dry-run read and check the declaration, change nothing
```
```
$ mesh-host profile
linux/amd64
yes container-runtime 29.7.2
no firewall nft exited 1: Operation not permitted (you must be root)
yes graphical-session x11: :1
yes overlay wg0
yes package-manager pacman 7.1.0
no privileged effective uid 1000, not 0
yes service-manager degraded
cannot be asked to: [firewall privileged]
```
## Applying
A declaration is JSON, versioned, and an **ordered list** of resources — the order is stated
rather than derived, because deriving it would be the host deciding
([`novox/hq` ADR 0043](https://git.novox.be/novox/hq)). The vocabulary is `directory`, `file`
and `service`, and **anything outside it refuses the whole declaration**: a host that skipped
what it did not understand would apply most of a declaration and report success.
```json
{"declaration":1,"resources":[
{"id":"mesh-etc","type":"directory","path":"/etc/mesh","mode":"0755"},
{"id":"node-conf","type":"file","path":"/etc/mesh/node.conf","content":"role = anchor\n","mode":"0640"},
{"id":"journal","type":"service","unit":"systemd-journald.service","state":"running"}
]}
```
**It converges rather than executes.** Applying twice changes nothing the second time; applying
to a drifted machine returns it. A mode is *maintained*, not merely set — a permission applied
at creation is not a permission held.
**It owns a footprint, and only that.** What it applied and is no longer declared is removed;
what it did not create is never touched. It knows which is which because it recorded what it
did, after each thing worked.
**A failed step fails the apply.** No step runs after a failure, and the error carries what had
already been done — the machine is in whatever state that left it, and pretending otherwise is
the fault this exists to prevent.
Stages 3 and 4 — the link, and enrolment — are designed and not built.
## A capability is detected, never assumed
The reason this is the first thing built rather than a detail of it.
**An installed package is not a capability.** A container client on disk with its daemon down
looks exactly like a working runtime, and a node assigned work on that basis fails at the
moment the work arrives. So every detector runs something that only succeeds if the thing is
**functioning** — the daemon is asked for its version, the package database is queried, the
firewall is asked to list a ruleset, which needs the privilege as well as the tool.
**Every verdict says how it knows.** A capability reported absent with no reason is a fault
nobody can act on. The reason is what a person reads when a node will not take work they
expected it to take.
**A unit that does not exist is not a unit that is stopped.** `systemctl is-active` says
`inactive` for both, so declaring a unit stopped reported success for a unit the host cannot
manage at all. `LoadState` separates them. Found by applying inside a raised machine, not by
reasoning — and its sibling: removing an orphaned service whose unit has since been uninstalled
used to fail the whole apply, which left a node able to apply *nothing*, ever.
**Exit codes are not the whole answer.** Found by running against a real machine rather than by
reasoning: `systemctl is-system-running` exits non-zero for every state except `running` —
including `degraded`, which means some units failed and the init is emphatically there. Reading
the exit code reported no service manager on a machine whose init it was. That is the same
fault in the mirror — installed-but-broken reported present, working-but-imperfect reported
absent — and both place work wrongly.
## Building
```
go test ./... structure and logic, and the same checks against this machine
CGO_ENABLED=0 go build -ldflags="-s -w" -o mesh-host ./cmd/mesh-host
```
Roughly 3 MB, static, no dynamic dependencies. Cross-compiles with `GOOS`/`GOARCH`; a host is
built once per architecture and copied, never built on the machine it runs on.
**Mocking the boundary is forbidden** ([`novox/hq` ADR 0034](https://git.novox.be/novox/hq)).
Every detector is exercised against a fake runner for its logic *and* against this machine for
its behaviour. The tests do not assert which capabilities a machine has — that varies, and is
the point of detecting — they assert that detection tells the truth about whatever is there.
## Where the reasoning lives
Design and decisions are in [`novox/hq`](https://git.novox.be/novox/hq), not here. This
repository carries implementation and does not carry decisions.
- `03-DESIGN/01-to-be/05-the-node-host.md` — what this is and the order it is built in
- `02-DECISIONS/0037-the-host-applies-it-does-not-decide.md` — the one concern
- `02-DECISIONS/0038-a-node-joins-by-linking-first.md` — one behaviour, two sources
- `02-DECISIONS/0039-the-link-is-the-security-boundary.md` — a node owns no password
- `02-DECISIONS/0041-the-host-depends-on-nothing.md` — why this is a static binary, and Go
- `04-ISSUES/007-an-installed-package-is-not-a-capability` — why detection works this way