# REPORT — controller v0.73.0: DR recipe (customer + apps half) **TASK — DR recipe slice (controller half).** Emit the secret-free customer + apps half of the reconstruction recipe as an additive hub-report section. Grounded in `SPIKE-dr-recipe-2026-06-16.md`. **The controller emitter is the boundary enforcement point** — it is the component that distinguishes secret from non-secret deploy fields, so v1 ships only an allowlist and the no-secrets boundary test lives here. ## Implementation - `internal/report/dr_recipe.go`: - `DRRecipeAppHalf{recipe_version, customer{id,display,domain}, apps[]}`. - `BuildDRRecipeAppHalf(custID, custName, domain, stacks, composeReader)` — pure given the reader; one `AppRecipe` per DEPLOYED, non-protected stack. - `AppRecipe{catalog_ref, enabled, storage_bindings}` — the **entire v1 surface**. `buildAppRecipe` reads NOTHING from `AppConfig.Env`. - `appStorageBindings(composeYAML, hddPath)` — pure compose parser; each `${HDD_PATH}`/`${USERDATA_PATH}` volume bind → `{container_path, drive=basename(HDD_PATH), subpath}`; named volumes excluded. - `readComposeFile` — the production reader (best-effort; unreadable → no bindings, never a failure). - `Report.DRRecipe *DRRecipeAppHalf` wired into `BuildReport` (customer fields + `GetStacks()`). ## The boundary (the Phase-1 lesson) The recipe carries ONLY `{catalog_ref, enabled, storage_bindings}` — identifiers/paths. It NEVER touches `AppConfig.Env`, where the controller keeps `ENC:` secrets. This is an **allowlist** (a new field is excluded by default), the inverse of the retired infra-backup that shipped `restic_password` / `cf_api_token`. Secrets stay in the PBS whole-CT snapshot + escrow, recovered with R, never here. ## Tests (the load-bearing boundary test + companion) - `TestBuildAppRecipe_NoSecrets` — emit a recipe for an app whose `Env` carries an `ENC:` value + a token-shaped value; assert NONE of the values and NO credential-shaped key survive; assert the allowlisted facts DID emit (non-vacuous). - `TestBuildAppRecipe_AllowlistIsLoadBearing` — the companion/red-proof: a guard-removed shape leaks the token; the production emitter does not. - `TestAppStorageBindings` (+ `_NoHDD`) — pins the compose parse (roms + resources bindings; named volume excluded; rootfs app → 0 bindings). - `TestBuildDRRecipeAppHalf` — assemble-correctness (deployed + non-protected only) + whole-half secret sweep. - **Live red-proof:** forcing `buildAppRecipe` to dump `Env` made `TestBuildAppRecipe_NoSecrets` and the companion FAIL (caught the token value + the `DB_PASSWORD`/`SECRET_KEY`/`IGDB_CLIENT_SECRET` keys); reverted → green. ## Versioning `recipe_version=1`; read is ignore-unknown for forward-compat. The hub assembles this half with the agent's storage/guest/PBS half (agent v0.38.0) into one customer recipe. ## Gate / deploy `go build`, `go vet`, `go test ./...` all green (local + build server). Built + pushed image v0.73.0; deployed to guest 9201 (bootstrap-managed). ## Deferred (NOT in this slice) Free-form non-secret deploy fields (the SPIKE's `non_secret_deploy_fields`) — v1 is the three-field allowlist only; additional fields land incrementally behind the same allowlist test. No re-provisioning automation; no recovery-mode consumption.