# REPORT — controller v0.105.0: fork-4 offsite password custody (hand-off + atomicity + DR inject + coord) **Date:** 2026-07-09 · **Class:** implementation (controller). **Baseline:** `main` @ `bde43f3` (v0.104.0) → `0b09a79` (v0.105.0). Pairs with agent v0.77.0. Design validated by the custody spike (`febdc56`). ## What shipped - **Hand-off** — `agentapi.Client.StageEscrowSecret` pushes the repo password to the agent's `POST /escrow/stage-secret` (authenticated pinned channel, value never logged). Enable flow (`offboxConfigHandler`) reads the 0600 password via `Manager.PushOffboxPasswordForEscrow` (the handler never sees the value) and sets `EscrowState="pending"`; a stage-push failure is surfaced, not marked escrowed. - **Atomicity gate** — `Manager.OffboxRunnable()`/`offboxEscrowed()`; `RunOffboxBackup` (→ the daily scheduler and the run handler) **refuses to run until `EscrowState=="escrowed"`** — no un-recoverable offsite ciphertext can exist. `OffboxConfigured()` unchanged (config/UI still work). New additive `settings.OffboxTarget.EscrowState` (`""|"pending"|"escrowed"`, preserved across config edits). - **Confirm + DR inject** — `POST /backup/offbox/confirm-escrow` → escrowed (operator, after the ceremony); `POST /backup/offbox/inject-password` → `Manager.InjectOffboxPassword` pre-places a recovered 64-hex password 0600 (tmp+rename), refusing to clobber without `force`; a subsequent `WriteOffboxSecrets` uses it. `/backups` shows a pending-escrow notice + "Letét megerősítése" button. - **DR recipe** — `DRRecipeAppHalf.OffsiteRestic *DRResticCoord {host,user,port,repo_path}` (coordinates ONLY — password escrowed, SFTP key regenerable), populated from `Manager.OffboxCoord()`. ## Files changed (commit `0b09a79`) `internal/agentapi/client.go`, `internal/settings/settings.go`, `internal/backup/offbox.go`, `internal/backup/offbox_test.go`, `internal/web/offbox_handlers.go`, `internal/web/offbox_escrow_test.go` (new), `internal/web/server.go`, `internal/web/templates/backups.html`, `internal/report/dr_recipe.go`, `internal/report/dr_recipe_test.go`, `internal/report/builder.go`, `CHANGELOG.md`, `CONTEXT.md`, `controller/README.md`. ## Tests + companion red-proofs Green gate `go build ./... && go vet ./... && go test ./...` — **ALL-GREEN**. UI gates (`template_id_gate`, `emoji_gate`) pass. Offbox suite extended (harness defaults to `EscrowState="escrowed"` so behavioral run tests exercise the run path). New: - `TestOffbox_PendingEscrowBlocksRun` (A) — a toggled app with a present unit is NOT backed up while pending (0 `restic backup` calls); `OffboxConfigured` true, `OffboxRunnable` false. **Companion red-proof (run + reverted):** disabling the gate (`if false && …`) → the run backed up while pending → the test FAILED (`NO offsite backup may run while escrow is pending, got […/backups/primary/app1]`). Proves the atomicity gate is load-bearing. - `TestOffbox_ConfirmEscrowEnablesRun` (B) — pending blocks; after `EscrowState="escrowed"` the run proceeds (1 backup). - `TestOffbox_InjectPasswordPrePlaced` (C) — an injected recovered password survives `WriteOffboxSecrets`; refuse-clobber without force; force overwrites; invalid rejected. **Companion `TestOffbox_NoInjectGeneratesDifferentPassword`:** without inject, `WriteOffboxSecrets` generates a DIFFERENT 64-hex password (which could never open the existing repo) — proves inject is load-bearing. - `TestOffbox_CoordForDR` — `OffboxCoord` returns the coords; ok=false when unconfigured. - `report.TestDRResticCoord_NoSecrets` (E) — the serialized `OffsiteRestic` coord clears the secret-name regex and emits host/user/port/repo_path with no password/key. - web: `TestOffboxWeb_RunGatedUntilConfirm` (run refused with the escrow-wait flash while pending; confirm → escrowed + runnable), `TestOffboxWeb_InjectPassword` (endpoint pre-places 0600; invalid → error flash). ## Deploy verification (guest 9201) Built + pushed `gitea.dooplex.hu/admin/felhom-controller:0.105.0` (digest `sha256:413d46a9…c0`), deployed via the bootstrap mechanism. `docker ps`: `:0.105.0 Up (healthy)`. Startup clean (settings loaded, scheduler registered, logscanner errors=0). **Atomicity gate live:** the standing demo offbox has `escrow_state` unset → it is now correctly **gated** (`/backups` renders the "…letétbe helyezésére vár" notice + "Letét megerősítése" button; scheduled/manual runs skip). Per the STOP boundary, NO ceremony/confirm/gated run was performed on the demo. ## NOT yet live-validated The supervised escrow ceremony end-to-end (enable → stage → `--selftest=escrow-create` with a real R → confirm → gated run against the box) is the **operator-run** follow-up — pending. Unit tests are this task's proof. ## Observations (noticed, not acted on) - The standing demo offbox (configured pre-fork-4) is now gated `pending` — this is the intended fail-safe, but it means the demo's daily offsite run will skip until an operator runs the ceremony + confirm. Flagged for the supervised follow-up. - `confirm-escrow` currently trusts the operator (pilot). The provisioning task should replace it with a hub-verified auto-confirm (the operator-forgets/operator-lies footgun) — noted in code + CONTEXT. - Pre-existing Tier-2 `TestBackupTier2Restore_DoubleClickRefused` flake (documented in v0.104.0 REPORT) is unchanged/untouched.