6.2 KiB
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— newManager.isStackDeployed, built onknownStackNames()→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 withGetAppDrivePath, 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 inPlaceOffsiteRestore. 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
- Bake and vouch a golden carrying controller 0.219.0. The
golden-currencygate is red until then, correctly: a machine installed now still receives 0.218.0. - Then raise the update floor to 0.219.0 — last, in a separate save.