From 13a02e487105febf0b076480595cbc9febab0f6a Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Fri, 12 Jun 2026 15:55:03 +0200 Subject: [PATCH] =?UTF-8?q?docs(v0.25.0):=20REPORT=20=E2=80=94=20slice=201?= =?UTF-8?q?0=20P1=20spike=20+=20P2=20passthrough;=20live-validated=20on=20?= =?UTF-8?q?9201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- REPORT.md | 87 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/REPORT.md b/REPORT.md index 3cbf49c..9bfb1f8 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,49 +1,50 @@ -# REPORT — felhom-agent v0.24.0: role-gate the eject path +# REPORT — slice 10: external user-data drive passthrough (P1 spike + P2) -## What changed (Part A of the storage-fixes spec) +**Agent v0.25.0** (+ controller v0.48.0, golden rebaked). P1 spike PASSED (gate); P2 BUILT + validated +live on guest 9201. P3 (self-heal) + P4 (dual-role) are the next phases. -`POST /disks/eject` is now **role-gated at the agent**. Previously `handleDiskEject` validated scope + -`where`, computed dependents, then unconditionally called `Unmount` — so a direct API call (or a -compromised controller) could `POST /disks/eject {where:"/var/lib/vz"}` (or the PBS mount) and unmount -a protected storage. The controller UI hid the button for system/backup, but the **agent did not -enforce it**, contradicting the "system is never unmountable" invariant and the defense-in-depth the -wipe path upholds. +## PHASE 1 — SPIKE (GATE PASSED), four proofs on 9201 +- **1A host→guest bind:** `pct set 9201 -mp0 /mnt/felhom-usb,mp=/mnt/felhom-usb` — **bind form** (host + path), never `storage:size`. Propagation `shared:49` host↔guest automatic. +- **1B write — chown, not idmap:** idmap not clean (mixed host ownership 1000+0, container-wide, + restart, subuid). Decision (refined with Viktor): chown only a fresh **`/felhom-data`** + namespace to `100000:100000`; the customer's existing data is never touched. Guest-root r+w confirmed. +- **1C guest→controller-container:** `-v /mnt:/mnt:rslave` + `/mnt` rshared in the guest → newly-mounted + drives propagate into the running container (proven). The de-priv `/mnt` gap, reopened scoped. +- **1D app-container:** busybox bind read+write; bytes land on `/dev/sdb1`. -### A1 — refuse eject of system/backup (`internal/localapi/disks.go`) -- `handleDiskEject` now, before `Unmount`, resolves the **authoritative protection role** of the - storage mounted at `where` via the new `roleForMountPath` (the agent's own storage-view + - host-topology classification — `RoleForStorage` keyed on `MountPath == where` — never the caller's - claim). It refuses **403 with no `Unmount`** unless the role is `user-data`. -- **Fail-safe**: a view error or no storage target found at `where` → treated as `system` (most- - protected) → refused. Consistent with the wipe gate's most-protected-on-ambiguity default. -- Mirrors the wipe path's logging: `"protected — eject refused by role"`. -- `hostReader()` seam added (`Options.HostReader`, optional; defaults to the production - `*storage.ProcHostReader`) so the role classification (`SystemDisks`) is unit-testable. `handleDisks` - and `deviceRole` now share the same seam (no production behaviour change — default is the real reader). +## PHASE 2 — passthrough (Model A: the felhom-data namespace is the in-guest mount) +- **P2A agent — `POST /disks/guest-attach`** (`internal/localapi`, `GuestBinder`): self-scoped; creates + `/felhom-data`, chowns it to the guest base (not -R), and `pct set -mpN + /felhom-data,mp=/mnt/` (RW bind). Idempotent; lowest free `mpN`; `where` validated. Only + Felhom's namespace crosses into the guest — the customer's other on-drive data never does. Tests: + `TestGuestAttach_*` (slot select, idempotency, bad-path, not-configured). +- **P2B golden — `configs/build-golden.sh`:** controller `docker run` gains `-v /mnt:/mnt:rslave`; the + bootstrap makes `/mnt` rshared first. Scoped to `/mnt` (only felhom-data-namespace mounts). +- **P2C controller (v0.48.0):** `agentapi.GuestAttach`; `runStorageInit`/`runStorageAttach`/ + `handleStorageRegister` call `attachIntoGuest` after register (best-effort; P3 heals a miss). -### A2 — non-hollow test -- `TestEject_RoleGated` asserts a `system` mount and a `backup` (PBS) mount are **refused with no - `Unmount`**, a `user-data` mount **ejects** (Unmount called once), and an unresolvable mount **fails - safe to refused** — the same non-hollowness (executor-not-invoked) the wipe tests use. -- `TestEject_UnmountAndDependents` updated to a user-data target so the role-gate permits it. +## Live validation (9201) +After `guest-attach` + a guest restart to activate mp0: +- mp0 = `/mnt/felhom-usb SOURCE /dev/sdb1[/felhom-data]` (Model A; the `[/felhom-data]` suffix the + controller's mount strip already handles). +- Controller container mountinfo has `/felhom-data /mnt/felhom-usb … /dev/sdb1`. +- An app (busybox bind) writes `proof.txt` → present on the **host** `/mnt/felhom-usb/felhom-data/...`, + `df` device `/dev/sdb1` (NOT the rootfs). +- Banner cleared: **`[PASS] Storage paths: 1 connected, 0 disconnected`**. +- `go test ./...` green (both repos). -## Build / deploy -- Version bumped `0.23.0 → 0.24.0` (`cmd/felhom-agent/main.go`). CHANGELOG updated (newest on top). -- Built on the build server (go1.26) with `-ldflags -X main.version=0.24.0`, installed to - `/usr/local/bin/felhom-agent` on `felhom-pve` (prior binary kept as `.bak-0.23.0`), service - restarted — `felhom-agent --version` → `0.24.0`, local-api listening on `192.168.0.162:8443`. +## KNOWN LIMITATION — live activation (decision needed before P3) +`pct set` does **not** hot-apply a mountpoint to a *running* guest when the guest's `/mnt` is rshared +(the P2B rbind shadows the live hotplug; the spike's live hot-apply worked only because `/mnt` wasn't +yet shared). So a drive enrolled into a **running** guest activates on the **next guest restart** — the +end-state is correct, but live enroll isn't seamless. This is a genuine hotplug-vs-propagation fork +(like the chown/idmap + Model A decisions) to settle before P3: options are (a) enroll triggers a guest +reboot; (b) drop the guest `/mnt` rbind and have enroll do `pct set` (live hotplug works) + a +lightweight controller-container restart to capture it; (c) let P3 self-heal reconcile drive activation. +Fresh guests from the rebaked golden are unaffected (mp mounts activate at first boot, before the +controller starts). -## Live validation (felhom-pve, guest 9201) -- `GET /disks` (guest 9201): roles `felhom-usb=user-data`, `local=system`, `felhom-pbs=backup`, - `local-lvm=system`. -- Hand-issued `POST /disks/eject {where:"/var/lib/vz"}` (the `local` system storage's real mount) → - **HTTP 403**, body `mount is system/backup-protected — eject refused (role: system)`; agent logged - `protected — eject refused by role … role=system`; **no unmount** occurred. -- `POST /disks/eject {where:"/mnt/nonexistent"}` (no storage target) → **HTTP 403** (fail-safe). -- `/mnt/felhom-usb` (the live user-data data drive) was deliberately **not** live-ejected to avoid - disrupting the running guest; the positive user-data eject path is covered by the non-hollow - `TestEject_RoleGated` (asserts `Unmount` is called exactly once for a user-data mount). -- `go test ./...` green for the whole module. - -## Notes / not done -- No golden change required for Part A — the agent runs host-side, not baked into guests. +## Not done (next phases) +P3 self-heal watchdog reconcile (4-state new/enrolled/ejected/decommissioned) + safety rails; P4 +dual-role + backup-aware wipe warning. Cross-drive backup ENGINE remains out of scope.