DR recipe slice (controller half), grounded in SPIKE-dr-recipe-2026-06-16. The
controller emitter is the BOUNDARY enforcement point: v1 ships an explicit
allowlist {catalog_ref, enabled, storage_bindings} and reads NOTHING from
AppConfig.Env, so no ENC:/token/password can leak. storage_bindings parsed from
the compose (${HDD_PATH}/${USERDATA_PATH} volume binds -> {container_path,
drive, subpath}).
Load-bearing tests: TestBuildAppRecipe_NoSecrets (synthetic-secret app -> none
leak) + TestBuildAppRecipe_AllowlistIsLoadBearing (red-proof companion) +
TestAppStorageBindings + TestBuildDRRecipeAppHalf. Red-proofed live: forcing the
emitter to dump Env makes the boundary test fail. recipe_version=1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3.3 KiB
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; oneAppRecipeper DEPLOYED, non-protected stack.AppRecipe{catalog_ref, enabled, storage_bindings}— the entire v1 surface.buildAppRecipereads NOTHING fromAppConfig.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 *DRRecipeAppHalfwired intoBuildReport(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 whoseEnvcarries anENC: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
buildAppRecipeto dumpEnvmadeTestBuildAppRecipe_NoSecretsand the companion FAIL (caught the token value + theDB_PASSWORD/SECRET_KEY/IGDB_CLIENT_SECRETkeys); 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.