# R120-golden-rebake-2026-07-30 — the golden is current again, and the class now has a gate that refuses **Run:** R-120, CC on DooPlex, 2026-07-30. **CLOSED on both halves.** Golden **0.186.0** baked, published, vouched and proven on a real day-0; and the class now has a **mechanism** — hub **v0.82.0**'s vouch-path gate, which **refuses** a golden the fleet has already outrun, deployed and proven live by re-attempting the original mistake. ## 1. Baselines, established separately | Thing | Value | Established from | |---|---|---| | controller on `main` | **0.186.0** | `felhom-controller/CHANGELOG.md:1` | | controller the golden baked **before** this task | **0.185.1** | the golden's **own record**: `drill/bake-0.185.1.log:1` (`baking controller …:0.185.1`) and `:330` (`GOLDEN_VERSION=0.185.1`) | | controller running, felhom-pve | **0.186.0** | `pct exec 9201 -- docker ps` | | controller running, demo-hp | **0.185.1** | same | | agent published + vouched | **0.116.0**, sha `b47c5c4d…` | registry GET + manifest read-back | | golden vouched **before** | **0.185.1**, sha `dba00f3e…` | manifest read-back | | `felhom.eu` HEAD at start | `772956d` | `git rev-parse` | **R-120 confirmed at source, not inferred:** `build-golden.sh:345` derives `GOLDEN_VERSION="${GOLDEN_VERSION:-${CONTROLLER_IMAGE##*:}}"` — the golden version **is** the controller tag it bakes. So golden 0.185.1 ⇒ every fresh box installed controller 0.185.1, which predates R-114 + R-112 (shipped in 0.186.0), while a deployed box was already on 0.186.0. One release behind, and the missing release is the one that tells the truth about an absent backup target. ## 2. The bake procedure — it IS written down Contrary to the risk the task flagged, the procedure exists and was followed, not reconstructed: - **`documentation/runbooks/RUNBOOK-manual-build.md:101-115`** §4 "Golden image (fresh Day-0 installs)" — the five steps: revert the drill VM to `virgin`, boot it, run `build-golden.sh `, teardown per GL-1, then vouch in the hub. - **`felhom-agent/configs/build-golden.sh`** — the tool, with its usage in its own header (`[VMID] [TEMPLATE_VOLID] [ROOTFS_STORAGE] [ARCHIVE_STORAGE] [BRIDGE] CONTROLLER_IMAGE`), and it publishes to Gitea itself (`:335-371`) when `GITEA_USER`+`GITEA_TOKEN` are set. There is no separate `publish-golden.sh` — the bake script is the publisher. - **`documentation/runbooks/publish-train-rules.md`** — rules 1–4. Rule 2 (**save the floor field LAST**, separate save) and rule 3 (MinAgent) were consulted; see §4 for how they applied. **One documentation-integrity finding, small but worth recording.** `RUNBOOK-manual-build.md:103-105` says to use the *"RECORDED* qemu launch line; do not reconstruct from memory" and points at `RUNBOOK-publish-0.79-0.110-2026-07-10.md` Phase C — but that line is *itself* labelled `PASS (reconstructed — DEVIATION)` at `:29`, with `:77-79` stating the canonical one-liner **was never saved**. So the runbook forbids reconstruction while citing a reconstruction. The line at `:29` is nonetheless correct — it booted first try here, and independently earlier today. **A trap learned today and worth adding to it:** adding any `-device` at launch shifts PCI slot assignment and renames the guest NIC, so `vmbr0` loses its port and SSH never comes up while the console shows a healthy PVE login. Boot the recorded line unchanged and hot-plug extras over QMP. ## 3. The bake — controller 0.186.0 Run in the DooPlex nested drill VM (its documented purpose — `target-selection.md` names it a **bake** fixture, and this is a bake), reverted to `virgin` first, `build-golden.sh v2.1.0` as a transient unit: ``` [golden] build-golden.sh v2.1.0 — baking controller gitea.dooplex.hu/admin/felhom-controller:0.186.0 docker OK (overlay2; data-root /var/lib/docker) INFO: including mount point rootfs ('/') INFO: including mount point mp0 ('/var/lib/docker') INFO: including mount point mp1 ('/mnt/sys_drive') INFO: archive file size: 618MB [golden] upload OK (HTTP 201) GOLDEN_VERSION=0.186.0 GOLDEN_SHA256=b760ac6a33e70700f427b4207afeebc8694bd6bb9478973c30901d9f059bde17 ``` Pass markers, counted on the saved log: `overlay2` present, **FATAL 0**, **exclusions 0**, **3 mounts included**, upload **201**. Template: `debian-13-standard_13.6-1_amd64.tar.zst` (the pinned version in the script's default has rotted away; the current one was resolved with `pveam available`). Log retained out-of-VM at `drill/bake-0.186.0.log` (330 lines); **token-leak grep against the literal token value on the saved log = 0**. **GL-1 teardown:** build guest 9100 `pct destroy --purge`; `.gitea-token`, the script and the in-VM log `shred -u`'d (0 residual); VM powered off; **`drill.qcow2` reverted to `virgin`** (snapshot intact, ID 1, 2026-07-03) — the fixture is exactly as found. ## 4. The three publish/vouch observables, quoted as returned ``` (1) PUBLISHED — anonymous GET, i.e. what the installer actually does, sha over the bytes returned: HTTP 200 bytes=648930639 b760ac6a33e70700f427b4207afeebc8694bd6bb9478973c30901d9f059bde17 /tmp/golden-0.186.0.tar.zst (identical to the bake's GOLDEN_SHA256) (2) VOUCHED — manifest read BACK after the POST, never the 303: agent_version: vouched=['0.116.0'] agent_sha256 = b47c5c4dab641ee5… golden_version: vouched=['0.186.0'] golden_sha256 = b760ac6a33e70700… min_agent = 0.113.0 min_controller_version = 0.156.0 wrapper_sha256 = 104db0a4401f65bb… (re-checked against configs/felhom-pbs-apply — NO drift) (3) A CONSUMER RESOLVES IT — the hub telling a real fresh box what to install: [INFO] Artifact manifest served for customer sess-f (agent=0.116.0 golden=0.186.0) ``` **Publish-train rules as applied:** rule 1 (manifest before floor) — satisfied trivially, no floor moved. **Rule 2 (floor field LAST, separate save)** — the floor was **not touched at all**; `min_controller_version` read back **0.156.0**, unchanged, and the floor is a separate form (`/configuration/global-floor`) which was never posted. **Rule 3 (MinAgent)** — left at **0.113.0**, because controller 0.186.0's CHANGELOG header declares `MinAgent 0.113.0 (unchanged)`; raising it would hold demo-hp (agent 0.113.0) for no reason. The agent vouch was carried through untouched. ## 5. §2.1 — the fresh-box proof **Machine: `demo-hp`** (Tier 0, the designated drill+build VM host). **A real day-0, not the fixture** — per the rule committed in Part 1, this is a *path* claim, so a fixture could not prove it. VM **9402** `r120-drill`, disks on a dir storage `r120-images` at **`/mnt/nvme-1tb` root** (never `local-lvm`), installed from the v1.25.0 ISO: self-register (appliance 14) → operator bind to `sess-f` → credentials delivered once → guest provisioned **from the new golden**. **R-120's fix, proven at the install path:** ``` hub → Artifact manifest served for customer sess-f (agent=0.116.0 golden=0.186.0) hub → Event from sess-f: controller_started (info) — Controller elindult (0.186.0) box → felhom-agent --version → felhom-agent 0.116.0 box → pct exec 9201 -- docker ps → gitea.dooplex.hu/admin/felhom-controller:0.186.0 ``` **A freshly installed box now runs 0.186.0. Before this task it ran 0.185.1.** That is the defect closed. Target drive enrolled through the real endpoints and detached (real hot-detach), so the box sits in R-120's exact state — and agent 0.116.0's R-116 fix is visible on it: ``` /disks felhom-backup row: state=disconnected role=system mount_path='' guest_path='/mnt/felhom-drives/cel' backup_target=True pvesm: felhom-backup dir inactive 0 0 0 ``` ### The capture — R-120's symptom, inverted Operator-relayed claim code, then `GET /api/storage/backup-target` **with the target still detached**, on controller **0.186.0** from the new golden. Verbatim: ```json {"data":{"degraded":true,"known":true,"label":"", "message":"A rendszermentés meghajtója nem érhető el — amíg vissza nem csatlakoztatod, a teljes rendszermentés nem készül el.", "target":"felhom-backup"},"ok":true} ``` - ✅ the **`TargetAbsent`** copy — *the backup drive is unreachable; until you reconnect it the full system backup will not be made*. Reconnect **that** drive. - ✅ **`offer_path` is absent from the payload entirely** — no drive is offered as a remedy. - ✅ **not** the generic *"A rendszermentés jelenleg ugyanazon a lemezen van, mint a rendszer"*. For contrast, the **same read on the OLD 0.185.1 golden** one day earlier (`audits/R116-v0116-2026-07-30.md` §6a caveat) returned the generic system-disk message **plus** `offer_label: "Adat meghajto"` and `offer_path: "/mnt/felhom-drives/adat"`. The false message and the wrong offer are both gone. **That is R-120 closed at the customer-visible layer.** The claim gate had to be cleared by an operator-relayed code — the endpoint first returned `{"ok":false,"error":"dashboard not yet claimed"}`. The claim gate runs **before** auth (`controller/internal/web/auth.go:52-67`) and exempts only `/claim`, `/claim/request-new-code`, `/api/health`, `/static/` (`claim.go:221-228`) — **no Bearer escape**, and `regen-password` (`hub/internal/web/configs.go:965-980`) regenerates the *retrieval passphrase*, not the dashboard password. So an operator-relayed code is the only route. **This is R-119's fourth instance.** ## 6. Part 3 — the ruling, and the recommendation with its wiring **Recommendation: the gate, and it must REFUSE, not warn.** The record is the argument: this gap has opened three times (**R-111**, **R-115**, **R-120**) and the first two were closed by re-baking and remembering. Remembering has now failed twice. And **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 distinguishing property is not *does a check exist* but **does it block**. **Where it wires — established, not proposed loosely.** `hub/internal/web/configs.go:1116` `handleSetArtifacts` is **the** vouch path: the only UI writer of `store.SetArtifactManifest` (`:1142`), reached from `server.go:484`. It already normalises the versions (`:1121-1125`) and resolves the SHAs (`:1128-1131`), so a refusal belongs immediately before `:1142`. Nothing can vouch a golden without passing through it — which is precisely what makes it a mechanism rather than a fourth orphan in `scripts/`. **The comparison signal is already local.** The hub stores `controller_version` per guest (`hub/internal/store/store.go:294`), populated from reports. A gate comparing the submitted `golden_version` against **the newest controller version any box has reported** would have caught this exact case: felhom-pve was reporting **0.186.0** while the manifest vouched a **0.185.1** golden. No outbound registry call, no new credential. **Its honest blind spot, stated up front:** if no box has ever run the newer controller, the hub cannot know it exists, so a golden baked behind an *unreleased-to-the-fleet* controller would still pass. That is a real limit — and it does not touch the failure that actually happened three times, which is a golden left behind a controller already running in the field. **The test must go through the production wiring**, not an injected seam: drive `handleSetArtifacts` over `httptest` with a guest row reporting 0.186.0 and a submitted golden of 0.185.1, then assert **both** that the response is the refusal **and** that the manifest was not written. Three shipped defects in this project were fully green with the seam disconnected; a gate that can be inert is the thing this gate exists to prevent. ### RULING: REFUSE — approved by the operator, 2026-07-30. Built, deployed, proven live. Shipped as **hub v0.82.0**: the gate in `handleSetArtifacts` immediately before the only write; `store.NewestReportedControllerVersion()` over `reports.controller_version` (the column `SaveReport` denormalises, `store.go:903`), **semver-compared in Go**; an operator-legible refusal flash naming the remedy; **fail-open in exactly two deliberate cases** — an empty golden field, and an unknown fleet version. **A near-miss worth keeping.** The first draft read **`guests.controller_version`** — a column that exists in the schema (`store.go:294`) and that **nothing writes**. That gate would always have seen `""` and failed open: **inert, i.e. this gate's own failure shape**, and one `grep` from shipping. Caught by looking for a writer before trusting the column. **Tests: 4, through the production handler over `httptest`, never an injected seam.** The refusal asserts **both** the flash **and** that the manifest was not written — a gate that redirects and saves anyway reads as enforcement while providing none. Plus the allow case, both fail-open cases, and the semver-ordering case (0.99.0 vs 0.186.0, a pair this fleet has shipped). **Red-proof:** deleting the block makes the stale golden vouchable and both refusal assertions fail. ### PROVEN LIVE on the deployed hub — the original mistake, re-attempted and blocked Hub 0.82.0 built, pushed, manifest pinned, ArgoCD hard-refreshed + synced → **Synced / Healthy**, pod on `felhom-hub:0.82.0`. Then the exact R-120 mistake was re-attempted — vouch the stale **0.185.1** golden: ``` POST /configuration/artifacts → HTTP 303 …/configuration?flash=golden_behind_fleet hub log: [WARN] artifact vouch REFUSED: golden 0.185.1 is older than the newest controller the fleet reports (0.186.0) — a fresh install would land on stale application code (R-120) manifest read back afterwards: golden still 0.186.0, sha still b760ac6a… — the refused POST did NOT write ``` **Refused *and* unwritten**, on the live hub against the real fleet signal rather than a unit fixture. **R-29's audit list now records it** (`ROADMAP.md`) as the **first enforced gate** beside its three orphans, so the contrast is kept rather than lost. The orphans are unchanged — this proves the pattern is available, not that the backlog moved. ## 7. Teardown | layer | item | disposition | |---|---|---| | 1 — machine | DooPlex bake fixture: build guest 9100 purged, `drill.qcow2` reverted to **`virgin`** | **DONE** | | 1 — machine | demo-hp VM **9402** `r120-drill` + all volumes | **DESTROYED** `qm destroy 9402 --purge`; `qm list` shows only `drill-r50` | | 2 — host | `r120-images` dir storage at `/mnt/nvme-1tb` | **REMOVED**; `felhom-backup` available **928787076 KiB after == before**, `local-lvm` **38.84 %** unchanged — space returned | | 3 — hub | customer **`sess-f`**, host **`sess-f-2670b5`**, appliance **14** | **GATE-BLOCKED — command recorded below.** Cascade attempted, correctly refused **HTTP 409** (host still ONLINE, `customer_delete.go:220-228`). Deletable once it ages ONLINE→DOWN, exactly as `sess-e` was this run | `pvesm status` on demo-hp before this run, for the layer-2 comparison at teardown: ``` felhom-backup dir active 983379700 4566012 928787076 0.46% local dir active 40516856 14980024 23446440 36.97% local-lvm lvmthin active 56545280 21962186 34583093 38.84% ← the fence figure ``` Teardown commands, recorded now: ```bash ssh demo-hp 'qm stop 9402; qm destroy 9402 --purge; pvesm remove r120-images; pvesm status' POST /configs/sess-f/delete ack_hosts=1 ack_reset=1 ack_purge=1 confirm_id=sess-f expect_hosts=1 ``` **Previous session's layer 3 is now DISCHARGED.** `sess-e` was gate-blocked on ONLINE last run with the command recorded; it aged to STALE and the cascade completed this run — `/customers/sess-e` → **404**, 0 hosts, and the hub logged `customer DELETE cascade COMPLETE for sess-e (journal #7) — full teardown` including `residue purged (reports=4 app_telemetry=4 … appliance_registrations=1)`. That is the §13 layer-3 discipline working as intended: recorded, then executed. **Fences held:** `drill-r50` untouched · guest 9201 on both demo boxes untouched · neither demo box re-targeted · nothing on `local-lvm` · Peti untouched · no existing box received the new golden · `felhom-agent`/`felhom-controller` **source untouched** · the hub DB copy taken for the break-glass credential was `shred -u`'d immediately.