Files
felhom-controller/REPORT.md
T
2026-08-22 13:25:36 +02:00

6.2 KiB
Raw Blame History

REPORT — R-356: the off-site restore refused every app that has no data drive

Controller v0.219.0, shipped and proven live on demo-hp 2026-08-22.

Baselines used (re-verified at session start)

repo main @ version →
felhom-controller 2da259af38cc v0.218.0 v0.219.0
felhom-agent 40d857b52711 v0.130.0 unchanged
felhom.eu (hub) f2edf7e54595 v0.106.0 unchanged
app-catalog 459766cb1639 — unchanged

MinAgent stays 0.129.0. All four hashes matched the prompt.

The catalogue count, measured in this session and not taken from the prompt: 53 templates, 13 declare needs_hdd: true, 40 declare false, and exactly 13 carry a backup: block. Agrees with §1.

The defect

ReconstituteFromOffsite and PlaceOffsiteRestore both resolved the restore destination with the raw stackProvider.GetStackHDDPath(stack) and refused when it was empty, with a message saying the app is not installed. For the 40 driveless apps that value is never set, so both refused permanently for a running, healthy app — and the remedy they offered ("reinstall it in the same place") cannot be carried out, because those apps offer no place to pick.

The capture side never had this: CaptureRecoveryUnit resolves via GetAppDrivePath, which falls back to systemDataPath. That is why the 2026-08-21 refusal could print /mnt/sys_drive — a destination the backup had recorded and the restore refused to use.

The change

  • internal/backup/backup.go — new Manager.isStackDeployed, built on knownStackNames() → ListDeployedStacks(). Nil provider ⇒ false, documented with its reason: the caller's next act is a write, so "cannot tell" must fail into the recoverable refusal, not into a copy.
  • internal/backup/offbox_reconstitute.go — the refusal now asks installed? first (R-351 sentence and the recorded-drive half intact), then resolves the destination with GetAppDrivePath, then has a third, distinct refusal for installed-but-no-resolvable-data-root. R-253 and R-351 comments kept and extended with what the refusal stopped covering and why.
  • internal/backup/offbox_restore.go — the same four steps in PlaceOffsiteRestore. The headroom gate below it then measures the resolved namespace, which is the correct disk in both cases.

Not changed, deliberately: offboxCaptureSet's raw GetStackHDDPath (offbox_capture.go:43) and offboxRestoreScratchDir.

ListDeployedStacks() semantics were verified, not assumed. Source: stackAdapter skips !s.Deployed (cmd/controller/main.go:2138). Test: positive and negative controls in TestR356_IsStackDeployedIsExactAndFailsClosed. Seam: an AST walk in cmd/controller/r356_deployed_seam_test.go pins both the SetStackProvider wiring in main() and the !s.Deployed guard itself.

Tests

New: internal/backup/r356_hot_only_restore_test.go (10 tests, Scenarios A–E across both entry points) and cmd/controller/r356_deployed_seam_test.go (2 AST tests). Test count in internal/backup 315 → 325, cmd/controller 42 → 44.

Every red-proof was SEEN failing. Mutation → observed failure:

# mutation applied observed
A destination back to stackProvider.GetStackHDDPath ScenarioA failed: "a(z) immich telepítve van, de a vezérlő nem tudja megállapítani, hová tartoznak az adatai…"
B GetAppDrivePath always returns systemDataPath 3 tests failed, incl. "placement …/sys/felhom-data/… left the app's own drive" — this is also the positive control for the "13-class unchanged" absence claim
C isStackDeployed always true 3 tests failed; the not-installed refusal was replaced by a placement-mismatch prompt
D widen nincs telepítve to cover the no-data-root case both ScenarioD tests failed: "a running app must not be told it is not installed"
E revert PlaceOffsiteRestore only (fix one entry point) ScenarioE_PlaceDriveless failed

No red-proof passed. Green gate: go build ./... && go vet ./... && go test ./... — all green.

Fixture correction, stated rather than hidden: several existing fixtures marked an app "installed" by giving it an HDD path — the exact conflation this change removes. 17 tests failed on that alone after the change; they now state deployment as its own fact (prov.deployed). No assertion was weakened, and TestPlace_UndeployedRefused was passing for the wrong reason before.

Live walk on demo-hp (controller v0.219.0, healthy)

Endpoint-level — claude-in-chrome is not available on DooPlex. Full record and every artefact: felhom.eu/documentation/audits/DRILL-r356-hot-only-restore-2026-08-22/.

Driveless (privatebin): planted through the app's own JSON API + a direct file plant with a Hungarian accented name; findability proved by plant→find→remove→fail-to-find; off-sited; 11 files deleted outright; scratch prepared (snapshot 306accff, unit-only — the shape never driven to completion before); POST /backup/offbox/reconstitute — no refusal; 15/15 files back byte for byte. Message (160 bytes): „A(z) privatebin: 0 fájl és 1 adatkötet visszaállítva (mentés: 2026-08-22 13:14) — az alkalmazás újraindult. Ennek az alkalmazásnak nincs adatbázisa." It names the volume, so it is not the R-354 shape.

Drive app (calibre-web): same walk, 3/3 byte-identical, placed on /mnt/felhom-drives/hdd_1; /mnt/sys_drive/felhom-data/userdata/media does not exist, so nothing was misplaced onto the SSD.

Every downstream leg held on first run. No finding raised against them.

Also in this session

CI run 387 (job 386) failed on instructions_gate — a real gate bug, not mine: ef6ac6f in felhom.eu compressed closed rows into CLOSED-ITEMS.md, which the gate does not read, so every citation of a compressed item became "a reference to nothing". Fixed in felhom.eu/scripts/instructions_gate.py (commit e18668f); both controls still convict.

Operator follow-up

  1. Bake and vouch a golden carrying controller 0.219.0. The golden-currency gate is red until then, correctly: a machine installed now still receives 0.218.0.
  2. Then raise the update floor to 0.219.0 — last, in a separate save.