The tools this host runs are pinned and hash-verified
- Status: accepted
- Scope: agentic-UDPspeeder-simd
- Date: 2026-08-10
Context and Problem Statement
The methodology pins the software a host builds. A component records a source
pin, a build recipe, an artifact hash, and may record a deps-bundle so the
build reproduces offline from inputs fixed in advance rather than from whatever a
network fetch returns that day.
It says nothing about the tools the host itself runs. Those were reached
through whatever this machine happened to carry. On 2026-08-10 that meant node
and allium from ~/.local, java from /usr/sbin, and tla2tools.jar from a
home share. Nothing recorded which versions those were, and nothing would have
noticed had they drifted.
The failure this invites is the one the OpenWrt work already taught. A package build that fetches its dependencies at build time is not reproducible, however carefully its source is pinned, because the fetch is the unpinned input. The same holds one level up: a gate is only as trustworthy as the binary that runs it, and an unpinned binary makes the gate's verdict a property of the machine rather than of the change under it.
Two concrete symptoms had already appeared. A generated pre-push hook carried
$HOME/.local/bin/node, so it worked here and nowhere else. And a stale
target/release/host-lint sat six minor versions behind the binary on PATH,
which turned a green suite red and cost an hour before the cause was found.
Decision
Every tool this host runs installs to a project-local path, at a version and a sha256 recorded in the repository.
.env names each tool's version and its in-tree path.
tools/install-tools.sh fetches each one into .host-tools/ and verifies its
sha256 before unpacking it, refusing on a mismatch. .host-tools/ is
gitignored: the recipe is committed, the binaries are not. Scripts read .env
rather than probing PATH, so a hook or a lane names the tool it means.
Where a publisher ships its own checksum file, the script verifies against that
rather than a hash we copied: node against the published SHASUMS256.txt, the
JDK against the Adoptium API's checksum. Where none exists, the recorded hash is
the one this project pinned, and a change to it is a visible diff.
Two mechanisms exist, and each tool uses exactly one. host-lint is a Where-room
component: .host-software already carries its pin, its artifact sha256, and the
hooks it installs, and software --install-hooks verifies the binary against
that hash. It is not repeated in .env, because a hash recorded twice is a hash
that can disagree with itself. Everything else, node, the JDK, tla2tools.jar,
host-lifecycle and allium, is pinned in .env and fetched by the script.
Consequences
A fresh clone gets the same tool versions as this one, and a drifted download fails loudly at the point of installation rather than quietly at the point of use.
The cost is that a tool upgrade is now a commit: the version and the hash both
move in .env, and that is the intent.
Two limits are worth stating plainly. The install verifies provenance, not
behaviour, so a tool that is authentic and wrong still passes. And .host-tools/
holds around 550 MB, mostly the JDK, which is a real cost on a machine that
already carries one.
This decision is scoped to this host. The general form, a pinned and
hash-verified tool set recorded in .host-software and checked by
software --verify-setup, is a change to the shared methodology and belongs
upstream in the template, proposed there rather than settled here.
Notes
allium is pinned to 3.5.0 rather than the template's 3.4.2. Neither version
installs from crates.io without --locked: allium-cli resolves a newer
allium-parser whose analyze_with_cross_module takes more arguments than the
call site supplies. 3.5.0 is the version tools/allium carries.