diff --git a/documentation/architecture/00-capability-map.md b/documentation/architecture/00-capability-map.md index b06dcb2..b36d19a 100644 --- a/documentation/architecture/00-capability-map.md +++ b/documentation/architecture/00-capability-map.md @@ -121,7 +121,7 @@ | Scenario | Components | Status | Evidence | Gap / roadmap | |---|---|---|---|---| -| An ABSENT backup-target drive raises its OWN alarm, paired with a matching recovery | agent v0.114.0, controller v0.184.1+, hub v0.81.0 | **PARTIAL** | `SESSION-C-2026-07-29` C5: the drive-absent gate fires (4 s) and an alarm reaches the hub — but it is the **generic** `storage_disconnected`, while the return fires the **specific** `backup_target_restored`, so the pair cannot be matched. `backup_target_absent` never fired (count 0) | RE-TESTED 2026-07-30 on the fully shipped stack (`R116-PART5-2026-07-30`) — **still generic**; the v0.115.0 fix was ineffective and the mechanism is not isolated. The specific alarm, its severity, Hungarian copy and hub routing remain unexercised end-to-end → **R-116** | +| An ABSENT backup-target drive raises its OWN alarm, paired with a matching recovery | agent **v0.116.0**, controller v0.184.1+, hub v0.81.0 | **PROVEN-LIVE** (2026-07-30) | `audits/R116-v0116-2026-07-30.md`. On a fresh box built through the **real day-0** on demo-hp, running the agent it installed **unaided from the vouched Day-0 manifest** (0.116.0), both drives enrolled through the real endpoints and device loss a real hot-detach — **the full four-event sequence, two matched pairs, correctly discriminated:** `07:20:04 backup_target_absent (error)` / `07:22:34 backup_target_restored (info)` for the TARGET, and `07:24:04 storage_disconnected (error)` / `07:25:34 storage_reconnected (info)` for a NON-target drive on the same box minutes apart. Gate fired in **3 s**. All four reached the hub — specific alarm, severity, Hungarian copy and hub routing now exercised end-to-end. **Discrimination is proven NON-trivially for the first time**: both prior runs had the target itself emit the generic event, so the mirror proved nothing. **Over-correction guard PASSES** with a positive observable — 0 ABSENT lines and 0 drive events over a 2m14s window with both drives present, target `degraded:false`, while 2 `RETURNED` lines prove the gate was ticking. | **The mechanism was isolated from the captured payload first** (`DIAG-r116-disks-payload-2026-07-30.md`), after two fixes aimed at shapes that do not occur. v0.116.0 joins the two records of one drive on the only identity that survives the device — the CONFIGURED path — so one row carries both the flag and the guest path the gate keys on. **Both smaller-looking fixes were rejected because they regress R-114** (`backup_target_offer.go:79` reads flag+mount_path as *healthy*). **Caveat worth reading: the drill box ran controller 0.185.1 from the golden, which PREDATES R-114** — so its absent-state banner showed the old false "backup is on the system disk" copy. That is the golden being a release behind, not a regression → **R-120** | | Health-degradation email (edge-triggered, cooldowns, Hungarian) via hub → Resend | controller, hub | **IMPLEMENTED** | delivery pipeline live-proven for the **enlarge-block** trigger (`CAMPAIGN-6D` P3-DELIVERY, op+customer "Kedves Ügyfél!"); `NotifyHealthChange` ok→warn/fail edge-trigger implemented | The **health-degradation** trigger specifically has never fired an email live in any doc. Demoted (pipeline proven for a different event). Deliverability to HU freemail → R-4 | | Event catalog: app_start_failed, dead-app, offbox_enlarge_blocked, claim/reset codes, critical severity | controller, hub v0.31/48/50/55 | **PROVEN-LIVE** | live-delivered: `CAMPAIGN-6D` P3-DELIVERY (enlarge-block, op+customer); `DRILL-day0-vm` F-4 (claim code); `DRILL-day0-take2` F-15 (reset code) | `app_start_failed`/`dead-app` delivery is unit-only (6C inconclusive) — the pipeline + 3 event families are live, those two are not | | Prefs safety: empty-email wipe guard | controller v0.137 + hub v0.71.0 | **IMPLEMENTED** | controller leg red-proofed 07-15; hub-side no-clobber belt (`handleSavePreferences` preserves a stored non-empty address on an empty-email push) red-proofed 07-22 | Born from a live incident; controller 0.160.0 guards both its push legs, so the hub belt covers older/rogue boxes | diff --git a/documentation/audits/R116-v0116-2026-07-30.md b/documentation/audits/R116-v0116-2026-07-30.md index 0105ac6..c7a1458 100644 --- a/documentation/audits/R116-v0116-2026-07-30.md +++ b/documentation/audits/R116-v0116-2026-07-30.md @@ -1,16 +1,15 @@ -# R116-v0116-2026-07-30 — the fix works at the payload layer, live; the events leg is blocked on an emailed code +# R116-v0116-2026-07-30 — R-116 CLOSED: the specific alarm and its matching recovery, both on the wire **Run:** R-116 join task, CC on DooPlex, 2026-07-30. Agent **v0.116.0** built, published, vouched, and -**installed by a fresh box from the Day-0 manifest**. +**installed by a fresh box from the Day-0 manifest**. **Result: ALL claims PASS.** | Claim | Verdict | |---|---| -| **The fix is effective live** — the absent target row carries the flag **and** the key the gate uses | ✅ **PASS**, on real hardware. `isTarget["/mnt/felhom-drives/cel"] = true` (was `false` through v0.115.0) | -| **R-114 not regressed** — no absent-state row combines the flag with a mount path | ✅ **PASS** | -| **No over-correction** — the absent row does not register presence | ✅ **PASS** (`bound_under_parent: false`) | -| **Discrimination at the payload layer** — non-target gets the flag on no row | ✅ **PASS** | -| **C5 — `backup_target_absent` / `_restored` events on the wire, paired** | ⛔ **NOT REACHED.** Blocked on the controller claim code (emailed only). See §6 | -| **Discrimination as events** | ⛔ **NOT REACHED**, same blocker. **Not reported as a pass** | +| **The fix is effective live** — the absent target row carries the flag **and** the key the gate uses | ✅ **PASS**. `isTarget["/mnt/felhom-drives/cel"] = true` (was `false` through v0.115.0) | +| **C5 — `backup_target_absent` / `_restored` on the wire, paired** | ✅ **PASS.** `backup_target_absent` **(error)** on detach, `backup_target_restored` **(info)** on return, same drive, both at the hub. Gate fired in **3 s** | +| **Discrimination — target ⇒ specific, non-target ⇒ generic** | ✅ **PASS, and NON-trivially for the first time.** Same box, minutes apart: target → `backup_target_absent`; non-target → `storage_disconnected` | +| **No over-correction** | ✅ **PASS** with a positive observable — 0 ABSENT lines / 0 drive events over 2m14s with both drives present, while 2 `RETURNED` lines prove the gate ticked | +| **R-114 not regressed by the fix** | ✅ **PASS at the payload layer** (no row combines the flag with a mount path) + unit-pinned. ⚠️ **not confirmable on this box** — it ran controller 0.185.1 from the golden, which predates R-114 → **R-120** | ## 1. Baselines as actually running @@ -145,7 +144,50 @@ reattach capture — only "the flag and the key rejoined on one row", which is w events leg runs, the `Return` branch will fire against a dead bind, so a `backup_target_restored` there proves pairing, **not** recovery. Recorded, not fixed (R-117 has its own row). -## 6. Why C5 was not reached — the one genuine human step, again +## 6a. C5 + discrimination — PASSED, the full four-event sequence + +The claim gate (§6b) was cleared with an operator-relayed code, the two paths registered through the real +`POST /api/storage/register`, and the cycle run. Controller log, verbatim, one continuous run: + +``` +07:20:03 [WARN] [gate] drive ABSENT /mnt/felhom-drives/cel — stopped+blocked 0 app(s): [] +07:20:03 [ERROR] [gate] the ABSENT drive /mnt/felhom-drives/cel is the WHOLE-GUEST BACKUP TARGET + — the system backup cannot run until it returns +07:20:04 [INFO] Event pushed: backup_target_absent (error) — A rendszermentés meghajtója nem érhető el: + Cel meghajto (/mnt/felhom-drives/cel) +07:22:34 [INFO] [gate] drive RETURNED /mnt/felhom-drives/cel — re-attached + restarted gate-stopped apps +07:22:34 [INFO] Event pushed: backup_target_restored (info) — A rendszermentés meghajtója újra elérhető: + Cel meghajto (/mnt/felhom-drives/cel) +07:24:03 [WARN] [gate] drive ABSENT /mnt/felhom-drives/adat — stopped+blocked 0 app(s): [] +07:24:04 [INFO] Event pushed: storage_disconnected (error) — Meghajtó váratlanul leválasztva: Adat meghajto +07:25:34 [INFO] [gate] drive RETURNED /mnt/felhom-drives/adat — re-attached + restarted gate-stopped apps +07:25:34 [INFO] Event pushed: storage_reconnected (info) — Meghajtó újra csatlakoztatva: Adat meghajto +``` + +All four **reached the hub** (`Event from sess-e: …` at 09:20:03 / 09:22:34 / 09:24:04 / 09:25:34 CEST). + +| # | drive | event | severity | pair | +|---|---|---|---|---| +| 1 | **target** `cel` | **`backup_target_absent`** | error | ↔ 2 | +| 2 | **target** `cel` | **`backup_target_restored`** | info | ↔ 1 | +| 3 | non-target `adat` | `storage_disconnected` | error | ↔ 4 | +| 4 | non-target `adat` | `storage_reconnected` | info | ↔ 3 | + +**Two matched pairs, correctly discriminated.** This is what R-116 existed to produce and what two prior +runs could not: both of those had the *target* emit the generic event, so "non-target ⇒ generic" proved +nothing about telling them apart. Here the two cases ran on **the same box, four minutes apart**, and +diverged. + +**Over-correction guard — positive observable, not an absent log line.** Window 07:26:51Z → 07:29:05Z with +both drives present: **0** `drive ABSENT` lines, **0** drive events, and +`{"degraded":false,"label":"Cel meghajto","target":"felhom-backup"}`. That the gate was *running* during +the window is established independently by the two `[gate] drive RETURNED` lines earlier in the same +container's log — so the silence is a decision, not a dead loop. + +Also emitted: `health_critical (error)` at 07:21:32 while the target was away, and its recovery. Expected +— the box's overall health reflects a missing backup target — recorded so the event count reconciles. + +## 6b. The claim gate — the one genuine human step, again (→ R-119) `planDriveGates` iterates `s.settings.GetStoragePaths()`. The drill controller has **none**: @@ -166,24 +208,35 @@ self-bind link is likewise mint-and-email — `handleSelfBindLinkSend` (`selfbin ever renders a flash, never the token. **There is no operator-side route to either secret**, which is the same wall E2D hit and named "the one genuine human step". -A fresh claim code (**generation 2**) was emailed to the registered address at 09:07:20 by -`POST /configs/sess-e/claim-resend`, so the code is waiting; the run stops here rather than reporting a -third trivial pass. +A fresh code (**generation 2**) was emailed by `POST /configs/sess-e/claim-resend` and **the operator +relayed it**, which is the only route that exists. Claim submitted through the real `POST /claim` (its own +pre-auth HMAC CSRF: GET the page, carry the token **and** its cookie), then login, then session-CSRF for +the writes. Positive discriminator that the gate moved, as E2D recorded: -**Explicitly NOT done, and not claimed:** no `backup_target_absent` or `backup_target_restored` event was -observed on the wire this run, and the non-target mirror was not exercised as events. The payload evidence -in §4 determines what the gate *will* decide, but it is not the event on the wire, and after two runs that -conflated the two, that distinction is kept. +``` +before: {"ok":false,"error":"dashboard not yet claimed"} +after: {"ok":false,"error":"authentication required"} (unauthenticated) +authed: {"data":{"degraded":false,"known":true,"label":"/mnt/felhom-drives/cel","target":"felhom-backup"},"ok":true} +``` + +**The cost is real and recurring: three sessions have now stopped at this wall.** → **R-119**. ## 7. Teardown — layers 1–3, per the §13 paragraph this task added -**DELIBERATELY RETAINED, pending the claim code**, so the events leg can finish without rebuilding: - | layer | item | disposition | |---|---|---| -| 1 — machine | VM **9401** `r116-drill` on demo-hp (+ `unused0`) | **retained**, running | -| 2 — host | `r116-images` dir storage at `/mnt/nvme-1tb` | **retained** | -| 3 — hub | customer **`sess-e`**, host **`sess-e-5d4427`**, appliance **13** | **retained** | +| 1 — machine | VM **9401** `r116-drill` + all four volumes | **DESTROYED** `qm destroy 9401 --purge` | +| 2 — host | `r116-images` dir storage at `/mnt/nvme-1tb` | **REMOVED**; space returned (below) | +| 3 — hub | customer **`sess-e`**, host **`sess-e-5d4427`**, appliance **13** | **GATE-BLOCKED — command recorded below.** The cascade was attempted and **correctly refused: HTTP 409 "Delete refused: host sess-e-5d4427 is ONLINE"**. Deletable once it ages ONLINE→DOWN (>1 h from its last report, `customer_delete.go:220-228`) | + +**Layer 2, measured:** `felhom-backup` available **928787076 KiB after** vs **928787080 KiB before the +run** (4 KiB = noise), used back from 17708084 → 4566012 KiB. `local-lvm` **38.84 %** vs 38.83 % — demo-hp's +own guest, not this run. `r116-images` gone; `qm list` shows only `drill-r50`. **The space came back.** + +**Secrets:** the break-glass credential and the hub DB copy it came from were `shred -u`'d; the claim code, +the drill controller password and the session cookie were shredded in the guest and on the box before +destruction, and the local copies on DooPlex are shredded. The in-guest `shred -u` left 3 files behind +(reported honestly rather than claimed clean) — they died with the purged disk moments later. `pvesm status` on demo-hp **before** the run, for the layer-2 comparison at teardown: diff --git a/documentation/backlog/OPEN-ITEMS.md b/documentation/backlog/OPEN-ITEMS.md index bcd1219..4f0eed2 100644 --- a/documentation/backlog/OPEN-ITEMS.md +++ b/documentation/backlog/OPEN-ITEMS.md @@ -16,7 +16,8 @@ State: `BLOCKED` · `READY` · `WAITING-ON-OPERATOR` · `WATCHING`. Every row ha | **R-110** | **`main` is the installer's publish channel — there is no staging.** `manifests/webpage.yaml` git-syncs `/scripts/` from `--branch=main` on a 30 s period and nginx serves that working tree directly (`location /scripts/`, `root …/current`). So pushing `scripts/felhom-host-install.sh` **is** publishing it: within thirty seconds it is what every subsequent `felhom-bootstrap.sh` fetch (`scripts/iso/felhom-bootstrap.sh:96`) and every operator-run day-0 command (`customer_unified.html:563`) receives. There is no tag, no pinned-version path, no staging copy and no rollback other than another push — for the artifact that runs as **root on a virgin box**, the single most privileged thing Felhom ships | **WAITING-ON-OPERATOR (S)** | operator ruling | **Two consequences worth stating:** E-2d is not a gate *before* exposure — 1.22.0 has been the live installer since it hit `main` on 2026-07-29 — and the precaution recorded on the old R-94 row as "do not point every new box at an installer that has never run" **was never available to take**. **Open question for the operator, not a defect to fix blind:** whether `/scripts/` should serve a pinned release (tag-tracked path, or a versioned directory with the customer command naming a version) or whether `main`-tracking is the accepted shape for a one-operator product. Exposure today is zero — there are no boxes installing — which is exactly why it is cheap to decide now. **SECOND INSTANCE, found 2026-07-29 by the E-2d run and filed here rather than as a new ID:** `felhom-host-install.sh` fetches **nine** files from `raw/branch/main` (`:2072`–`:2206`) and the hub manifest vouches a sha for exactly **one** (`wrapper_sha256` → `felhom-pbs-apply`; re-checked this run, no drift). E-2a's `felhom-backup-target-apply` (`:2116`) is installed **0755 to `/usr/local/sbin` and root-fenced in sudoers**, validated only by `bash -n` — a root-executed artifact taken from `main` with no pinned integrity, which is this row's class exactly | CC | | **R-111** | ~~**The Day-0 artifact channel is 17 agent releases stale — a box installed today gets agent `0.96.0`, not `0.113.0`.**~~ `felhom-host-install.sh` does not use `main`: it reads the hub-vouched manifest (`:423-436`) and fetches Gitea generic packages (agent `:1945`, golden `:2573`). Gitea's newest are **agent 0.96.0** and **golden 0.161.0**, and the hub's manifest selects exactly those — so a fresh box lands on **agent 0.96.0 + controller 0.161.0** (global floor `v0.156.0` < the golden's 0.161.0, so no self-update) against `main`'s 0.113.0 / 0.185.1. Agent 0.113.0 reached both demo boxes by **direct deploy and was never published** | **SHIPPED 2026-07-29 — the channel now serves agent 0.113.0 + golden 0.185.1** | — | **FIXED the same day it was found.** Agent **0.113.0** built from the clean tree @ `58b598b` and published (`scripts/publish-agent.sh`), sha `5f3247f756cb658e…`, round-trip GET verified. Golden **0.185.1** baked on the nested drill VM embedding controller `0.185.1`, published, sha `dba00f3e845c415e…` — bake clean: `Result=success`, overlay2, **all 3 mounts included** (rootfs+mp0+mp1), 0 FATAL/exclusions, upload HTTP 201, token-leak grep 0; log `drill/bake-0.185.1.log`; GL-1 teardown done (guest 9100 purged, secrets shredded, disk restored to `virgin`). Hub Day-0 manifest moved **both together in one POST** so it never vouched a new agent against an old golden; `min_agent` **0.93.0 → 0.113.0**, which is what controller v0.185.0 declares (`felhom-controller/CHANGELOG.md:15`) — **zero fleet impact, verified: all three enrolled hosts already run agent 0.113.0, so no box is held.** `wrapper_sha256` preserved verbatim (re-checked against `configs/felhom-pbs-apply` — no drift). **The global controller floor was deliberately NOT raised**: the golden now bakes 0.185.1, so a fresh box needs no self-update, and raising it would have been an unnecessary fleet-wide write. Original finding follows. **Found 2026-07-29 by the E-2d Phase 0 gate, which stopped the run before a VM was created.** 17 unpublished releases (v0.97.0–v0.113.0) strand the **entire R-82 tiered-backup arc** plus **F-CRIT-2** (a failed backup looking fresh — 7 days silent) and **F-REBOOT** (a guest rebooted mid-backup never returns): a new customer's box would install without them. **Blocks E-2d's C3/C4/C5** — those test endpoints and events that do not exist in 0.96.0/0.161.0. The **controller is fine** (registry has 0.185.1, floor-driven self-update), so the gap is specific to the two Gitea-generic artifacts. **Mirror of R-110, not a duplicate:** R-110 = the installer publishes instantly with no staging; R-111 = the agent/golden publish gate exists and was never walked. Fix should decide whether publishing joins the release train rather than staying a remembered step (R-29's shape, one layer up). Evidence: `audits/E2D-fresh-vm-2026-07-29.md` **DEFERRED LEG, AND IT RECURRED → R-115.** This row's shipped half stands and is not reopened: the bump happened, was verified, and was proven end-to-end by the E-2d install. But its own closing line — *decide whether publishing joins the release train rather than staying a remembered step* — was never acted on, and agent 0.114.0 reproduced the exact condition the same afternoon. The recurrence is filed as **R-115**, not as a reopen, because the stale-channel finding is closed while the process defect that caused it is a distinct problem with a distinct fix. | CC | | **R-115** | **Publishing is a remembered step, and it was forgotten within eight hours of being documented as forgettable.** A box installs the agent from a Gitea generic package the hub explicitly vouches, never from git. Nothing in the build, deploy or session-end path publishes or checks that a version was published — so "deployed" and "installable" are independent states that drift silently. **Two instances, both real:** **R-111** (2026-07-29 morning) — 17 agent releases v0.97.0–v0.113.0 stranded, so a new customer would have installed without the entire R-82 tiered-backup arc plus F-CRIT-2 and F-REBOOT; found only because the E-2d Phase 0 gate happened to look. **Agent 0.114.0** (same afternoon) — the R-113 fix, built and pushed at `b58d7bc`, deployed to felhom-pve, and **unpublished until this task**, which blocked Session C: a fresh drill box would have installed 0.113.0 and proven the bug rather than the fix | **WAITING-ON-OPERATOR (M)** | operator ruling on the release process | **The finding is the RECURRENCE, not either instance** — both instances are fixed. R-111's own text already named this leg (*"decide whether publishing joins the release train rather than staying a remembered step"*) and closed SHIPPED without it; the leg then recurred the same day, which is the evidence that a note is not a mechanism. **Class: → R-29, one layer up** — a control that exists and is never walked; deliberately NOT given its own ID. **The decision is the operator's; the options, mechanisms first:** (a) **publish as a step in the build/release path**, so deployed and installable cannot diverge; (b) **a gate that refuses to deploy a version that is not published+vouched** — the strongest, and it fails closed; (c) a session-end checklist entry; (d) accept it as manual and add a pre-Session-C verification. **(a) and (b) are mechanisms; (c) and (d) are reminders — and R-29's whole finding is that reminders do not hold.** No code this session by design | CC | -| **R-116** | **The drive-absent alarm and its recovery are a MISMATCHED PAIR — absent fired the GENERIC `storage_disconnected`, return the SPECIFIC `backup_target_restored`** | **FIX SHIPPED + PROVEN-LIVE AT THE PAYLOAD LAYER (agent v0.116.0, 2026-07-30); the EVENTS leg is the one open item** | an emailed controller claim code (→ R-119) | **v0.116.0 works live where v0.115.0 was inert** (`audits/R116-v0116-2026-07-30.md`). Built, published (sha `b47c5c4dab641ee5…`, independent registry GET verified), vouched in the Day-0 manifest, and **installed unaided by a fresh box** — `Artifact manifest served for customer sess-e (agent=0.116.0 golden=0.185.1)`, host `sess-e-5d4427 … 0.116.0 ONLINE`. Real day-0 on a nested PVE on **demo-hp** (per `runbooks/target-selection.md`, which sent this run to demo-hp rather than the DooPlex fixture the prior run used), drives enrolled through the real endpoints, device loss a real hot-detach. **CAPTURED LIVE: the absent target is now ONE row carrying `backup_target:true` AND `guest_path:/mnt/felhom-drives/cel` with `mount_path:""`, so `isTarget["/mnt/felhom-drives/cel"] = TRUE`** (it was `false` through v0.115.0) — the gate will take the specific branch, and RETURNED gives `true` too so the pair matches. All three guards pass from the same payload: **R-114 preserved** (no row combines the flag with a non-empty `mount_path`, so `backup_target_offer.go:79` does not match and the `TargetAbsent` branch stays reachable), **no over-correction** (`bound_under_parent:false`), **discrimination at the payload layer** (the non-target carries the flag on no row — the thing both prior runs could not show). **THE RULING, and it is the substantive output:** the spec's preferred option (back-fill `MountPath` on the Observe row) and its alternative (flag the registry row, whose `MountPath` comes from the stale unit file) **both regress R-114**, which shipped 2026-07-29 — either manufactures a row that `:79` reads as *healthy* while the drive is missing. R-114's correctness rested on the absent rows NOT combining those two fields, a coupling invisible until the payload existed. Taken instead: give the Observe row the **guest path only** (from a new `ConfigPath`, `json:"-"` so the cross-repo golden/key-set contract is untouched) and dedup the registry row **on guest path** — the join being CONFIGURATION (storage.cfg `path` vs the `.mount` unit's `Where`), the only identity that survives the device. Tests 845→849, 4 red-proofs each verified to land; red-proof 1 replays v0.115.0's code and fails, which is the empirical proof it was inert. Its green test had supplied a `MountPath` production never supplies AND left `DriveTargets` nil so the union loop never ran — both corrected. **STILL OPEN — the events on the wire.** `planDriveGates` iterates registered StoragePaths; the drill controller has none (`[WARN] Storage paths: no storage paths registered`) and every storage route answers `401 dashboard not yet claimed`. The claim code is bcrypt-hashed and emailed-only, and `handleSelfBindLinkSend` (`selfbind_mint.go:139-161`) renders a flash, never the token — **no operator-side route exists** (→ **R-119**). A gen-2 code was re-sent 09:07:20Z; the drill VM 9401, `r116-images` and customer `sess-e` are **deliberately RETAINED** with teardown commands recorded, so the leg finishes without a rebuild. **No `backup_target_absent` was seen on the wire this run and none is claimed.** v0.115.0 left in place (inert, harmless). R-118's symptom vanishes incidentally on this one row; R-118 NOT fixed | CC | +| **R-116** | ~~**The drive-absent alarm and its recovery were a MISMATCHED PAIR — absent fired the GENERIC `storage_disconnected`, return the SPECIFIC `backup_target_restored`; `backup_target_absent` never fired at all**~~ | **SHIPPED + PROVEN-LIVE** (agent v0.116.0, 2026-07-30) | — | **CLOSED. The full four-event sequence, on the wire, on a fresh box** (`audits/R116-v0116-2026-07-30.md`): `backup_target_absent (error)` on detach → `backup_target_restored (info)` on return for the TARGET, and `storage_disconnected (error)` → `storage_reconnected (info)` for a NON-target drive on the same box four minutes apart. **Two matched pairs, correctly discriminated — and discrimination is proven NON-trivially for the first time**, since both prior runs had the target itself emit the generic event. Gate fired in **3 s**; all four events reached the hub, so the specific alarm, its severity, its Hungarian copy and the hub routing are now exercised end-to-end. **Over-correction PASSES with a positive observable** (0 ABSENT lines / 0 drive events over 2m14s with both drives present, target `degraded:false`, while 2 `RETURNED` lines prove the gate was ticking). Ran on a nested PVE on **demo-hp** per `runbooks/target-selection.md` — through the **real day-0** from the v1.25.0 ISO, with the agent **installed unaided from the vouched Day-0 manifest** (published sha `b47c5c4dab641ee5…`, independent registry GET verified, manifest read back), drives enrolled through the real endpoints, device loss a real hot-detach. **THE FIX, and the ruling is the substantive part:** the mechanism was first isolated from the captured payload (`DIAG-r116-disks-payload-2026-07-30.md`) after two fixes aimed at shapes that do not occur. **Both smaller-looking options were REJECTED because they regress R-114** — `backup_target_offer.go:79` reads `BackupTarget && MountPath != ""` as *"a real drive with its own mountpoint — healthy"* and returns before its `TargetAbsent` branch, so back-filling `MountPath` on the Observe row **or** flagging the registry row (whose `MountPath` is the stale unit-file value) would have told the customer the backup target is fine while its drive was gone. **R-114's correctness was resting on R-116's bug** — a coupling invisible until the payload existed. Taken instead: the Observe row gets the **guest path only** (`mount_path` stays `""`, which is true) from a new `ConfigPath` (`json:"-"`, so the cross-repo golden + key-set contract is untouched), and the union row is deduped **on guest path** — the join being CONFIGURATION (`storage.cfg`'s `path` vs the `.mount` unit's `Where`), the only identity that survives the device. Tests 845→849; 4 red-proofs each asserted to land, and red-proof 1 replays v0.115.0's code and fails, which is the empirical proof it was inert. Its green test had supplied a `MountPath` production never supplies AND left `DriveTargets` nil so the union loop never ran — both corrected. v0.115.0 left in place (inert, harmless). Teardown all 3 layers; hub layer gate-blocked on ONLINE with the command recorded. **Caveat: the drill's controller was 0.185.1 from the golden, which PREDATES R-114**, so its absent-state banner showed the old false copy — the golden being a release behind, not a regression → **R-120** | — | +| **R-120** | **The golden bakes a controller that predates R-114 + R-112, so a FRESH customer box shows the WRONG absent-target message.** The Day-0 golden is **0.185.1**; both R-114 (the third state — *configured and its drive is gone* vs *never configured*) and R-112 (the banner having any UI consumer at all) shipped in **0.186.0**. Observed live this run on the drill box with its target detached: the endpoint returned the generic degraded branch — *"A rendszermentés jelenleg ugyanazon a lemezen van, mint a rendszer"* (**false** — the target is a drive that vanished) **plus** `offer_path: /mnt/felhom-drives/adat`, offering another drive as the remedy. That is E2D §5.3's exact payload, still reachable on any newly installed box | **READY (S)** | — | **This is R-115's class one layer over: baking is a remembered step.** R-111 was closed by re-baking the golden to 0.185.1; 0.186.0 then shipped and the golden was not moved, so the gap reopened silently — and this time the stale artifact carries a **customer-facing falsehood** in exactly the state R-116 now alarms about correctly. Fix is mechanical (bake + publish + vouch a 0.186.0 golden), but the decision that matters is whether the golden joins the release train or the manifest gains a gate that refuses to vouch a golden older than the newest controller — **reminders have already failed twice here** (R-111 → R-115). Evidence: `audits/R116-v0116-2026-07-30.md` §6a caveat + §1 | CC | | **R-117** | **A returned drive's guest bind is a DEAD MOUNT, and the gate restarts the customer's apps onto it while every signal reads healthy.** After a detach/reattach the host raw mount is fine on the NEW device node, but the guest's bind still names the OLD one and every operation through it returns `EIO`: host `findmnt /mnt/cel` → `/dev/vdc`; guest `findmnt /mnt/felhom-drives/cel` → `/dev/vdb[/felhom-data] …,shutdown`; guest `ls` and write → **`Input/output error`**; and `/disks` reports `state:"attached"`, `role:"user-data"`, `bound_under_parent:TRUE`, `backup_target:true` | **READY (M) — NEW 2026-07-30, deliberately not chased** | — | **Worse than R-116 and it outranks it.** `planDriveGates` computes `present[gp] = … \|\| d.BoundUnderParent` (`intermediary.go:226`) = `true`, and the drive is `Disconnected`, so the gate takes the **`Return`** branch (`intermediary.go:281`, `:301-320`): it clears the flag, **restarts the gate-stopped apps onto a namespace that `EIO`s on every call**, reports the drive healthy and emails `backup_target_restored`. **No alarm on any channel.** R-113's conjunction cannot catch it — half 1 (guest mountinfo has an entry at the guest path) is satisfied by the STALE entry and half 2 (`devicePresent`) by the NEW device; **neither half compares the two**. Not a false invariant: `disks.go:158-165` claims only the boot-ordering and outlived-bind cases, so this is a genuine gap. **Seen but uninvestigated in E-2d, Session C and Part 5 (three consecutive runs) as "the drive returned as /dev/sdc while findmnt still read /dev/sdb"** — that framing read as cosmetic; it is not. Reproduced deliberately and probed to EIO in `audits/DIAG-r116-disks-payload-2026-07-30.md` §11. Deserves its own run | CC | | **R-118** | **An absent drive's union row advertises the ROOT filesystem's capacity as its own.** In the absent-state payload the registry-union row reports `total_bytes: 49675956224 / used_bytes: 4584579072` — **byte-identical to the `local` row** (`durable_id: path:/var/lib/vz`, i.e. `pve-root`) in the same response. The real drive is **4 GB** | **READY (XS) — NEW 2026-07-30** | — | Cause: `statfsCapacity(d.MountPath)` (`disks.go:335-338`) statfs's `/mnt/cel`, which with the device gone is a **bare directory on the root filesystem**. `observe.go:176-183`'s comment warns about exactly this trap and guards the Observe path ("*an unmounted removable dir-storage's mountpoint reverts to a bare directory on root … catastrophic DR mis-id*"); **the union path has no equivalent guard.** **Not a DR mis-id** — `durable_id` on that row is still the correct `uuid:…`, so re-attach identity is safe. It is a **false capacity** reaching every consumer of `total_bytes`/`used_fraction` (fill monitors, storage cards): a detached 4 GB drive advertises 46 GiB at 9.2 % used. Same class as `role.go:180-181` — an absent drive's fields decaying to the root filesystem's. Evidence: `audits/DIAG-r116-disks-payload-2026-07-30.md` §12 | CC | | **R-113** | ~~**The drive-absent gate CANNOT FIRE on device loss — E-2b's alarm is wired to an unreachable condition.**~~ `planDriveGates` (`intermediary.go:216-262`) computes presence by OR-ing `d.BoundUnderParent` into `present[GuestPath]`, and the agent derives `BoundUnderParent` from `GuestSeesMount()` — *"is this path a mount target in the guest's `/proc//mountinfo`"* (`localapi/disks.go:210`). The raw drive mount is a **device-bound systemd unit** and dies with the device; **the agent's own bind under the shared parent is NOT device-bound, so its mountinfo entry outlives the device**. The gate reads the surviving bind as "present" ⇒ no `Stop` action ⇒ `notifyDriveAbsent` never called | **SHIPPED + PROVEN-LIVE** (agent v0.114.0, 2026-07-29) | — | **PROVEN LIVE `audits/SESSION-C-2026-07-29.md`.** Target drive hot-detached on a fresh box running the SHIPPED agent 0.114.0 (from the Day-0 manifest, not a hand build). **The gate fired in 4 seconds** — E-2d measured ZERO over 4½ minutes — and `SetDisconnected` was reached. It fired on exactly the shape that defeated it: raw `/mnt/mentes` NOT mounted while the bind `/mnt/felhom-drives/mentes` still read `/dev/sdb[/felhom-data]`. **Over-correction guard PASSED:** with both drives present, 0 ABSENT lines and the target stayed healthy. **Note: C5 still FAILED — but on a different defect (→ R-116), not on this one.** **SESSION C IS NOW UNBLOCKED.** Agent **0.114.0 published and vouched** 2026-07-29 (sha `5e4c15ebee2d7583…`, round-trip GET verified; hub manifest read back showing it) — a fresh drill box now installs the fix under test instead of the bug. The golden was NOT re-baked and `min_agent` was NOT raised: the golden bakes the controller, not the agent, and controller v0.185.0 declares MinAgent 0.113.0, which 0.114.0 already satisfies. **FIX: `BoundUnderParent` is now a CONJUNCTION — bound under the parent AND the drive's raw host mount still mounted** (`devicePresent`, new `deviceCheck` seam), at BOTH `/disks` construction sites. The raw mount is the device-bound systemd unit that dies with the device; the agent's bind is not — so the raw mount IS the device signal, grounded in E-2d's measurement rather than inference. **Conjunction, deliberately:** the device half alone would regress boot ordering (raw mounts early, bind lands ~18 s later — that window must keep reading absent), so existing behaviour is byte-identical and only the unreachable case is closed. **Unknown is never absent** (`devicePresent("")` = true) — a false absent stops a working customer's apps. **Controller UNCHANGED, no MinAgent bump:** `BoundUnderParent` has exactly one functional consumer (`planDriveGates:226`); a new `DevicePresent` bool was rejected because absent-from-JSON decodes to `false`, which would have made every drive on an older agent read ABSENT. +6 tests (208→214), 4 red-proofs run and reverted. **Deployed to demo-felhom; over-correction guard verified in production** — raw mount present, drive still reads present, 10/10 apps untouched, no gate action, no false alarm. **demo-hp deliberately left on 0.113.0** (the spec scoped deploy to felhom-pve) — it still carries the bug. **⚠️ SESSION C BLOCKER: the hub Day-0 manifest vouches agent 0.113.0**, so a fresh drill box would install WITHOUT this fix and validate nothing — publish + vouch 0.114.0 first (R-111's trap, same shape). **Awaiting live proof of exactly one leg:** device loss → gate `Stop` → `SetDisconnected` → `backup_target_absent` on the wire. Original finding: **PROVEN LIVE 2026-07-29 on a fresh box.** Target drive hot-detached under a running agent; over 4½ min (budget was 60 s): the **agent** said `enrolled drive absent by UUID` every 20 s, the controller logged **0** `[gate]` lines, and the hub received **zero** events — no `backup_target_absent` **and no generic `storage_disconnected`**. Measured with the device gone: `/mnt/mentes2` NOT mounted, `/mnt/felhom-drives/mentes2` still `/dev/sdb[/felhom-data]`. **Not a virtualisation artefact** — the asymmetry is device-bound-mount vs manual-bind, identical on metal (caveat: proven on SCSI hot-detach; physical unplug not staged). **Mirror scenario reasoned, not observed:** both the specific and generic events come from the same `a.Stop` branch, so the generic one is equally unreachable. **Sixth instance of seam-built-but-never-wired** — E-2b wired the seam, to a condition that cannot occur. Evidence: `audits/E2D-fresh-vm-2026-07-29.md` §5.2 **Session C scope UNCHANGED by Session B** — R-114/R-112 shipped as controller v0.186.0 and neither touches the agent; the leg awaiting proof is still device loss → gate `Stop` → `SetDisconnected` → `backup_target_absent` on the wire. One rebuild now validates all three. | CC |