The supervision was already right — a clean exit means the host stood aside and the launcher runs what is on disk, failures are counted, and a rollback happens at the limit. Two things made it dead code: nothing told the running host a successor was waiting, and the rollback resolved its known-good version through pacman, which no machine here uses and which two of three operating systems do not have. Keeping a version rather than a path was the clue. Versions live side by side in directories named for them; the newest runs; the running one stands aside between reconciles; a reconcile that completes records itself and retires what is older than its predecessor; rollback starts that predecessor. No new resource kind and nothing new on the bus — an archive already fetches by digest, and the path written is never the path executing. Answers issue 142.
77 lines
4.5 KiB
Markdown
77 lines
4.5 KiB
Markdown
---
|
|
status: located
|
|
opened: 2026-09-29
|
|
located-in:
|
|
- mesh-host internal/upgrade
|
|
- mesh-host cmd/mesh-host
|
|
- mesh-controller (no build source for the host; no resource delivers it)
|
|
fixed-by:
|
|
amended-design: 03-DESIGN/01-to-be/05-the-node-host.md
|
|
---
|
|
|
|
# 142 — The host is the one thing the mesh does not deliver
|
|
|
|
## What was observed
|
|
|
|
A change to the host was merged and could not reach any machine without a person copying a file.
|
|
|
|
Checked on the mesh of four machines, 2026-09-29:
|
|
|
|
- **The host is not a build target.** Asked what had been built for it, the control plane answered
|
|
`nothing has been built for mesh-host`. A merge on the forge builds every changed module and the
|
|
control plane itself, because the control plane is a module. The host is not one, and nothing
|
|
builds it.
|
|
- **No declaration delivers it.** No resource kind names an executable to place on a machine, and
|
|
nothing on a machine fetches one.
|
|
- **The half that recovers from a bad host exists and is unused.** `internal/upgrade` can report that
|
|
the executable this process started from has been replaced on disk, and records which version last
|
|
completed a reconcile so a shell script can roll back a host that will not start. The launcher reads
|
|
that record and rolls back. But `Replaced()` is called by nothing except its own tests — the
|
|
recovery is wired and the delivery was never built.
|
|
- **Every machine runs a byte-identical binary, stamped by hand.** All four carry the same size and
|
|
the same timestamp, from the last time somebody built it on a workstation and copied it out. No
|
|
package owns the file.
|
|
|
|
## Why it matters beyond this instance
|
|
|
|
**The component that implements updating is the one thing not updated.** The mesh's stated shape is
|
|
that a push produces the right builds and they reach the machines running them with nobody asking. It
|
|
is true of every module and of the control plane. It is false for the host, which is what applies all
|
|
of them.
|
|
|
|
**It is a bootstrap problem being answered by a person.** The host cannot be an ordinary module
|
|
because the host is what applies modules; a module that replaces the thing applying it has to survive
|
|
its own replacement. That is a real difficulty, and the work already done — noticing that the
|
|
executable changed, recording a known-good version, a launcher that rolls back — is the hard half of
|
|
solving it. What is missing is the easy half, and its absence makes the hard half dead code.
|
|
|
|
**A hand-copied binary has no record anywhere.** Nothing says which version a machine runs, so
|
|
nothing can say a machine is behind, and the mesh's own account of itself — every machine current with
|
|
its source — cannot include the host. Four machines agreeing today is luck, not a property.
|
|
|
|
**And it silently gates any change that starts in the host.** A change that needs the host to report
|
|
something new cannot be rolled out by merging it: the control plane must wait for a person, and until
|
|
then it either refuses what depends on the new report or renders something wrong. That cost is paid by
|
|
every future change of this shape, and it was paid today.
|
|
|
|
## Open questions
|
|
|
|
- How is the host delivered without being applied by itself? A candidate shape: the host is built like
|
|
anything else, published as an artifact, and the *running* host fetches and stages the next one, then
|
|
stands aside — which is what `Replaced()` was written for and what the launcher's rollback already
|
|
covers.
|
|
- **Should this ride the bus, rather than becoming a mechanism of its own?** Everything else that
|
|
reaches a machine already does: a declaration is sent over it, a report comes back over it, and a
|
|
build announces what it produced on it, which is how a module's new version reaches the machines
|
|
running it. A host build announcing itself the same way, consumed by the host already running,
|
|
would make this the existing mechanism pointed at one more artifact rather than a second way of
|
|
delivering things. It would also give the machine somewhere to say which host it is running, on the
|
|
report it already sends.
|
|
- What records which version of the host a machine runs, so "behind" is answerable? Nothing does now.
|
|
- Does the host's version belong in its report, beside the other facts a machine states about itself?
|
|
- Who decides when a machine takes a new host — the mesh, on a build, or an operator per machine as
|
|
with converging? The rollback path means a bad host costs a reconcile rather than a machine, which
|
|
argues for the former.
|
|
- Does the same gap apply to the launcher and the units beside the binary, which are also files no
|
|
declaration names?
|