Files
felhom-controller/REPORT.md
T

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-offagentapi.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 gateManager.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 injectPOST /backup/offbox/confirm-escrow → escrowed (operator, after the ceremony); POST /backup/offbox/inject-passwordManager.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 recipeDRRecipeAppHalf.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_CoordForDROffboxCoord 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.