hub v0.82.0 (R-120): the vouch path REFUSES a golden the fleet has already outrun
The golden's version IS the controller it bakes (build-golden.sh:345 defaults GOLDEN_VERSION to the controller tag), so a golden behind the newest deployed controller means every FRESH install lands on stale application code. On the R-120 occurrence that stale code shipped a customer-facing falsehood: a box from the 0.185.1 golden told a customer whose backup drive had fallen out that the backup was on the same disk as the system -- false, the drive was gone -- and offered a different drive as the remedy. WHY A GATE, NOT A REMINDER. The gap has opened three times: R-111 (golden's agent 17 releases behind), R-115 (agent built and deployed, never published), R-120 (this). The first two were closed by re-baking and remembering; remembering then failed again. R-29 is the standing proof that a check nobody runs is worse than none because it reads as coverage -- hostinstall_gates.py sat RED and uninvoked across three version bumps and hub_confirm_gate.py has never run at all. So the property that matters is not whether a check exists but whether it BLOCKS. - Wired into handleSetArtifacts (internal/web/configs.go), immediately before the only write, on the sole UI path to SetArtifactManifest -- it runs on every vouch without anyone choosing to. A script in scripts/ would have been a fourth orphan. - It REFUSES (operator ruling, 2026-07-30), with a flash naming the remedy. - Signal: store.NewestReportedControllerVersion() over reports.controller_version, SEMVER-compared in Go -- MAX() in SQL ranks 0.99.0 above 0.186.0, a pair this fleet has shipped. No outbound call, no new credential. - Fail-open in exactly two deliberate cases: an empty golden field (clearing the manifest is legitimate) and an unknown fleet version (a new hub must vouch its first golden). NEAR-MISS RECORDED: the first draft read guests.controller_version, a column that exists in the schema and that NOTHING writes -- it would always have seen "" and failed open, i.e. inert, this gate's own failure shape. Caught by grepping for a writer before trusting the column. Blind spot stated rather than papered over: a controller no box has ever run is invisible to this signal. Not the failure that has bitten -- all three instances were deployed-newer-than-baked. 4 tests through the PRODUCTION handler over httptest, never an injected seam. The refusal asserts both the flash and that the manifest was NOT written, because a gate that redirects and saves anyway reads as enforcement while providing none. Red-proof: deleting the block makes the stale golden vouchable and both assertions fail. ROADMAP R-29's audit list now records this as the FIRST enforced gate, so the contrast with its three orphans is kept rather than lost. The orphans are unchanged. Suite rc=0 read separately from this commit.
This commit is contained in:
@@ -1139,6 +1139,37 @@ func (s *Server) handleSetArtifacts(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/configuration?flash=artifact_sha_invalid", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
// ── R-120 GATE — refuses a golden older than the controller the fleet is already running ─────────
|
||||
//
|
||||
// WHY THIS IS A GATE AND NOT A SCRIPT. The golden's version IS the controller it bakes
|
||||
// (felhom-agent configs/build-golden.sh: GOLDEN_VERSION defaults to ${CONTROLLER_IMAGE##*:}), so a
|
||||
// golden behind the newest deployed controller means every FRESH install lands on stale
|
||||
// application code. That has happened three times — R-111 (the golden's agent 17 releases behind),
|
||||
// R-115 (an agent built and deployed but never published), R-120 (this: the golden a controller
|
||||
// release behind, shipping a customer-facing FALSEHOOD, since 0.186.0 is what made the
|
||||
// absent-backup-target message true). The first two were closed by re-baking and remembering, and
|
||||
// remembering then failed again — which is why this is enforcement, not a reminder.
|
||||
//
|
||||
// It lives HERE, immediately before the only write, because handleSetArtifacts is the sole UI path
|
||||
// to SetArtifactManifest: it therefore runs without anyone choosing to run it. R-29 is the standing
|
||||
// proof that the alternative does not work — `hostinstall_gates.py` sat RED and invoked by nothing
|
||||
// across three version bumps while every report said green, and `hub_confirm_gate.py` has never run
|
||||
// at all. A check in scripts/ asserting this same fact would have been a fourth orphan.
|
||||
//
|
||||
// It REFUSES rather than warns (operator ruling, 2026-07-30): a non-blocking check reads as coverage
|
||||
// it is not providing, which is R-29's whole finding.
|
||||
//
|
||||
// FAIL-OPEN, deliberately, in exactly two cases: an empty golden field (clearing the manifest is a
|
||||
// legitimate operator act) and an unknown fleet version (no guest has reported one — a brand-new hub
|
||||
// must be able to vouch its first golden). Neither is the drift this catches.
|
||||
if goldenVer != "" {
|
||||
if newest := s.store.NewestReportedControllerVersion(); newest != "" && compareVersions(goldenVer, newest) < 0 {
|
||||
s.logger.Printf("[WARN] artifact vouch REFUSED: golden %s is older than the newest controller the fleet reports (%s) — "+
|
||||
"a fresh install would land on stale application code (R-120)", goldenVer, newest)
|
||||
http.Redirect(w, r, "/configuration?flash=golden_behind_fleet", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := s.store.SetArtifactManifest(store.ArtifactManifest{
|
||||
AgentVersion: agentVer,
|
||||
AgentSHA256: agentSHA,
|
||||
|
||||
Reference in New Issue
Block a user