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:
2026-07-30 10:42:43 +02:00
parent 49b627684c
commit 1a68b53b06
7 changed files with 224 additions and 2 deletions
+98
View File
@@ -152,3 +152,101 @@ func excerpt(s string) string {
}
return s
}
// ── R-120: the vouch path REFUSES a golden older than the controller the fleet already runs ──────
//
// These drive handleSetArtifacts — THE production vouch path, the only UI writer of
// SetArtifactManifest — over httptest. Deliberately NOT through an injected seam: the whole point of
// this gate is that it cannot be inert, and three shipped defects in this project were fully green
// with the seam disconnected. The assertions check BOTH the operator-visible outcome and that the
// manifest was not written, because a gate that redirects but still saves is worse than none.
// RED-PROOF: delete the `goldenVer != ""` gate block in handleSetArtifacts → this fails, because the
// stale golden is accepted and the manifest is overwritten.
func TestVouchRefusesGoldenBehindFleet(t *testing.T) {
s, st := newTestServer(t)
// The R-120 situation exactly: a box is running 0.186.0 while the operator vouches a 0.185.1 golden.
seedReport(t, st, "demo", "0.186.0")
if err := st.SetArtifactManifest(store.ArtifactManifest{
AgentVersion: "0.116.0", GoldenVersion: "0.186.0",
AgentSHA256: strings.Repeat("a", 64), GoldenSHA256: strings.Repeat("b", 64),
}); err != nil {
t.Fatal(err)
}
form := url.Values{"agent_version": {"0.116.0"}, "golden_version": {"0.185.1"}}
form.Set("agent_sha256", strings.Repeat("c", 64))
form.Set("golden_sha256", strings.Repeat("d", 64))
r := httptest.NewRequest(http.MethodPost, "/configuration/artifacts", strings.NewReader(form.Encode()))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
s.handleSetArtifacts(w, r)
if got := w.Header().Get("Location"); !strings.Contains(got, "golden_behind_fleet") {
t.Errorf("expected the refusal flash, got Location=%q — the operator would not know it was refused", got)
}
// The load-bearing half: the manifest must be UNCHANGED.
if m := st.GetArtifactManifest(); m.GoldenVersion != "0.186.0" {
t.Fatalf("REFUSED but still wrote the manifest: golden=%q, want 0.186.0 untouched. A gate that "+
"redirects and saves anyway is worse than no gate — it reads as enforcement.", m.GoldenVersion)
}
}
// The gate must not block legitimate vouches, or it gets disabled and becomes another R-29 orphan.
func TestVouchAllowsGoldenAtOrAheadOfFleet(t *testing.T) {
s, st := newTestServer(t)
seedReport(t, st, "demo", "0.186.0")
for _, golden := range []string{"0.186.0", "0.187.0"} {
form := url.Values{"agent_version": {"0.116.0"}, "golden_version": {golden}}
form.Set("agent_sha256", strings.Repeat("a", 64))
form.Set("golden_sha256", strings.Repeat("b", 64))
r := httptest.NewRequest(http.MethodPost, "/configuration/artifacts", strings.NewReader(form.Encode()))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
s.handleSetArtifacts(w, r)
if m := st.GetArtifactManifest(); m.GoldenVersion != golden {
t.Errorf("golden %s (>= fleet 0.186.0) should be vouchable, manifest holds %q", golden, m.GoldenVersion)
}
}
}
// Fail-open case 1: a hub whose fleet has reported nothing must be able to vouch its first golden.
func TestVouchAllowedWhenFleetVersionUnknown(t *testing.T) {
s, st := newTestServer(t)
form := url.Values{"agent_version": {"0.116.0"}, "golden_version": {"0.185.1"}}
form.Set("agent_sha256", strings.Repeat("a", 64))
form.Set("golden_sha256", strings.Repeat("b", 64))
r := httptest.NewRequest(http.MethodPost, "/configuration/artifacts", strings.NewReader(form.Encode()))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
s.handleSetArtifacts(w, r)
if m := st.GetArtifactManifest(); m.GoldenVersion != "0.185.1" {
t.Errorf("with no reported fleet version the gate must fail OPEN; manifest holds %q", m.GoldenVersion)
}
}
// Fail-open case 2: clearing the golden field is a legitimate operator act, not drift.
func TestVouchAllowsClearingGolden(t *testing.T) {
s, st := newTestServer(t)
seedReport(t, st, "demo", "0.186.0")
form := url.Values{"agent_version": {"0.116.0"}, "golden_version": {""}}
form.Set("agent_sha256", strings.Repeat("a", 64))
r := httptest.NewRequest(http.MethodPost, "/configuration/artifacts", strings.NewReader(form.Encode()))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
s.handleSetArtifacts(w, r)
if got := w.Header().Get("Location"); strings.Contains(got, "golden_behind_fleet") {
t.Error("clearing the golden must not trip the gate")
}
}
// The comparison must be SEMVER, not lexical — 0.99.0 vs 0.186.0 is the pair that breaks string order,
// and it is not hypothetical: the fleet has shipped both 0.9x and 0.18x controllers.
func TestNewestReportedControllerIsSemverOrdered(t *testing.T) {
_, st := newTestServer(t)
seedReport(t, st, "a", "0.99.0")
seedReport(t, st, "b", "0.186.0")
if got := st.NewestReportedControllerVersion(); got != "0.186.0" {
t.Errorf("NewestReportedControllerVersion() = %q, want 0.186.0 — a lexical MAX() would return 0.99.0", got)
}
}