Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
5.3 KiB
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.StageEscrowSecretpushes the repo password to the agent'sPOST /escrow/stage-secret(authenticated pinned channel, value never logged). Enable flow (offboxConfigHandler) reads the 0600 password viaManager.PushOffboxPasswordForEscrow(the handler never sees the value) and setsEscrowState="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 untilEscrowState=="escrowed"— no un-recoverable offsite ciphertext can exist.OffboxConfigured()unchanged (config/UI still work). New additivesettings.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.InjectOffboxPasswordpre-places a recovered 64-hex password 0600 (tmp+rename), refusing to clobber withoutforce; a subsequentWriteOffboxSecretsuses it./backupsshows 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 fromManager.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 (0restic backupcalls);OffboxConfiguredtrue,OffboxRunnablefalse. 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; afterEscrowState="escrowed"the run proceeds (1 backup).TestOffbox_InjectPasswordPrePlaced(C) — an injected recovered password survivesWriteOffboxSecrets; refuse-clobber without force; force overwrites; invalid rejected. CompanionTestOffbox_NoInjectGeneratesDifferentPassword: without inject,WriteOffboxSecretsgenerates a DIFFERENT 64-hex password (which could never open the existing repo) — proves inject is load-bearing.TestOffbox_CoordForDR—OffboxCoordreturns the coords; ok=false when unconfigured.report.TestDRResticCoord_NoSecrets(E) — the serializedOffsiteResticcoord 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-escrowcurrently 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_DoubleClickRefusedflake (documented in v0.104.0 REPORT) is unchanged/untouched.