jschoubben 337126603e Complete the host's vocabulary: package, container, action
The three shapes the substrate bootstrap needs and the host did not have. Until
now tier 1 could not be raised at all -- step 0 is a package, step 1 a
container, steps 2 and 3 actions -- so every line of the tier 1 and 2 designs
was unbuildable.

package -- present, never upgraded, never uninstalled. Removal is "forgotten",
not "removed": the host cannot know what else needs the package, uninstalling a
container runtime because a declaration changed would stop every container on
the node, and the machine may have had it before the mesh saw it. Reporting it
removed would claim an effect the host declined to have.

container -- identified by a label carrying a digest of the declaration that
made it. Comparing every field the runtime reports cannot be done reliably: a
runtime normalises, defaults and reorders what it is given, and that is
indistinguishable from real drift. There is no in-place update; a container's
configuration is fixed at creation, so any change is a replacement, and saying
so beats a partial update that leaves the running thing half-declared. This is
the one shape the host removes, because it is the one the host created.

action -- bundle-only, per ADR 0047. Verify is mandatory and does double duty:
it is the idempotency check as well as the read-back. The host does not know
what a database is, so "is it already there" is a question only the declaration
can ask. `in` runs the action inside a named container, which steps 2 and 3
need.

Parse now refuses actions; ParseTrusted permits them. The safe path is the
default and the permissive one has to be named. The bundle and a local file
handed to a root process use ParseTrusted; the link will use Parse.

Also replaced the per-type "fields this type ignores" check with a field-set
diff stated as what each type USES. The negative form needs every type revisited
whenever a field is added, and the one nobody revisits silently accepts a field
it will never read.

Images must be pinned by digest (ADR 0046). A bundle naming a tag pins nothing.

Verified against a real machine, not only fakes: an action ran and was
idempotent on the second apply; an action that exits zero and satisfies nothing
fails the apply; a real container was created, labelled, replaced when its
declaration changed, exec'd into, and removed; a real package query round-
tripped. Each new test was also confirmed to fail on an injected fault -- five
injections, each breaking exactly its own test.

One existing test changed: a vanished unit is now reported "forgotten" rather
than "removed", which is what actually happened.
2026-08-27 20:36:27 +02:00
2026-08-26 22:06:54 +02:00

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).

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 from a file
mesh-host reconcile      make this machine match the declaration this host carries
mesh-host bundle         show what this host carries
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). 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.

{"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.

The bundle a host carries

A host built for a machine carries its declaration inside the binary:

make host BUNDLE=path/to/substrate.lock

mesh-host reconcile then applies it. That is the first node's path — no mesh present, nothing fetched, nothing else copied onto the machine. copy it and run it stops being true the moment a second file has to arrive with it, which is why the bundle is embedded rather than beside it.

A default build carries nothing and refuses to reconcile, saying so. A host that applied nothing and reported success would look exactly like one that raised a first node, and the difference would surface later as a mesh that never came up with nothing to point at.

Stages 3 and 4 — the link, and enrolment — are designed and not built.

What stage 2 does not yet prove

The design defines stage 2 as the host applies substrate.lock with no mesh present, and calls out the claim underneath it: that one host can raise the substrate alone.

The mechanism is proved — a sealed machine, one binary, and it configures itself from what it carries. The claim is not. The substrate is four container services, and:

  • the vocabulary has no container type, because a container needs an image and where images come from is open (novox/hq research 012);
  • what belongs in a substrate is not known — the closure for a one-node mesh is what research 011 and 012 exist to answer;
  • and the machine used to test this has no container runtime, because a sealed network cannot install one.

So substrate.lock here is a real bundle with a placeholder's content. Saying that plainly beats shipping a host that claims a substrate it has never raised.

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). 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, 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
S
Description
Novox Mesh — tier 0. One statically linked binary that requires nothing present. Applies declared state on a machine; decides nothing.
Readme
3.3 MiB
Languages
Go 98.2%
Shell 1.5%
Makefile 0.3%