From 45052da4dcb450686a79c34dba05fdd4f1771dfb Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Sun, 14 Jun 2026 18:14:08 +0200 Subject: [PATCH] docs(spike): bake in locked fork decisions (B0 gate cleared) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1) keep operator-signed decommission; 2) Q4 both — intent-aware ReassertGuestBinds load-bearing/sufficient, GuestBindStore.Remove hygiene (re-commission re-Records via recordGuestBind, B2 to state); 3) data move = controller via rsync copy-verify-remove (not cp -a); 4) decommission role-gated unmount. Gate cleared; B1/B2 may proceed. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...SPIKE-decommission-migration-2026-06-14.md | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/documentation/audits/SPIKE-decommission-migration-2026-06-14.md b/documentation/audits/SPIKE-decommission-migration-2026-06-14.md index 8d92711..7f62fcc 100644 --- a/documentation/audits/SPIKE-decommission-migration-2026-06-14.md +++ b/documentation/audits/SPIKE-decommission-migration-2026-06-14.md @@ -49,10 +49,12 @@ Both enrolled storage paths (`/mnt/felhom-usb`, `/mnt/sys_drive/felhom-data`) ar inside the controller container. **Conclusion — chosen layer = CONTROLLER.** The controller already holds RW access to every enrolled drive -under /mnt, so it can perform an app's data move (`cp -a /mnt/X/... → /mnt/Y/...`) **itself**, in-process, -no agent round-trip for the bytes. The agent is needed only for the privileged side-effects that bracket the -move: **unmount** (eject/decommission) and **intent** recording. This matches the existing app-data backup -design (the controller already rsyncs between drives for Tier-2 off-drive copies). +under /mnt, so it can perform an app's data move **itself**, in-process, no agent round-trip for the bytes. +The agent is needed only for the privileged side-effects that bracket the move: **unmount** +(eject/decommission) and **intent** recording. **The move primitive is `rsync`, not `cp -a`** (see locked +decision #3): B1 needs copy→verify→remove that is idempotent/resumable — `rsync -a --checksum` (copy + +verify) → confirm → only then remove the source; a crash mid-copy just re-runs. rsync is already the +controller's cross-drive primitive (Tier-2 off-drive backup). --- @@ -247,15 +249,34 @@ rendszerből" remains the deliberate hard-remove when they're truly done. | Q | Finding | file:line | Recommendation | |---|---|---|---| -| Q1 | Controller mount is `-v /mnt:/mnt:rslave`, de-privileged; `rslave` is RW for content. **Live:** controller wrote+deleted markers under both enrolled /mnt paths. | `build-golden.sh:184` (`:173-178`); live 9201 | **Data move = CONTROLLER** (in-process `cp -a` between /mnt drives); agent only for unmount + intent. | +| Q1 | Controller mount is `-v /mnt:/mnt:rslave`, de-privileged; `rslave` is RW for content. **Live:** controller wrote+deleted markers under both enrolled /mnt paths. | `build-golden.sh:184` (`:173-178`); live 9201 | **Data move = CONTROLLER** (in-process **`rsync -a --checksum`**, copy→verify→remove, between /mnt drives); agent only for unmount + intent. | | Q2 | Migrate button is a vestigial greyed ``, no handler/route/JS; `SetDecommissioned` never called. | `settings.html:337`; `settings.go:821` | Build the whole round-trip: data-move executor, HDD_PATH rewrite+redeploy, `POST /api/storage/migrate`, source decommission, registry mark. | | Q3 | Decommission today = operator-signed only (Destructive in `Classify`); executor only sets intent; controller has no endpoint/agentapi method. Eject is the self-serve precedent. | `classify.go:36,103-107`; `decommission.go:54-79`; `main.go:437`; eject `server.go:239`+`disks.go:209` | Add self-serve `POST /disks/decommission` (withGuest, user-data role-gate, durable-id resolved server-side, intent+bind-remove+unmount, **no wipe**). Hub can't reach local API (8443 LAN, per-guest token, outbound-only); compromised controller gains nothing beyond eject/wipe. **Keep** operator-signed path for absent-drive/DR. | -| Q4 | `GuestBindStore` has no `Remove`; `ReassertGuestBinds` ignores intent; watchdog gates host-mount to `enrolled`. **Live:** an `ejected` drive still sits in `guest-binds.json`. Gap: a decommissioned-but-still-mounted drive would be re-bound on agent restart. | `guestbindstore.go` (no Remove); `disks.go:643,649-654,673-677`; `watchdog.go:174-178`; live intent/bind state | **Both:** intent-aware `ReassertGuestBinds` (skip non-`enrolled`) [must-have] + decommission endpoint `Unmount` + new `GuestBindStore.Remove` [hygiene/DiD]. | +| Q4 | `GuestBindStore` has no `Remove`; `ReassertGuestBinds` ignores intent; watchdog gates host-mount to `enrolled`. **Live:** an `ejected` drive still sits in `guest-binds.json`. Gap: a decommissioned-but-still-mounted drive would be re-bound on agent restart. | `guestbindstore.go` (no Remove); `disks.go:643,649-654,673-677`; `watchdog.go:174-178`; live intent/bind state | **Both:** intent-aware `ReassertGuestBinds` (skip non-`enrolled`) = **load-bearing correctness fix, sufficient alone** (covers self-serve + signed paths) + `GuestBindStore.Remove` = hygiene/DiD (re-commission re-`Record`s via `recordGuestBind` — B2 must state). | | Q5 | `SetDecommissioned` keeps the entry as a soft marker (Decommissioned=true, Schedulable/IsDefault=false, MigratedTo); UI renders it. | `settings.go:821-839`; `handlers.go:855-857`; `settings.html:242-256` | **Keep soft marker** — it blocks A1-additive resurrection (skip-by-presence) + preserves audit. Hard-remove would risk re-registration if any reference lingers. | -## Forks needing the product owner's call (GATE) +## Decisions — LOCKED by product owner (2026-06-14). Gate cleared; B1/B2 may proceed. -1. **Operator-signed decommission** — keep as DR/absent-drive path (**recommended**) vs remove. → *recommend keep.* -2. **Q4 fix shape** — intent-aware `ReassertGuestBinds` only, vs `GuestBindStore.Remove` only, vs **both** (**recommended**). → *recommend both.* -3. **Data-move layer** — controller-side `cp -a` (**recommended**, proven live) vs agent-side mover. → *recommend controller.* -4. **Decommission = unmount?** — endpoint unmounts the drive (mirror eject, so it's physically removable; **recommended**) vs leaves it host-mounted. → *recommend unmount.* +1. **Operator-signed decommission — KEEP.** Retained as the DR / absent-drive path alongside the new + self-serve endpoint. (Q3.) + +2. **Q4 fix — BOTH, with a clear load-bearing split:** + - **Load-bearing (correctness):** make `ReassertGuestBinds` **intent-aware** — skip any durable-id whose + intent is not `enrolled`. **This alone is sufficient** for the invariant: it covers BOTH the self-serve + decommission AND the operator-signed (absent-drive) path, because both routes set + `IntentDecommissioned` and the guard keys on intent, not on who set it. + - **Hygiene (not correctness):** `GuestBindStore.Remove` keeps `guest-binds.json` honest for *present* + drives (no lingering records like the live `ejected` one). It is defense-in-depth, not the guard. + - **Nuance the B2 spec MUST state:** if `Remove` is implemented, **re-commission must re-`Record`** the + bind. This is already covered — `recordGuestBind` runs on every enroll/guest-attach + (`disks.go:290,309`) — but B2 must call it out explicitly so a future change doesn't break it. + +3. **Data-move layer — CONTROLLER, via `rsync` (NOT `cp -a`).** This is a **B1 implementation choice, not a + fork.** B1 requires **copy → verify → remove**, idempotent/resumable; `cp -a` gives neither resumability + nor verification. `rsync` is already an existing controller primitive (cross-drive Tier-2 backup, per + CONTEXT). B1 contract: `rsync -a --checksum` (copy + verify) → confirm success → **only then** remove the + source. A crash mid-copy simply re-runs (idempotent). [Supersedes the `cp -a` wording above in Q1 / the + summary table — same layer, better primitive.] + +4. **Decommission UNMOUNTS — agreed.** The endpoint performs a **role-gated** unmount (mirror eject) so the + drive is physically removable. (Q3 / Q4 belt.)