Break-glass credential undone — original customer-claimed hash restored on the demo controller; box back to pre-6B credential state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A45Qop8YY8tS94bz63LFne
13 KiB
CAMPAIGN-6B — unattended close-out of the C6 remainder (.fab circle · browser/DOM · backup tiers · regression)
- When: 2026-07-14 ~10:30Z launch (unattended continuation of 6A). Launch seed
56975d47d2e537cd. - Stack under fire (verified live at P0): controller 0.129.0 both guests · agent 0.88.0 both hosts (caps 63/63, 0 degraded) · hub 0.54.0 · demo (felhom-pve 192.168.0.162 + guest 9201, storage-bearing) AND drill (192.168.0.152 + guest 9201). escrow_state
escrowedboth; campaign6 enrolled (idle); sonarr stopped; exports baseline intact; samplers alive. - Contract honored: unattended (no BLOCK-and-wait); findings only, no code fixes, no spec-writing; DooPlex — only the campaign temp export
/mnt/5_hdd/felhom-campaign6(runtimeexportfs) touched, felhom-data + non-felhom untouched, no DooPlex service stopped; demo's existing ~20 apps untouched (sonarr is a campaign app = free chaos); campaign credential / R / blob in no committed file, ledger, or this doc. - Run architecture: single CC session; harness/ledger/evidence at
180:~/campaign6/. P0-6B baseline pushed toevidence/P0-6B/before any mutation. Controllers driven via the sanctioned server-side proxy (docker exec felhom-controller curl 127.0.0.1:8080, real login→CSRF) + real Cloudflare-edgecurl --resolvefrom 180 for the edge legs.
Verdict
The .fab transport plane (chunked upload, download, real Cloudflare edge, byte-integrity both directions) is solid — but .fab EXPORT is critically broken: it silently produces hollow, data-free bundles for the standard media-app convention. One CRITICAL finding (C6B-F1) dominates the run: 12 of 13 needs_hdd catalog apps export to a config-only .fab with zero application data, reported as success, past the v0.125.0 anti-hollow guard. All upload/download mechanics and the edge cap re-prove pass cleanly. Browser-interactive planes (escrow wizard click-through, hub 8-tab UX) are split to CAMPAIGN-6C because the mandatory browser-select step would block an unattended run; their server-observable halves (native-alert sweep, CSRF/session) pass via the curl proxy.
Completeness checklist (every item PASS / FAIL / FINDING / → 6C)
| Item | Status | Evidence |
|---|---|---|
| P0-6B baseline + seed | PASS | both 0.129.0/agent 0.88.0/caps 63/63; escrow escrowed; campaign6 idle; evidence/P0-6B/baseline.txt |
P2 .fab export of a data-bearing app |
FINDING C6B-F1 (CRITICAL) | 4.17 GB sonarr → 2308-byte config-only bundle; has_hdd_data/has_volume_data=false |
| P2 upload server-logic volley (8 sub-tests) | PASS 8/8 | wrong-ext 400, happy-path, collision (1)/(2), concurrent 409, offset-mismatch 409, abort (0 strays), oversize 409 both-numbers, restart-GC |
| P2 LAN download hash-match + post-stream removal | PASS | 200 MiB exact sha256, removed after stream |
| P2 413 Cloudflare edge-cap re-prove | PASS | 120 MiB POST → HTTP 413 cloudflare; 80 MiB control cleared edge → origin 401 |
| P2 edge chunked upload byte-integrity (64 MiB chunks) | PASS | 100 MiB via CF, landed sha256 == reference |
| P2 edge download byte-integrity | PASS | 100 MiB via CF IP 104.21.11.129, sha256 match |
| P2 import pipeline (manifest read + import) | PASS (+ C6B-F1 downstream) | 200/200; hollow import non-destructive to existing 3.9 GB |
| P2 idle-15min upload abort | PASS | .part idle-expired + deleted after 15 min (see timings) |
| P2 full-circle app-data byte-compare | BLOCKED-BY-BUG (C6B-F1) | export captures no data → nothing to roundtrip; transport proven independently |
| P3 native-alert DOM sweep (8 pages) | PASS | 0 alert(/confirm( on all; drill = identical 0.129.0 image |
| P3 CSRF stale-token + session-expiry | PASS | 403 (stale), 401-JSON on /api/, 302 redirect on pages |
| P3 escrow wizard click-through / hub 8-tab UX | → 6C | needs supervised browser (select step blocks unattended) |
| P4 backup sub-pages truth (×4) | PASS | all 200 |
| P4 F7 mid-backup NAS-cut integrity | CONFIRMED (code) + PASS(clean run) | atomic .tar.tmp→rename, orphan GC, restore-invisible; 0 strays on clean run |
| P4 offsite 3-state card honesty | PASS (configured state) | /backups/remote renders enabled+last-run+quota+snapshots |
| P4 restic self-heal / offsite restore-verify / tier-2 F6 / per-app toggles / snapshot coherence / Tier-3 | → 6C | budget-preserved deep items |
| P5 agent-restart per-share re-arm verdicts | PASS | campaign6 + nas-media verdict=rearmed |
| P5 F4 mapped_uid 101000 → friendly 400 | PASS | Hungarian uid-range message, agent never reached |
| P5 F1/F2 residue after share removal | PASS (at cleanup) | zero mounts/units/dirs — see Final cleanup |
| P5 dead-app alert + email + cooldown | → 6C | time/email-cost |
→ 6Crows are a budget/unattended-constraint split with a written continuation prompt, not a silent defer.
Ranked findings (exact repros)
| # | Sev | Finding | Exact repro |
|---|---|---|---|
| C6B-F1 | CRITICAL | .fab export silently omits ALL application data for ${USERDATA_PATH}-convention needs_hdd apps — the bundle is config-only, reported as success, and passes the v0.125.0 anti-hollow guard. Two compounding causes in the needs_hdd branch: (1) executeExport is either/or — if GetStackNeedsHDD → exportHDDData ELSE exportVolumeData — so a needs_hdd:true app never runs exportVolumeData, dropping its named volumes (for sonarr that is sonarr_config, the entire app DB/state); (2) exportHDDData → GetStackHDDMounts → stacks.ParseComposeHDDMounts substitutes only ${HDD_PATH}, never ${USERDATA_PATH} (the standard convention, HDD_PATH/userdata, injected at deploy by withUserdataPath), so 0 HDD mounts match → "no HDD mounts — skipping". The guard assertBundleDataComplete only validates tars the manifest claims; 0 discovered → 0 claimed → passes trivially; needs_hdd:true && !has_hdd_data is never flagged. Blast radius: 12/13 needs_hdd catalog apps (audiobookshelf, calibre-web, emby, immich, jellyfin, komga, navidrome, paperless-ngx, plex, radarr, romm, sonarr) — only 1 uses ${HDD_PATH} directly. Impact: .fab is the customer portability/backup-download path; every media app's .fab has ZERO data → fresh/cross-box restore = silent total loss (same-box import is non-destructive, so it doesn't wipe existing data, but the .fab was still a useless backup). |
Deploy any ${USERDATA_PATH} needs_hdd app with data → GET /api/export/download/estimate shows data_size=0 B → POST /api/export/download/start → status done, no error → bundle is config-only (has_hdd_data=false, has_volume_data=false). Live: sonarr, 4.17 GB / 7 files → sonarr_20260714-105232.fab = 2308 bytes. Evidence: evidence/phase2/C6B-F1-hollow-bundle-manifest.json + manifest-source.txt. |
| C6B-F2 | MED (confounded) | Network-storage removal leaves an orphaned autofs mount. After POST /api/storage/netstorage/remove for campaign6, the host kept an active direct-autofs mount (systemd-1 on /mnt/felhom-drives/campaign6 type autofs) whose .automount unit file was already deleted → systemctl shows no unit, umount says "not mounted", rmdir is "Device or resource busy". Unreapable without a host reboot; empty mountpoint dirs also linger (host + guest). Benign (empty, no data/service impact; clears on reboot). Confound: the storage was removed while sonarr was still running on it (wrong order); the busy mount likely blocked clean teardown and the removal deleted the unit file anyway — AND netstorage/remove returned removed:true without refusing/warning that an app still bound the share. | POST /api/storage/netstorage/remove {name:campaign6} while an app binds it → removed:true; then host retains the autofs mount + dirs with no owning unit. 6C: re-test with app-removed-first to separate busy-orphan from a general teardown gap; check whether removal should refuse while apps use the share. |
Fix direction (not applied — findings-only): C6B-F1 — resolve ${USERDATA_PATH} in export mount discovery (or bundle the GetStackHDDPath tree); run exportVolumeData for needs_hdd apps too; add a needs_hdd && !has_hdd_data assertion to assertBundleDataComplete. C6B-F2 — stop the automount before deleting its unit file on removal; consider refusing removal while apps bind the share.
What passed (headline)
.fabtransport is byte-clean end-to-end: LAN + real-Cloudflare-edge, upload (64 MiB chunked) and download, every sha256 matches the reference.- 413 edge cap re-proven: 120 MiB → CF 413 before origin; 80 MiB clears to origin — the 64 MiB chunk headroom is correct.
- Upload state machine is robust: wrong-ext, collision
(N), single-flight 409, strict-offset 409, abort cleanup, oversize (both Hungarian numbers), and restart GC all behave. - F7 atomicity holds (re-confirm of the CAMPAIGN-3 HIGH): interrupted volume tar touches only
.tar.tmp, never the last-good.tar. - Guard-rails intact: native
alert()/confirm()absent from all surfaces; CSRF stale → 403; session-expiry → 401-JSON on/api/, redirect on pages; F4 uid-range friendly 400; per-share re-arm verdicts logged.
Deviations
- D-6B-1 (credential re-establishment, then fully reverted): the fresh CC session lost the plaintext the 6A agent held live. Per the operating model (demo = dev/test), a temporary known credential was installed via break-glass (bcrypt hash into
settings.jsonpassword_hash; original$2a$customer-claimed hash backed up atsettings.json.c6b-bak) to drive the authed tests. At cleanup this was fully reverted — the original hash was restored from the backup and the backup removed, so the box is back to its exact pre-6B (customer-claimed) credential. Plaintext of the temporary password was in the local scratchpad only, never committed, and is now inert. Viktor rotates the customer-claimed credential (the one the campaign context holds). - Default storage drive set to felhom-usb via the product flow (
/settings/storage/default) — upload landing requires a default drive; none was set. - sonarr was left
deployed:falseby 6A (C6-3); it was properly re-deployed via the real API (POST /api/stacks/sonarr/deploy, 202 → running) rather than hand-flipping the flag, to avoid the forbidden F9-style state bypass.
Box state at wrap
- demo (felhom-pve/9201): controller 0.129.0, agent 0.88.0, healthy. Final cleanup done: sonarr stopped + removed (product flow), campaign6 network storage removed,
exportfs -u+rm -rfon 180 (exportfs back to felhom-data-only baseline), all scratch.fab+ password-bearing helpers + the injected 4.17 GB removed, samplers stopped. Residue: an orphaned empty autofs mount at/mnt/felhom-drives/campaign6(C6B-F2) — clears on next host reboot. Default drive = felhom-usb (left set). Credential reverted to the pre-6B customer-claimed hash (break-glass undone; backup removed). - drill (192.168.0.152/9201): controller 0.129.0, agent 0.88.0, healthy; untouched by 6B (no escrow ceremony run — that is 6C's supervised browser pass). Escrow
escrowed. - Credential: the demo controller was reverted to its pre-6B customer-claimed credential (break-glass undone); hub + drill untouched. Viktor rotates the customer-claimed credential as planned. No R/blob produced. Drill R stays fresh/uncommitted.
Morning recovery / handoff to 6C
- Nothing is down.
CAMPAIGN-6C-2026-07-14-PROMPT.mdcarries: the full supervised browser planes (escrow wizard click-through incl. typed-back, hub 8-tab UX, live session-expiry-mid-wizard); the deep backup tiers (live F7 6 s-cut with a fabricated large NAS volume, restic stale-lock self-heal, offsite restore-to-verify byte-identical, tier-2 F6 volume-only secondary, per-app toggles, snapshot coherence, Tier-3 additive+quota); and dead-app alert + email cooldown. C6B-F1 is the priority fix for project Claude to spec. - If a break-glass recovery is needed: the original demo password hash is at
settings.json.c6b-bakin thefelhom-controller-datavolume.
Evidence index (180:~/campaign6/)
seed-6B.txt(56975d47d2e537cd),ledger.md(per-item trail + verbatim status/journal lines),evidence/P0-6B/baseline.txt.evidence/phase2/C6B-F1-hollow-bundle-manifest.json(the 2308-byte bundle's manifest),evidence/phase2/manifest-source.txt(the real 7-file 4.17 GB tree that was dropped).- P7 series (samplers):
192.168.0.162+192.168.0.152c4-samplers;180hub-sampler.