hub v0.102.0 — refuse to vouch a version that cannot be installed (R-273)

The guard owed since Friday morning. Agent v0.128.0 was published as a package
and never git-tagged; it was vouched here; and because felhom-host-install.sh
fetches an agent's configs from raw/tag/v<version>/configs/, every fresh install
and reinstall died at step 5 of 8, as root, on a virgin machine, for most of a
day. handleSetArtifacts is the sole UI path to SetArtifactManifest, so the check
belongs here and nowhere else.

TWO LEGS, because both failed inside two days: the TAG (missing, R-273) and the
PACKAGE (pruned from under a still-tagged version, R-287). Either alone catches
one of them.

It asserts configs/felhom-mkfs-guarded.sh -- the FIRST of the installer's sixteen
fetch_raw calls and literally the file whose 404 broke Friday. A test pins the
constant, because probing a path that merely exists is how it stayed invisible.
The golden gets the package leg only: it has no config tree, so a tag probe would
assert something the installer never does.

"Could not verify" refuses too, with its own message. No override -- the registry
is the operator's own server, so if it is unreachable the vouch can wait.

ORDERING IS LOAD-BEARING AND A FAILING TEST FOUND IT. The probes run before
resolveArtifactSHA, whose flash conflates "missing", "unreachable" and "bad sha".
Probing first means an unreachable registry is reported as unreachable.

Five scenarios each naming the wrong outcome; three red-proofs, mutations asserted
applied and reverted. With the tag check removed, scenario A reports artifacts_set
-- Friday's exact defect returns.
This commit is contained in:
2026-08-09 19:13:45 +02:00
parent 6088afcbed
commit b55fc17d82
5 changed files with 428 additions and 0 deletions
+78
View File
@@ -1110,6 +1110,12 @@ func normalizeSHA256(raw string) (string, bool) {
// hash that can never match.
var sha256HexRe = regexp.MustCompile(`^[0-9a-f]{64}$`)
// installerProbeConfig is the FIRST config felhom-host-install.sh fetches from a vouched agent's
// tag (step 5/8, `fetch_raw "configs/felhom-mkfs-guarded.sh"`). It is the exact file whose 404 broke
// every install on 2026-08-09, and it is what the installability gate asserts — the path the
// installer uses, never a path that merely exists.
const installerProbeConfig = "configs/felhom-mkfs-guarded.sh"
func (s *Server) handleSetArtifacts(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
@@ -1122,6 +1128,78 @@ func (s *Server) handleSetArtifacts(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/configuration?flash=artifact_ver_invalid", http.StatusSeeOther)
return
}
// --- INSTALLABILITY GATE (R-273) -----------------------------------------------------------
//
// THE INCIDENT. On 2026-08-08 agent v0.128.0 was published as a package and never git-tagged.
// It was vouched here. felhom-host-install.sh fetches an agent's config files from
// `raw/tag/v<version>/configs/`, so EVERY fresh install and every reinstall died at step 5 of 8
// — as root, on a virgin machine — for most of a day. Nothing checked at the moment of risk, and
// the moment of risk is this handler: it is the sole UI path to SetArtifactManifest.
//
// TWO INDEPENDENT LEGS, because both have failed within two days of each other:
// the TAG — missing on 2026-08-08 (R-273)
// the PACKAGE — pruned out from under a still-tagged version on 2026-08-08/09 (R-287)
// Checking one would have caught one of them.
//
// IT ASSERTS THE PATH THE INSTALLER ACTUALLY FETCHES. `configs/felhom-mkfs-guarded.sh` is the
// FIRST of the sixteen `fetch_raw` calls (felhom-host-install.sh, step 5) and is literally the
// file whose 404 broke Friday. Probing some other path that merely exists is how that failure
// stayed invisible.
//
// IT RUNS BEFORE resolveArtifactSHA, and that ordering is load-bearing. The sha lookup fails
// with `artifact_sha_invalid`, whose text reads "version missing / Gitea unreachable / bad sha"
// — three different facts in one message. If it ran first, an unreachable registry would be
// reported to the operator as a possibly-missing artifact. Probing first means the operator is
// told which of those it actually is. (Found by scenario E failing against the first draft.)
//
// UNDETERMINED IS ALSO A REFUSAL, and it says something different. A warning beside a success is
// read as a success, and this project has the scars; so an unreachable registry refuses too,
// with its own message. There is NO OVERRIDE: the registry is the operator's own server, and if
// it cannot be reached then vouching is moot rather than urgent.
if s.gitea != nil {
ctx := r.Context()
type probeTarget struct{ label, pkg, file, version, repo string }
targets := []probeTarget{}
if agentVer != "" {
targets = append(targets, probeTarget{"agent", pkgAgent, fileAgent, agentVer, "felhom-agent"})
}
if goldenVer != "" {
targets = append(targets, probeTarget{"golden", pkgGolden, fileGolden, goldenVer, ""})
}
for _, t := range targets {
// Leg 1 — the tag, but only where a tag is what the installer uses. The golden is
// fetched as a package by version and has no config tree, so a tag probe on it would be
// asserting something the installer never does.
if t.repo != "" {
res := s.gitea.TagServesFile(ctx, t.repo, "v"+t.version, installerProbeConfig)
if res.Err != nil {
s.logger.Printf("[WARN] artifact vouch REFUSED: could not verify the %s tag v%s: %v", t.label, t.version, res.Err)
http.Redirect(w, r, "/configuration?flash=artifact_unverifiable", http.StatusSeeOther)
return
}
if !res.OK {
s.logger.Printf("[WARN] artifact vouch REFUSED: %s v%s has NO usable git tag — "+
"raw/tag/v%s/%s does not resolve, so every install would 404 at step 5/8 (R-273)",
t.label, t.version, t.version, installerProbeConfig)
http.Redirect(w, r, "/configuration?flash=artifact_tag_missing", http.StatusSeeOther)
return
}
}
// Leg 2 — the artifact itself is fetchable.
res := s.gitea.PackageDownloadable(ctx, t.pkg, t.version, t.file)
if res.Err != nil {
s.logger.Printf("[WARN] artifact vouch REFUSED: could not verify the %s package %s: %v", t.label, t.version, res.Err)
http.Redirect(w, r, "/configuration?flash=artifact_unverifiable", http.StatusSeeOther)
return
}
if !res.OK {
s.logger.Printf("[WARN] artifact vouch REFUSED: %s package %s is NOT downloadable (R-287)", t.label, t.version)
http.Redirect(w, r, "/configuration?flash=artifact_pkg_missing", http.StatusSeeOther)
return
}
}
}
agentSHA, okAS := s.resolveArtifactSHA(r.Context(), pkgAgent, fileAgent, agentVer, r.FormValue("agent_sha256"))
goldenSHA, okGS := s.resolveArtifactSHA(r.Context(), pkgGolden, fileGolden, goldenVer, r.FormValue("golden_sha256"))
if !okAS || !okGS {