# 142 — the mesh can now build and publish its own host *2026-09-30. Both of ADR 0141's named gaps are closed; the host is not yet placed by the mesh.* ## What ADR 0141's insight named, and what each cost > The remaining work is a way to build the host and a way to name a version in a path, and until both > exist nothing delivers a version and every machine takes the fallback. **1. Nothing could compile it.** The toolchain list was a closed set of typescript and python, and its own warning — every language is another implementation of the contracts modules share, so adding one commits to keeping N implementations in step — does not attach to Go. Go is how the host, the control plane and the builder are written, and none of them is a module in that sense: the host is what *applies* modules. Closed by a Go toolchain naming a new base module, `mesh-tools-go`. Named and not pinned, so the mesh answers with the copy it holds and moving compiler is a build rather than an edit to the control plane's source ([ADR 0044](../../02-DECISIONS/0044-a-public-name-is-provisioned-like-any-capability.md), [0142](../../02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md)). **2. A version could not reach the path.** An archive named a fixed path and nothing interpolated the build into it, so nothing could ask for `…/versions//`. Closed by `${version}` in any value of a resource that uses an archive or a bundle. **The version is the artifact's digest, short, and not the commit**: two builds of one commit are meant to be the same bytes — the toolchains are `-trimpath` for that — so a content-addressed version means an unchanged build resolves to the path it already had, where a commit-named path would move for an identical binary and recreate everything reading it. ## Three more things were in the way, and none was in the record Found by doing it, in the order they appeared: - **`sourcesFor` turned every entrypoint into a `.ts` file.** One language's file extension, written into the code that serves every language. The extension is the toolchain's now. - **The output directory was left to the compiler.** `tsc --outDir` makes one; `go build -o` writes into a directory and does not create it, failing with a message about a path rather than about a build. Made for every toolchain, because which compilers are forgiving is not something a reader should have to know. - **A bundle was refused if it named what it is built from.** The reason — a bundle is the module's own directory compiled whole — holds for an interpreted language and cannot hold for a compiled one: a Go repository carries several commands, the host and its bootstrap among them, and "the module's own directory" is then not a package at all. A compiled bundle may now say which package; the refusal stands for every interpreted one. And a fourth, in the base module itself: its first Dockerfile ran `apt-get`, and the golang image the mesh holds is Alpine. That is [issue 136](../136-a-module-may-name-a-program-the-machine-does-not-have/00-report.md) in an image rather than on a machine, and the build refused rather than a module failing later — which is the behaviour that issue wants. ## Measured: the mesh compiles its own host and publishes it to its own registry ``` $ mesh-controller build host-arch bundle artifact-store://mesh-host/host-arch/blobs/sha256:ad62528c… mesh-host 1, built on novox from b5196e97 ``` Fetched from the mesh's registry and opened: ``` mesh-host: ELF 64-bit LSB executable, x86-64, statically linked, stripped $ ./mesh-host help mesh-host — the node host ``` Statically linked matters: what a machine holds is a file rather than a container, so a binary needing a libc it did not bring is a delivery that works until a machine differs. **The host is a module now**, with one bundle for `arch` built from `cmd/mesh-host`. A system is required for a compiled artifact because a binary is pinned at link time so a host refuses to touch a machine it was not built for ([ADR 0005](../../02-DECISIONS/0005-the-node-host.md)); `arch` is what all four of this mesh's machines report themselves to be, and another system is another artifact and another build. ## What is left **The host module declares no resources**, so nothing places the built bundle on a machine yet. That is the next piece and it is the one with the interesting question in it: the resource is an archive unpacked to `…/versions/${version}/`, applied by the host that is running, and the bootstrap is not circular because the two are different versions ([ADR 0141](../../02-DECISIONS/0141-the-host-delivers-its-own-successor.md)). The host half of that mechanism — versions side by side, the newest runs, the running one stands aside between reconciles, rollback picks a directory — is built and tested and has never had a version to work on. **One thing worth noticing while writing it.** The systems list exists because "the difference between two of them is a C library, not a kernel" — and a static Go binary has no C library. So one build would in fact run on all three. The pin is then a policy (a host refuses a machine it was not built for) rather than a necessity, which is a reasonable thing to keep and is worth knowing is a choice. **And a sharper one, asked as a question and worth its own record.** The artifact's system is validated and then read by nothing: it does not reach the compiler, no machine is matched against it, and nothing chooses between two artifacts by it. The host built here is x86-64 because the build machine is, not because anything in the declaration said so — correct for this mesh by coincidence. That is [issue 159](../159-an-artifacts-system-is-checked-and-then-ignored/00-report.md). ## Delivered, started, and one fact short (2026-09-30, later) The loop closed. The launcher is delivered as a **file** resource rather than inside the archive, and that difference is the safety: a file is written atomically — temp file, then rename — so the running launcher keeps the inode it was started from, where an archive writes in place with truncate and would cut the script a running shell is reading. The manifest carries a second copy of the launcher and a test refuses any difference from `packaging/nox-mesh-host-launch`. On the workstation, in order: the version landed, the launcher was replaced, the running host saw a delivered version and stood aside, and after one restart of the unit the launcher started `/usr/lib/nox-mesh-host/versions/637f65559d16/nox-mesh-host`. **A host the mesh compiled, published, delivered and started.** It would then have refused the first declaration it was asked to apply. The host's Makefile links in two facts the mesh's toolchain does not, and one of them — the system it was built for — is read before anything is applied. That is [issue 161](../161-a-delivered-host-carries-none-of-its-link-time-facts/00-report.md), and the machine is back on its hand-placed binary until it is answered. **The fallback is what made that safe**, and it was not luck: the launcher runs the pinned version, or the newest delivered one, or the one placed by hand — so moving the delivered versions aside restored the machine in one step. ## Self-update works (2026-09-30, end of the day) ``` running: /usr/lib/nox-mesh-host/versions/093231796eb0/nox-mesh-host mesh-controller node show shanks host 093231796eb0 ``` One machine runs a host the mesh compiled, published, delivered and started, applying declarations and reporting the version it was delivered as. The two facts a delivered binary was missing are [issue 161](../161-a-delivered-host-carries-none-of-its-link-time-facts/01-resolution.md) and resolved: the system comes from the artifact, the version from where the binary sits. **Three machines still run a hand-placed host**, and rolling each forward is one assignment and one push. The control node is worth last. One thing this found on the way out: an archive cannot be undeclared, and the attempt stops the machine applying anything at all — [issue 162](../162-an-archive-cannot-be-undeclared/00-report.md). It is how undoing the first delivery froze the workstation, and it is not specific to the host. ## Every machine self-updates (2026-09-30, evening) ``` shanks 76f4566bef3d/nox-mesh-host active g14 76f4566bef3d/nox-mesh-host active novox 76f4566bef3d/nox-mesh-host active ace 76f4566bef3d/nox-mesh-host active mesh-controller status: (no host split) ``` The last delivery was unattended on all four: the fixed host was built, pushed, each machine stood aside exactly once for the genuinely newer version, and the delivered launcher started it — no restart by hand. A following push that delivered nothing new was applied and reported by every machine and stood nobody aside, which is the check [issue 163](../163-a-delivered-host-stood-aside-on-every-push-and-reported-nothing/00-report.md) asks for. **Two more faults on the way, both mine, both found by reading the machine rather than the success line.** A delivered host compared the newest delivered version against its link-time stamp rather than the version it was running, so it stood aside on every push and — because standing aside cancels the report — never reported again (163). And the adopted machine kept its found launcher as the adoption rule says, so the delivery there needed a `take` before the launcher moved. **The crossover needs one restart of the unit per machine, once.** The launcher process that was running on each machine was the old script, executing from its own inode; a new file beside it changes nothing until the unit restarts. Every subsequent delivery is unattended. **Timing, measured:** on a machine, hearing a declaration to reporting it applied is about three seconds. A push as the operator sees it takes 17–20 seconds, and the difference is the control plane composing the declaration before it sends. A `--wait` shorter than that reads as "did not report" for a machine that did; the three-minute default read as slowness for a machine that never would. Neither number is a defect being chased here, and both are worth knowing before reading a push's answer. ## What this leaves - [Issue 162](../162-an-archive-cannot-be-undeclared/00-report.md): an archive cannot be undeclared, so the host module — and any module with an archive — cannot be unassigned, and trying stops the machine applying anything. - [Issue 107](../107-a-declaration-carries-no-order/00-report.md) is unblocked: a declaration field is now a build and a push rather than an expedition. - Three stale version directories on the workstation from the first attempts, moved aside under `/var/lib/mesh-host/versions-held-back/`, and a backup of the adopted machine's hand-placed binary beside its state. Both are safe to delete and are not the mesh's to delete.