# SPIKE — Self-serve raw-drive plug-and-enroll

**Date:** 2026-06-15
**Type:** GATE spike — report-only + one live throwaway experiment on `/dev/sdc`.
**Host:** `felhom-pve` (node `demo-felhom`), PVE 9, guest 9201.
**Status:** findings complete; live FORMAT path proven end-to-end; sdc left as a clean enrolled `felhom-flash` PVE storage (B3 migration target).
**No production code, no version bump.** The build spec is gated on the fork picks at the end.

> **Goal (operator's vision):** a customer plugs in a drive and enrolls it from the controller UI with **no operator/Proxmox tinkering**. Flow: agent detects the raw drive → decides usable-as-is vs needs-format → `storage/init` shows it (model/size/serial, partitions+filesystems, recommendation) → customer chooses → controller tells agent → agent creates a PVE dir storage (formatting only if chosen) → binds the `felhom-data` namespace into the guest (existing `AttachBind`) → drive usable.

---

## One-screen summary

The flow is **mostly assemblable from primitives that already exist**; the genuine net-new is a **raw-disk discovery source** (the agent is blind to any disk that isn't already a PVE storage) and a **create-storage call** (the agent never creates PVE storages today). The security classifier is **already written** (`RoleForRawDevice`) and correctly protects the system disk live — but it needs **two hardening additions** (LVM-PV membership + whole-disk-granularity "already-enrolled" subtraction) before it can drive a *discovery* list.

Proven live on this host:

| Disk | What it is | disks/list `used` | Classifies | Why |
|---|---|---|---|---|
| **sda** SSD 512G | boot/root: ESP + LVM VG `pve` (root, swap, local-lvm thinpool, **guest 9201's own disks**) | `BIOS boot` | **PROTECTED (system)** | only LVM PV (`/dev/sda3`); `/boot/efi`=`sda2` pins it |
| **sdb** HDD 1T | enrolled data drive `felhom-usb`, ext4 `/mnt/felhom-usb` | `ext4` | **PROTECTED (enrolled data)** | backs registered storage `felhom-usb` |
| **sdc** USB 128G | throwaway flash, junk iso9660+EFI+hfsplus | `iso9660`→(after)`ext4` | **OFFERABLE** | removable, backs nothing, not a system disk |

The live FORMAT experiment (Q6) turned sdc into a registered `felhom-flash` dir storage in **one PVE API call**, and it **immediately appeared in the controller's agent-drive view as user-data / data-bearing / "Nem regisztrált" with a `Regisztrálás` button** — the existing enroll path picks it up with no agent restart.

**Spec premise correction:** there is **no local PBS disk** on this host. `felhom-pbs` is a *remote* `pbs:` storage (`server 192.168.0.180`); it has no backing block device. The "PBS disk classifies PROTECTED" assertion is satisfied at the *storage* tier (`RoleForStorage` → `RoleBackup`), not at any raw-disk tier.

---

## Q1 — Current discovery (the gap), confirmed in source

`internal/storage/observe.go:131,139` — `Observe` builds its entire view from exactly two PVE calls and nothing else:

```go
cfg, _  := o.api.ListStorage(ctx)   // observe.go:131  → GET /storage
node, _ := o.api.NodeStorage(ctx)   // observe.go:139  → GET /nodes/{node}/storage
```

The proxmox client (`internal/proxmox/query.go`) has **no raw-disk method**. Full read surface: `Version, Nodes, NodeStatus, ListLXC, GuestStatus, GuestConfig, ListStorage(:58), NodeStorage(:64), StorageContent, LatestBackupVolID`. None call `/nodes/{node}/disks/*`.

Critically, the localapi disk endpoint is **also** sourced from `Observe`, not from any block-device scan:

```go
// internal/localapi/disks.go:130 (handleDisks, GET /disks)
targets, err := s.storage.Observe(r.Context())
```

**Conclusion:** a raw `/dev/sdc` that is **not yet a PVE storage is invisible to both the hub-facing `Observe` and the localapi `GET /disks`.** A drive must *already* be a registered PVE storage before anything in the agent can see it. That is precisely the missing primitive. (`host.Removable`, observe.go:182, only annotates a storage that is *already* in the view — it does not discover.)

**What's missing, exactly:**
1. A proxmox-client (or host-level) method to enumerate raw block devices.
2. A discovery path in the agent that lists *candidate* disks (not-yet-storages) and runs the safety classifier over them.
3. A create-storage primitive (Q5).

---

## Q2 — Raw-disk discovery API, validated live

`pvesh get /nodes/demo-felhom/disks/list --output-format json` (abridged):

```json
[
 {"devpath":"/dev/sda","type":"ssd","size":512110190592,"model":"AirDisk_512GB_SSD","serial":"QDF922W009654S30EX","used":"BIOS boot","gpt":1,"health":"PASSED","osdid":-1,"by_id_link":"/dev/disk/by-id/ata-AirDisk_512GB_SSD_QDF922W009654S30EX"},
 {"devpath":"/dev/sdb","type":"hdd","size":1000204886016,"model":"TOSHIBA_MQ04ABF100","serial":"65NOP3HDT","used":"ext4","gpt":1,"rpm":"5400"},
 {"devpath":"/dev/sdc","type":"usb","size":128320801792,"model":"Flash_Drive","serial":"0375123010001586","used":"iso9660","gpt":1,"health":"UNKNOWN"}
]
```

`disks/list` gives: `devpath, by_id_link, type (ssd/hdd/usb), size, model, serial, wwn, health, gpt, osdid, used`. The `used` enum is the single most useful built-in signal but it is **insufficient alone**:
- It distinguishes sda (`BIOS boot`) but reports both sdb (enrolled data) and sdc (junk) as just a filesystem type (`ext4` / `iso9660`). After enrollment sdc *also* reads `ext4` — identical to the protected sdb. `used` cannot separate "offerable" from "already-enrolled data".
- It does **not** report removable, nor which disk holds LVM PVs / the guest's image / the root mount.

Companion endpoints:
- `pvesh get /nodes/demo-felhom/disks/lvm` → VG `pve` on `/dev/sda3`, `lvcount 9`. **Authoritative LVM-PV-membership signal.**
- `pvesh get /nodes/demo-felhom/disks/directory` → `[]` **before** the experiment; **after**, lists only `felhom-flash`. **It does NOT list `felhom-usb`** — PVE's directory tracking only reflects storages *it* created via the `disks/directory` API, not the agent's own systemd mounts. So `disks/directory` is **not** a reliable enumerator of dir storages.

**Recommendation — discovery source:** use **`disks/list` as the candidate enumerator**, cross-referenced with **`disks/lvm`** (PV membership) and the agent's **existing host-mount read** (`SystemDisks`, which already parses the mount table) and the **`Observe` storage view** (to subtract already-enrolled disks). The removable flag is already available to the agent via `/sys/block/<disk>/removable` (`hostread.go`, `ProcHostReader.Removable`) and via `lsblk RM`; PVE's `disks/list` does not expose it, so keep reading it host-side. A small `lsblk -J` is still worthwhile to get serial/by-uuid/child-partition detail per disk for the `storage/init` display, but the *safety* decision is driven by `disks/list` + `disks/lvm` + mount-table + storage-view, not by labels.

---

## Q3 — The safety gate (security-critical core)

**Good news: the classifier primitive already exists and is correct for the system disk.** `internal/storage/role.go`:

- `SystemDisks(host)` (role.go:46) — resolves the OS whole-disks from the **mount table** (`/`, `/boot`, `/boot/efi` → whole disk), root-free. On this host `/` is on `pve-root` (device-mapper, untraceable to a raw disk), but **`/boot/efi` = `sda2` pins `sda`** exactly as the code comment intends.
- `RoleForRawDevice(device, sysDisks, sysKnown)` (role.go:133) — system-vs-user-data for a raw `/dev` path; **defaults to system on ambiguity**.
- `RoleForStorage(...)` (role.go:105) — tiers a *registered* storage; PBS→backup, USB/local-dir-on-non-system-disk→user-data, everything else→system.
- `deviceRole(...)` (disks.go:836) — wraps both: prefers a matching storage target's role, else `RoleForRawDevice`.

**Validated live** (reproducing the agent's logic; `sysDisks={/dev/sda}` via `/boot/efi`=`sda2`, `sysKnown=true`):
- `RoleForRawDevice("/dev/sda")` → `isSystemBacked` → `sysDisks["/dev/sda"]` = **system** ✓ PROTECTED.
- `RoleForRawDevice("/dev/sdc")` → not in set → **user-data** ✓ offerable.
- `felhom-pbs` (`RoleForStorage` PBS) → **backup** ✓ PROTECTED (storage tier; no device).

Confirmed at the UI: the controller's agent-drive view renders `local`=🔒system, `local-lvm`=🔒system, `felhom-pbs`=🔒backup, `felhom-usb`/`felhom-flash`=user-data.

### Two hardening gaps that MUST close before this drives a *discovery* list

1. **Whole-disk-granularity "already-enrolled" subtraction.** `deviceRole` matches a registered storage by **exact** `t.BackingDevice == device` (disks.go:840). The format endpoint passes the exact backing device, so this works *there*. But a discovery flow enumerates **whole disks** (`/dev/sdb`), while `felhom-usb`'s backing device is the **partition** `/dev/sdb1`. The exact match fails → fall-through to `RoleForRawDevice("/dev/sdb")` → **user-data → would be offered for format**, even though it holds live customer data. **Fix:** the offerable filter must subtract any disk whose **whole-disk** equals `wholeDiskOf(t.BackingDevice)` for *any* `Observe` target — compare at whole-disk granularity, not device-path equality.

2. **LVM-PV / guest-image membership.** `SystemDisks` resolves the system disk via the **mount table only**. On this host that catches `sda` because the ESP is on it. But a disk that holds `local-lvm` PVs or guest disk images **without** carrying `/boot`/`/boot/efi` would be **missed** (→ misclassified user-data). **Fix:** add a positive PV-membership check from `disks/lvm` (any disk whose partition is a PV of the `local-lvm` VG, or the VG that backs any guest's `rootfs`/`mp`), folding it into the protected set alongside `SystemDisks`.

### Proposed offerable predicate (the gate)

A whole disk `D` is **OFFERABLE** iff **all** of:
- `RoleForRawDevice(D) == user-data` (not the OS/boot disk; fail-safe system on ambiguity), **and**
- `D` is **not** in the LVM-PV/guest-image protected set (`disks/lvm` + guest-config backing), **and**
- `wholeDiskOf(D)` backs **no** registered `Observe` target (subtract already-enrolled, whole-disk granularity), **and**
- `D` is **not** a local PBS datastore disk (here: N/A — PBS is remote).

Everything else (incl. on **any** ambiguity) → **PROTECTED, never listed for format**. This extends the existing *"mkfs-not-on-data-bearing-device"* discipline (`disks.go:526`, `InspectDevice` blkid+lsblk) to *"mkfs-not-on-system-disk."*

### Non-hollow test shape

A test that **FAILS if the system disk is ever offerable**, fed a realistic topology fixture (sda=ESP+LVM-PV+guest images; sdb=ext4 backing a registered `felhom-usb`; sdc=removable junk):

```
TestOfferableDisks_NeverOffersProtected:
  offer := OfferableDisks(disksList, lvmView, observeTargets, hostMounts)
  assert sda  NOT in offer        // ESP + sole LVM PV  → system
  assert sdb  NOT in offer        // backs registered felhom-usb (whole-disk subtraction)
  assert sdc  IN  offer           // removable, backs nothing, not system
  // adversarial: relabel sdc's disks/list "used" to "BIOS boot" (lying label)
  //   → still offerable iff state says so; and relabel sda "used"="" (lying blank)
  //   → MUST still be protected via ESP/LVM-PV state, not the label.
  // mutation guard: drop the LVM-PV check → test MUST fail on a PV-only protected disk fixture.
```

The mutation clause (removing either hardening check makes the test red) is what keeps it non-hollow.

---

## Q4 — Usable-as-is vs needs-format

Decision driven by the agent's **existing** `InspectDevice` (`internal/storage/hostops.go`, blkid `-p -o export` + `lsblk -J`) which already yields `HasFilesystem/FSType/HasPartitionTable/HasPartitions/Mounted`:

- **Enroll-as-is** iff the disk presents **exactly one mountable, supported data filesystem** we can own — i.e. a single partition (or whole-disk fs) with `FSType ∈ {ext4, xfs}` (the fs types the agent's mount machinery + PVE dir storage support), not currently mounted by the host/another storage, and not the system/protected set.
- **Needs-format** iff: no filesystem, an unsupported fs (the live sdc had **iso9660** + a **vfat** ESP + **hfsplus** — none ownable), a partition table with **multiple data partitions** (ambiguous which to adopt — sdc had 4), or any signature we will not silently adopt.
- **On ambiguity → recommend format** (never silently adopt an unknown layout), mirroring the destructive-on-ambiguity invariant inverted for safety: we don't *destroy* on ambiguity, we *refuse to enroll-as-is* and require an explicit format choice.

**What `storage/init` should display** per candidate:
- Identity: model, size, serial, bus type (ssd/hdd/usb), removable badge — all from `disks/list` + `lsblk`.
- Layout: each partition with size + detected fs (`lsblk -J -o NAME,FSTYPE,SIZE,MOUNTPOINT`), plus the data-bearing verdict + reason from `InspectDevice`.
- Recommendation: **"Enroll partition X as-is (ext4, N GB)"** when the as-is predicate holds, else **"Format required — current contents will be erased"** with the detected junk listed (e.g. "iso9660 + EFI + hfsplus").

---

## Q5 — PVE-storage creation via the agent — BOTH paths

### FORMAT path — validated live (one call does everything)

```
pvesh create /nodes/demo-felhom/disks/directory \
    --name felhom-flash --device /dev/sdc --filesystem ext4 --add_storage 1
```

The single call ran synchronously and performed, in order:
```
# /sbin/sgdisk -n1 -t1:8300 /dev/sdc                  → partition sdc1
# /sbin/mkfs -t ext4 /dev/sdc1                          → fs UUID 81a26531-62d8-408d-812f-a178b1d35310
# /sbin/blkid /dev/sdc1 -o export
Created symlink …/mnt-pve-felhom\x2dflash.mount         → systemd mount unit, enabled
add_storage 1                                           → wrote storage.cfg
```

Resulting `storage.cfg` entry:
```
dir: felhom-flash
        path /mnt/pve/felhom-flash
        content rootdir,images,backup,iso,snippets,vztmpl
        is_mountpoint 1
        nodes demo-felhom
```

Resulting systemd unit (`mnt-pve-felhom\x2dflash.mount`):
```
[Mount]
What=/dev/disk/by-uuid/81a26531-62d8-408d-812f-a178b1d35310
Where=/mnt/pve/felhom-flash
Type=ext4
Options=defaults
```

**Conventions check:**
- `is_mountpoint 1` ✓ (locked convention).
- Mount unit keyed by **fs-UUID** (`/dev/disk/by-uuid/…`) ✓ — matches the agent's own `mountunit.go` (`What=/dev/disk/by-uuid/<UUID>`).
- Durable-id will derive **`uuid:81a26531-…`** from the storage's own fs UUID ✓ (`durableid.go:deriveDurableID`, USB/local-dir → `uuid:`).
- **Mount path = `/mnt/pve/felhom-flash`** ✗ — diverges from the felhom `/mnt/<name>` convention (`felhom-usb` is `/mnt/felhom-usb`). PVE's `disks/directory` API has **no option to override the path**; it is always `/mnt/pve/<name>`. → **FORK.**
- **`content` = all types** ✗ — `felhom-usb` is `content backup`. The data drive that holds the bind-mounted `felhom-data` namespace doesn't *use* PVE content types, but the entry should be normalized for consistency. → minor fork.

### ENROLL-AS-IS path — validated by the living reference

`felhom-usb` itself **is** the enroll-as-is proof: it is a `dir` storage with `is_mountpoint 1` at `/mnt/felhom-usb`, **absent from `disks/directory`** → it was created by **mounting an existing fs + `pvesm add dir`**, *not* by PVE's formatting `disks/directory` API. The agent already owns both halves:
- `EnsureMount` / `renderMountUnit` (`internal/storage/mountunit.go`, `HostOps.EnsureMount`) — stages a by-UUID systemd `.mount` unit at `/mnt/<name>`, `daemon-reload`, `enable --now`. **No mkfs.**
- the missing half is the `pvesm add dir <id> --path /mnt/<name> --is_mountpoint 1 --content …` call (the agent does not create PVE storages today — see Q5 net-new).

So enroll-as-is = `EnsureMount(existing-fs-UUID, /mnt/<name>)` + `add-dir-storage` — both primitives understood, one not-yet-wired.

### Both paths feed the existing enroll flow

`Observe` iterates `NodeStorage`, so **either** path's resulting storage is picked up with **no agent restart** (proven for the format path below). The customer-facing enroll (`AttachBind`, disks.go `handleDiskGuestAttach`) then binds `<where>/felhom-data` into the guest.

---

## Q6 — Live experiment (end-to-end, sets up B3)

Every call run against `felhom-pve`, all guarded by a **serial-identity fence** (`/dev/sdc` serial must equal `0375123010001586`, must be removable `RM=1`, must have no mountpoint — abort otherwise) so sda/sdb could never be touched.

1. **Guard + wipe.** Guard passed (serial matched, RM=1). `wipefs -a /dev/sdc` erased the iso9660 + 2×GPT + PMBR signatures; `sgdisk --zap-all` cleared the GPT. After: `lsblk` shows bare `sdc` (no fs), and `disks/list` reports `sdc used=None gpt=0` — **PVE's `used=None` is the "genuinely blank" signal.**
2. **Create (format path).** The `pvesh create …/disks/directory` call above — partition + mkfs ext4 + systemd mount + `add_storage`, one call, synchronous success (`UPID:…:dircreate:felhom-flash:…`).
3. **Verify storage.** `storage.cfg` entry written (above); `findmnt /mnt/pve/felhom-flash` → `/dev/sdc1 ext4 rw`; fs UUID `81a26531-…`; `disks/directory` now lists felhom-flash (by-uuid device); `disks/list` sdc now `used=ext4`.
4. **Verify Observe.** `pvesh get /nodes/demo-felhom/storage` → `felhom-flash dir active=1 total=125704151040` (~117 GiB) — the agent's `Observe` source now contains it.
5. **Verify enrollable in controller.** The controller settings page → *"Meghajtók (ügynök nézet)"* now shows:
   > **felhom-flash** — `usb · /dev/sdc1 · /mnt/pve/felhom-flash` · **Felhasználói adat** · **Adatot tartalmaz** · **Nem regisztrált** · buttons **Regisztrálás** / Leválasztás / Törlés…

   i.e. classified **user-data**, data-bearing detected, **not yet enrolled, with a one-click `Regisztrálás` (enroll = `AttachBind`)**. End-to-end mechanism proven.

**State left for B3:** sdc is a **clean, registered `felhom-flash` PVE storage** (ext4, mounted `/mnt/pve/felhom-flash`, is_mountpoint, in the agent view, classified user-data). It is **PVE-enrolled and "ready-to-enroll-into-guest"** — the final `AttachBind` + guest reboot was intentionally **not** run to avoid disrupting live guest 9201; that is the one remaining step a real customer enroll performs. This is the B3 migration target.

---

## Q7 — Wiring map (net-new vs reused)

### Reused (already exists, no change)
| Capability | Where |
|---|---|
| System-disk resolution (mount table → whole disk) | `storage/role.go:46 SystemDisks` |
| Raw-device role classifier (system vs user-data, fail-safe) | `storage/role.go:133 RoleForRawDevice` |
| Registered-storage role tiering (PBS=backup, etc.) | `storage/role.go:105 RoleForStorage` |
| Data-bearing inspection (blkid + lsblk) | `storage/hostops.go InspectDevice` + `disks.go:526` gate |
| Mount existing fs (by-UUID systemd unit at `/mnt/<name>`) | `storage/mountunit.go`, `HostOps.EnsureMount` |
| Storage view the controller consumes | `storage/observe.go Observe` (iterates ListStorage/NodeStorage) |
| Bind `felhom-data` namespace into guest + reboot | `localapi/guestbind.go:48 AttachBind` |
| Enroll/assign/eject/decommission endpoints | `localapi/disks.go` (`/disks`, `/disks/assign`, `/disks/guest-attach`, …) |
| Durable-id (uuid:) derivation | `storage/durableid.go deriveDurableID` |

### Net-new
| Capability | Lands in | Note |
|---|---|---|
| Raw-disk enumeration (`disks/list`, `disks/lvm`) | `internal/proxmox` (new `ListDisks`, `ListDisksLVM`) | the client has no `/disks/*` method today |
| Whole-disk **wipe** (`wipefs`/`sgdisk`) | `storage.HostOps` (new) | gated by the Q3 offerable predicate |
| **Create dir storage** — format path (`disks/directory` create) | `internal/proxmox` (new `CreateDirectoryStorage`) **or** a host-side `pvesh` call in `HostOps` | one call = partition+mkfs+mount+add_storage |
| Create dir storage — **enroll-as-is** (`pvesm add dir … --is_mountpoint 1`) after `EnsureMount` | `storage.HostOps` (new `AddDirStorage`) | the agent creates **no** PVE storages today |
| Raw-disk **discovery + Q3 offerable filter** (combine disks/list + disks/lvm + SystemDisks + Observe subtraction, whole-disk granularity) | `internal/storage` (new `OfferableDisks`) + a new localapi endpoint e.g. `GET /disks/raw` | the security core; needs the two Q3 hardening checks |
| New localapi endpoints: list-raw, init (format), attach-as-is | `internal/localapi/disks.go` (new handlers) | mirror the existing `/disks/*` handler shape + `withGuest` scoping |
| Controller `storage/init` UI: raw-disk list, identity/layout/recommendation, format-vs-enroll choice | controller (felhom-controller) | the *"🔧 Új meghajtó inicializálása"* / *"🔗 Meglévő meghajtó csatolása"* buttons already exist on the settings page as entry points |

### The controller-decision → agent-task → done handshake
Lives exactly where the existing disk ops live: the controller POSTs the customer's choice to a **new localapi endpoint** (init/format or attach-as-is), `withGuest`-scoped + TLS-pinned, same as `POST /disks/assign` / `POST /disks/guest-attach` today. The agent **re-derives** the safety verdict from device state (never trusting the controller's claim — same discipline as `handleDiskFormat`), executes, and the result surfaces back through `Observe` → the controller's agent-drive view (proven live). No new transport, no hub round-trip: this is host-agent-local, exactly like the current enroll path. Final guest usability is the existing `AttachBind` + reboot.

---

## FORK LIST (operator decides before the build spec)

1. **Discovery source.** `disks/list` + `disks/lvm` + agent mount-table + Observe subtraction (recommended, no extra shell-out for the *safety* decision) — vs additionally shelling `lsblk -J` for richer per-disk display. *Recommendation: both — disks/list+lvm for the safety gate, lsblk for the init-screen detail.*
2. **Safety-gate signals — ship vs hardened.** Mount-table `SystemDisks` only (works on this host via the ESP) — vs **+LVM-PV membership +guest-image backing +whole-disk-granularity enrolled-subtraction** (required to be robust on a host where the system isn't all on one ESP-bearing disk). *Recommendation: hardened — the two Q3 checks are non-negotiable for a customer-facing format gate; the mount-table-only version can offer a non-boot LVM disk.*
3. **Mount-path convention.** Accept PVE default `/mnt/pve/<name>` (free with `disks/directory`, but leaks PVE internals into the guest bind path) — vs roll-our-own `/mnt/<name>` via `EnsureMount` + `pvesm add dir` (matches `felhom-usb`, reuses agent machinery, but re-implements the format steps). *Recommendation: `/mnt/<name>` for consistency with the existing enrolled drive and a clean in-guest bind path.*
4. **Format-vs-enroll-as-is rules.** Single supported-fs partition → enroll-as-is; everything else (no fs / unsupported / multi-partition / ambiguous) → format. *Recommendation: as stated; format on any ambiguity, never silently adopt.*
5. **Endpoint shapes.** New `GET /disks/raw` (offerable list) + `POST /disks/init` (format path) + `POST /disks/attach-existing` (enroll-as-is), all `withGuest`-scoped, agent re-derives the gate. *Recommendation: as stated, mirroring `/disks/assign`.*
6. **v1 scope.** **Format-only first** (the path proven live this spike), enroll-as-is in v1.1 — vs both in v1. *Recommendation: format-only v1 (covers the plug-in-a-blank/junk-drive case, which is the common one), enroll-as-is fast-follow (the `felhom-usb`-style reuse-an-existing-ext4 case).*
7. **Content normalization.** Set `content backup` (or an explicit minimal set) on creation rather than PVE's all-types default. *Recommendation: yes, normalize.*
8. **`content`/storage create transport.** Typed proxmox-client method (`CreateDirectoryStorage`) vs host-side `pvesh` in `HostOps`. *Recommendation: typed client method, consistent with the rest of `internal/proxmox`.*

---

## Live cleanup state
- `sdc` → registered PVE storage **`felhom-flash`** (ext4, `/mnt/pve/felhom-flash`, is_mountpoint, fs-UUID `81a26531-62d8-408d-812f-a178b1d35310`), classified user-data, **not** guest-bound. Intentionally left as the B3 migration target.
- `sda`, `sdb` untouched (serial-fenced throughout).
- No agent/controller code changed; no version bump; no service restarted.
