Files
felhom-agent/REPORT.md
T
admin 8abc1b8852 agent v0.38.0: DR recipe — emit secret-free storage/guest/PBS half in host-report
DR recipe slice (agent half), grounded in SPIKE-dr-recipe-2026-06-16. Additive
`dr_recipe` host-report section = the non-secret reconstruction scaffolding the
operator must rebuild before PBS bytes can land. Built by pure
BuildDRRecipeHostHalf from facts the report already collects (no new reads):
guests[] sizing, drives[] (user-data by durable_id/role/mount/intent),
pve_storage[] (storage.cfg), pbs coordinates.

BOUNDARY (Phase-1 lesson): every field is an identifier/intent/size/coordinate —
never a key/password/token/hash/ENC:. PBS key stays in escrow; restic password
stays in escrow; the recipe names only the coordinates the restore targets.

Tests: BuildDRRecipeHostHalf selection, NoPBS, NoSecrets (boundary mirror),
dr_recipe key-set in the cross-repo golden contract test. recipe_version=1,
ignore-unknown on read.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 18:32:15 +02:00

3.2 KiB

REPORT — felhom-agent v0.38.0 (DR recipe: agent storage/guest/PBS half)

TASK — DR recipe slice (agent half). Emit the secret-free reconstruction-recipe scaffolding the agent already owns, as an additive host-report section. Grounded in SPIKE-dr-recipe-2026-06-16.md.

What

The recipe is the non-secret re-provision plan that complements escrow (keys) and PBS/restic (bytes): guest sizing, drive inventory (durable-id → role → mount → intent), PVE storage defs, and PBS coordinates — the host/guest/storage scaffolding the operator must rebuild on new hardware before the PBS bytes can land. The agent owns every fact (StorageTarget, GuestSpec, PBSSnapshot), so it emits its half additively in the existing host-report; the hub assembles it with the controller's app half.

Implementation

  • internal/hub/dr_recipe.go — wire types + the pure BuildDRRecipeHostHalf(guests, targets, pbs):
    • guests[] ← each Guest.Spec (cores/memory/disk), skipping status-unknown guests.
    • drives[] ← user-data external drives only (isUserDataDrive: usb/local-dir with a non-empty durable-id + mount path) → {durable_id, role, mount_path, intent=enrolled, total_bytes}.
    • pve_storage[]every storage target {name, type, content} (rebuild storage.cfg).
    • pbs ← the latest snapshot's {repo_id (the pbs storage id), namespace, latest_snapshot_id}.
  • HostReport.DRRecipe *DRRecipeHostHalf json:"dr_recipe" (always set in Collect(), never null).
  • No new reads — derived from the just-collected report facts.

Boundary (non-negotiable)

Every field is an identifier / intent / size / coordinate. The PBS key stays in escrow, the access token in identity-escrow, the restic password in escrow — the recipe names only the coordinates the restore targets. This is the exact axis the retired infra-backup violated (it shipped encryption_key_b64/restic_password/cf_api_token).

Tests (non-hollow)

  • TestBuildDRRecipeHostHalf — drives = only user-data; pve_storage = all targets; pbs = latest snapshot; guests skip nil-spec.
  • TestBuildDRRecipeHostHalf_NoPBS — no snapshots → pbs omitted, slices non-nil.
  • TestDRRecipeHostHalf_NoSecrets — the boundary mirror: serialized half has NO key matching (?i)(password|secret|token|hash|passphrase|api[_-]?key|\bkey\b|enc:). (The load-bearing boundary test, with a synthetic-secret app + allowlist red-proof, lives on the controller emitter.)
  • dr_recipe key-set + sub-array element key-sets added to TestHostReport_ContractMatchesGolden (the cross-repo golden, byte-pinned with the hub's copy).

Versioning

recipe_version=1, carried in the section. Read is ignore-unknown (encoding/json default) for forward-compat, mirroring storage_manifest. Golden discipline: host-report.golden.json here must stay byte-identical to the hub's copy — manual checksum-diff on any wire change (the golden spans three repos).

Gate

go build, go vet, go test ./... all green (local + build server). Deployed to felhom-pve.

Deferred (NOT in this slice)

The agent-side restore_directive consumption / recovery-mode execution (syncer.go:92) — slice-10D. This slice only EMITS the recipe.