Files
felhom-controller/REPORT.md
T

69 lines
5.3 KiB
Markdown

# 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.