docs(audit): DIAGNOSE re-enrolled drives not binding after 9201 re-provision

Root cause: controller handleStorageRegister registers the raw /mnt/<name> path
instead of the stable /mnt/felhom-drives/<name> path the agent binds the drive at.
Drives ARE live in guest+container at the stable path; controller watches an empty
rootfs placeholder. Not v0.40.0 mp1; not stale agent state. Read-only diagnosis,
no fix applied. Recommended fix direction documented.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PsnU2ASocYrvzqE82YDYW
This commit is contained in:
2026-06-23 17:15:30 +02:00
parent add5be5bd5
commit e8a5bdc492
@@ -0,0 +1,154 @@
# DIAGNOSE — re-enrolled external drives not binding into guest 9201 (after re-provision)
**Date:** 2026-06-23 · **Class:** diagnostic (read-only; no fix, no mutation) · **Guest:** 9201 (demo-felhom)
**Versions live:** agent v0.40.0, controller v0.77.0 (`66d8462`).
**Verdict:** controller code bug in ONE of the three drive-enroll paths (`handleStorageRegister`) — it
registers the **raw** `/mnt/<name>` path instead of the **stable** `/mnt/felhom-drives/<name>` path the
agent actually binds the drive at. NOT the v0.40.0 `mp1` change. NOT stale agent state.
---
## 1. Intended path model (from source)
Post the intermediary-mount migration (`SPIKE-intermediary-mount-2026-06-15.md`), an external drive is
visible **in the guest ONLY at its STABLE path** `/mnt/felhom-drives/<name>`:
- **Agent** (`felhom-agent/internal/localapi/intermediary.go`): `StableParentDir = "/mnt/felhom-drives"`.
A single permanent parent bind (`mp8 /mnt/felhom-drives`) is set into the guest once; the agent then
binds each drive's `felhom-data` namespace **under** it host-side
(`mount --bind /mnt/<name>/felhom-data /mnt/felhom-drives/<name>`), and host→guest mount propagation
carries it live. `StablePathForRaw("/mnt/<name>")``/mnt/felhom-drives/<name>` (intermediary.go:70).
- The agent's `/disks` list reports **both** paths per drive (`disks.go`): `mount_path` = the RAW host
PVE mount `/mnt/<name>` (disks.go:113), and `guest_path` = the STABLE in-guest path
(disks.go:146, set via `StablePathForRaw`), plus `bound_under_parent` (disks.go:150).
- **Controller** (`felhom-controller/internal/web/intermediary.go:73-98`, verbatim):
*"the REGISTERED storage path + every app's HDD_PATH + FileBrowser source = the STABLE path; the AGENT
still operates on the RAW /mnt/<name> host mount, so controller→agent `where` is mapped back to raw via
`agentWhere()`."* `stablePathForName(name)``/mnt/felhom-drives/<name>` (intermediary.go:87).
**So the intended registered path is the STABLE `/mnt/felhom-drives/<name>`.** The drive-absent gate
`planDriveGates` only manages paths under `/mnt/felhom-drives/` (intermediary.go:220) — a path registered
any other way is invisible to the gate.
---
## 2. Evidence (live, read-only — host / guest / container / state / logs)
| # | Where | Observation |
|---|-------|-------------|
| E1 | HOST `pct config 9201` | `mp0 /var/lib/docker`(200G), `mp1 /mnt/sys_drive`(50G), **`mp8 /mnt/felhom-drives`**, `mp9 bootstrap`. **No per-drive mpN** — correct (intermediary model). |
| E2 | HOST `findmnt` | `/mnt/felhom-drives/felhom-flash``/dev/sdc1[/felhom-data]`; `/mnt/felhom-drives/felhom-usb``/dev/sdb1[/felhom-data]` (each twice — the known double-bind). Raw `/mnt/felhom-flash`←sdc1, `/mnt/felhom-usb`←sdb1 also present (host-level, dated Jun 14-15, survived the destroy). |
| E3 | HOST `ls …/felhom-data` | Both drives' `felhom-data` namespace exists + is guest-owned (100000:100000). Bind source present — **H4 refuted**. |
| E4 | GUEST `findmnt` | `/mnt/felhom-drives/felhom-flash``sdc1[/felhom-data]` ✅ and `/mnt/felhom-drives/felhom-usb``sdb1[/felhom-data]` ✅ — the drives **are live in the guest at the stable path**. `/mnt/felhom-flash`, `/mnt/felhom-usb` are **plain dirs on rootfs disk-0** (created Jun 23 14:52, this boot). |
| E5 | CONTROLLER container `findmnt` | `/mnt/felhom-drives/felhom-flash``sdc1[/felhom-data]` ✅; `/mnt/felhom-drives/felhom-usb``sdb1[/felhom-data]` ✅. `/mnt/felhom-flash`**notmount**; `/mnt/felhom-usb`**notmount**. The real drives ARE visible in-container — at the stable path. |
| E6 | CONTROLLER `settings.json` | `storage_paths` = **`/mnt/felhom-flash`** (added `2026-06-23T14:52:10Z`) + **`/mnt/felhom-usb`** (`14:52:16Z`), labels `Tárhely (felhom-flash/usb)` (auto-inferred). **The registry holds the RAW paths.** |
| E7 | CONTROLLER selftest | `[WARN] Storage paths: 0 connected, 2 disconnected`; `[PASS] System data path: /mnt/sys_drive`. |
| E8 | AGENT journal (this boot) | Repeated `guest-attach: drive bound under shared parent (normalized to one bind, live) … where=/mnt/felhom-usb stable=/mnt/felhom-drives/felhom-usb prior_binds=2` and `…where=/mnt/felhom-flash stable=/mnt/felhom-drives/felhom-flash`. The agent binds correctly at the **stable** path every reconcile tick. |
| E9 | AGENT `guest-binds.json` / `drive-intents.json` | (Jun 22) 9201 → {81a26531, da9e7089 enrolled; ce9d ejected}. `ReassertGuestBinds` re-bound the enrolled drives at the stable path; intent-aware skip of the ejected `ce9d`. Agent state is **correct** — H3 refuted as a *cause*. |
**The chain:** drive present + host-bound + parent-propagated → live in guest AND container at
`/mnt/felhom-drives/<name>` (E2/E4/E5/E8). The controller registered `/mnt/<name>` (E6) → it watches an
empty placeholder dir on rootfs disk-0 → "0 connected, 2 disconnected" / "Rendszermeghajtón" / 31.2 GB.
---
## 3. Hypothesis verdicts
- **H1 — intermediary bind never placed:** **REFUTED.** E2/E4/E5/E8 show the binds placed and live all
the way into the container.
- **H2 — path-model mismatch (controller watches `/mnt/<name>`, drive lives at
`/mnt/felhom-drives/<name>`):** **CONFIRMED — primary cause.** E6 (raw in registry) vs E4/E5 (drive at
stable). Source: `handleStorageRegister` registers raw (see §4).
- **H3 — stale agent drive-state vs fresh guest:** **REFUTED as cause.** The Jun-22 agent state is stale
but *correct* — it drove the right stable binds (E8/E9). It is not what mis-registered the path.
- **H4 — felhom-data namespace absent → placeholder:** **REFUTED.** Namespaces present (E3). (The
rootfs placeholder dirs DO exist, but the controller created them itself — see §4 — not the guest-hook
for an absent bind source.)
- **H5 — "Újraindítás most" is a no-op for activation:** **CONFIRMED as a downstream symptom.** The
button calls agent `GuestReboot` (`agentapi/client.go:366`). A reboot correctly re-binds the drive at
the stable path, but the controller's *registered* path is the raw rootfs placeholder, so
`pendingActivationDrives` (storage_handlers.go:172) keeps flagging it (`attached[/mnt/felhom-usb]` raw
key happens to match the raw registered `sp.Path`, and `system.IsMountPoint("/mnt/felhom-usb")` is
false because it's a same-device rootfs dir) → the "2 meghajtó aktiválásra vár" banner persists no
matter how many reboots. A reboot cannot fix a wrong registered path.
- **H6 — propagation broken by an unprivileged edge:** **REFUTED.** Propagation works (E5: the stable
path is live inside the container).
**Single primary cause: H2.** H5 and the badge are downstream consequences of it.
---
## 4. Root cause in source
`felhom-controller/internal/web/storage_handlers.go`:
- `runStorageInit` (line 127-128) and `runStorageAttach` (line 160-161) both do
`stable := stablePathForName(path.Base(where))``registerStoragePath(stable, …)`**correct**.
- **`handleStorageRegister` (line 667-694)** — the *"register an already-mounted, unregistered drive"*
action — does `s.registerStoragePath(req.Where, …)` with `req.Where` = the **raw** `/mnt/<name>`
**verbatim** (line 682). It is **missing the `stablePathForName` mapping** its two sibling enroll paths
have. `attachIntoGuest(req.Where)` still (correctly) hands the agent the raw path, so the agent binds
the drive at the stable path — but the controller registers and watches the raw one.
- `registerStoragePath` then calls `appbackup.EnsureUserdataSkeleton(where=/mnt/felhom-flash)`
(storage_handlers.go:230), which `mkdir`s the path → **this is what created the empty
`/mnt/felhom-flash` / `/mnt/felhom-usb` placeholder dirs on the guest rootfs** (E4, Jun 23 14:52).
Why this path ran: after a destroy + re-provision the HOST drive mounts survive (E2), so on the fresh
controller (registry wiped) the drives appear *mounted-but-unregistered* → the natural UI action is
"Regisztrálás" → `POST /api/storage/register``handleStorageRegister` → the raw-path bug. The
auto-inferred label in E6 confirms this handler (not the user-labelled free-form add at `handlers.go:1310`).
---
## 5. The five required answers
1. **Root cause (one sentence):** The controller's `handleStorageRegister` enroll path registers the
drive's **raw** `/mnt/<name>` mount path instead of the **stable** `/mnt/felhom-drives/<name>` path the
agent binds it at, so the controller watches an empty placeholder dir on the guest rootfs while the
real drive is live (and correctly bound) at the stable path. Evidence chain: E2/E4/E5/E8 (drive live at
stable, host→guest→container) vs E6 (raw path in registry) → source storage_handlers.go:682.
2. **Is v0.40.0 `mp1` implicated?** **No.** `/mnt/sys_drive` = `disk-2`, `[PASS]` (E1/E7); the
drive-enroll registration path is wholly independent of the golden's `mp1` volume and of `bringup.go`.
The bug lives in controller v0.77.0 `handleStorageRegister`, unchanged by the agent v0.40.0 work.
3. **Re-provision-specific or fresh-enroll too?** It is a **genuine code bug in the
"register-already-mounted-drive" path**, not a stale-state artifact. It is *surfaced* by the
re-provision because surviving host mounts make "Regisztrálás" the natural action. A **first-time**
enroll of a **new** drive uses "Inicializálás" (`runStorageInit`) or "Csatolás" (`runStorageAttach`),
which map to the stable path correctly and do **not** hit it. So: any use of the *register existing
mount* action is broken (Day-0 relevant for that flow), most reliably triggered by re-provision.
4. **"Rendszermeghajtón" label:** **Purely downstream**, not an independent bug. `CheckBackupDestination`
/ the storage view use `isSameBlockDevice(path, "/")` (`system/mounts_linux.go:173,225`) — `stat()` on
`/mnt/felhom-flash` (an empty dir on rootfs) yields the same `st_dev` as `/``SystemDrive=true`; and
`GetDiskUsage` returns the 32 GB rootfs (≈31.2 GB). The label faithfully reports that the *registered
path* is on the system drive — because it is the wrong path. Register the stable path and `stat`
resolves to `sdc1`/`sdb1` (different device) → badge clears.
5. **Recommended fix direction (NOT implemented):** In `handleStorageRegister`
(storage_handlers.go:667-694), map the incoming raw path to the stable path before registering —
`stable := stablePathForName(path.Base(req.Where)); s.registerStoragePath(stable, req.Label, …)`
exactly as `runStorageInit`/`runStorageAttach` do; keep `attachIntoGuest(req.Where)` on the RAW path
(the agent operates on raw). Consider the same guard for the free-form add at `handlers.go:1310` when
the path is a `/mnt/<name>` drive. **Remediation for 9201** (separate from the code fix): deregister
the two raw `storage_paths`, re-register at the stable paths (or re-enroll once the fix ships), and
remove the stray `/mnt/felhom-flash` `/mnt/felhom-usb` skeleton dirs on the rootfs.
**What NOT to do:** do **not** "fix" by auto-creating `felhom-data` under `/mnt/<name>` or by making
the agent bind the drive at `/mnt/<name>` — that breaks the intermediary model (live hot-swap +
confinement). The drive is already correctly at `/mnt/felhom-drives/<name>`; the only thing wrong is
which path the controller wrote down. This is a **normal controller change** (Docker-only,
build+deploy) — **not** a risky/supervised agent/golden task.
---
## 6. Secondary observations (not acted on)
- **Double-bind churn:** the agent normalizes `prior_binds=2 → 1` on `/mnt/felhom-drives/<name>` every
~20s tick (E8) — the pre-existing double-bind under the shared parent (each drive appears twice in
`/proc/mounts`). Cosmetic here; flagged in prior memory as the v0.36.x peer-group area. Out of scope.
- **`pendingActivationDrives` raw/stable coupling** (storage_handlers.go:188-199): it keys `attached`
by the agent's raw `MountPath` and compares against the registered `sp.Path`. With the fix (stable
registration) this comparison would never match a raw key, so the "pending activation" detection for
stable paths should be revisited to key off `guest_path` / `bound_under_parent` (as `planDriveGates`
already does). Worth checking when fixing #5 so the activation banner reflects the stable model.