hub v0.13.0: DR recipe — assemble + store + view the secret-free reconstruction recipe

DR recipe slice (hub half), grounded in SPIKE-dr-recipe-2026-06-16. The hub
receives two additive dr_recipe halves on the existing report paths (agent
storage/guest/PBS on host-report; controller customer/apps on the controller
report), stores them PLAINTEXT in a DEDICATED dr_recipe table keyed by customer
(each half preserves the other), and AssembleDRRecipe stitches them into one
operator-readable recipe (ignore-unknown + version-skew tolerant).

View: a DR-recipe panel on the customer page + GET /customers/{id}/dr-recipe.json
download (operator-auth, no secrets to redact). Plaintext-at-rest is correct —
the recipe is the clean inverse of the retired infra-backup.

Tests: store round-trip (each half preserves the other), assemble-matches-golden,
ignore-unknown + version skew, partial halves, no-secrets sweep. Manifest tag
bumped to v0.13.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 18:49:45 +02:00
parent 228dac4c06
commit 5f5e3c54a1
13 changed files with 597 additions and 69 deletions
+30
View File
@@ -1,5 +1,35 @@
# Felhom Hub — Changelog
## v0.13.0 — DR recipe: assemble + store + view the secret-free reconstruction recipe (2026-06-16)
**DR recipe slice (hub half)** — the assemble-store-view side of the secret-free reconstruction recipe
(`documentation/audits/SPIKE-dr-recipe-2026-06-16.md`). The hub receives two additive halves via the
existing report paths — the agent's storage/guest/PBS half (on the host-report) and the controller's
customer/apps half (on the controller report) — and assembles them into one operator-readable recipe per
customer. This is the clean inverse of the retired infra-backup: same "re-provision plan" goal, but
PLAINTEXT-at-rest is *correct* because the recipe has zero secrets.
- **Store** (`internal/store/dr_recipe.go`): a DEDICATED `dr_recipe` table (NOT `host_escrow`, NOT the
dropped `infra_backup*` tables) keyed by `customer_id`, holding `host_half_json` + `app_half_json` +
`recipe_version` + `host_id`. `SaveDRRecipeHostHalf` / `SaveDRRecipeAppHalf` each upsert their half and
PRESERVE the other (last-write-wins per half). `AssembleDRRecipe` stitches the two into an
`AssembledRecipe{recipe_version, customer, guests, pbs, drives, pve_storage, apps}` — sub-sections pass
through as `json.RawMessage` (verbatim), **ignore-unknown** at the top level and version-skew tolerant
(`recipe_version` = max of the two halves) for forward-compat across the three repos.
- **Ingest** (`internal/api/handler.go`): `handleHostReport` persists the `dr_recipe` host-half (keyed by
the host's customer); `handleReport` persists the `dr_recipe` app-half (keyed by `customer_id`) —
mirroring the `app_telemetry` pattern, backward-compatible (old agents/controllers omit the field), and
never fatal to the heartbeat.
- **View** (`internal/web/dr_recipe.go` + customer page): a DR-recipe panel on the customer detail page
(which half has landed + last-updated) with a **Download recipe (JSON)** link →
`GET /customers/{id}/dr-recipe.json` serves the assembled recipe (operator dashboard-auth, pretty JSON,
`Content-Disposition` attachment). No decrypt, nothing to redact.
- Tests: `TestDRRecipe_StoreRoundTrip` (each half preserves the other), `TestAssembleDRRecipe_MatchesGolden`
(the assembled wire shape pinned in `testdata/dr-recipe.golden.json`),
`TestAssembleDRRecipe_IgnoreUnknownAndVersionSkew` (a forward-compat half still assembles; version = max),
`TestAssembleDRRecipe_PartialHalves` (one half present), `TestAssembleDRRecipe_NoSecrets` (defense-in-depth
credential-key sweep). Pairs with felhom-agent v0.38.0 + felhom-controller v0.73.0.
## v0.12.0 — retire Infra Backup + purge its plaintext secrets + fix the daily backup-deadline email (2026-06-16)
Phase-1 of the Infra Backup retirement (per `documentation/audits/SPIKE-infra-backup-2026-06-15.md`).