Instance key provision: Astra round 1 brief + planner's report
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+88
−0
docs/superpowers/plans/CODEX-PLAN-INSTANCE-KEY-BRIEF-1.md+26 −0docs/superpowers/plans/INSTANCE-KEY-PLAN-REPORT-R0.md+62 −0
CI failed — run details
diff --git a/docs/superpowers/plans/CODEX-PLAN-INSTANCE-KEY-BRIEF-1.md b/docs/superpowers/plans/CODEX-PLAN-INSTANCE-KEY-BRIEF-1.md| new file mode 100644 |
| index 000000000..815ac97f1 |
| --- /dev/null |
| +++ b/docs/superpowers/plans/CODEX-PLAN-INSTANCE-KEY-BRIEF-1.md |
| @@ -0,0 +1,26 @@ |
| +# Astra round 1 brief: instance key provision (L21), plan rev 1 |
| + |
| +**Review:** `docs/superpowers/plans/2026-10-10-instance-key-provision.md` on branch `instance-key-provision`, cut from main at 0d338c14. |
| + |
| +**What it decides.** Paul's decision of 2026-10-09: the platform creates each instance's key before the app's first start. The plan has the agent mint `CARLOS_INSTANCE_KEY` (32 CSPRNG bytes, base64) create-only into `<host>.env`, the file that already carries `CARLOS_ADMIN_TOKEN`. The framework contract is rastrillo's `carlos.InstanceKey`, on rastrillo main e3ea08d. Its order is: |
| +1. the app's own key wins; |
| +2. otherwise `$CARLOS_INSTANCE_KEY`, verbatim; |
| +3. on CARLOS without a key, the app refuses to start; |
| +4. off CARLOS, nothing changes. |
| + |
| +Rollout: the key is written on every box, existing instances included, before any app adopts the helper. |
| + |
| +**Why it matters.** Region HA (docs/superpowers/specs/2026-10-04-region-ha-box-placement-design.md, H6 and §5.6) restores a dead box's instance from the replicated database and the files backup. A key minted after start and never backed up leaves sealed data unrecoverable. |
| + |
| +**Context.** The planner's report is in `docs/superpowers/plans/INSTANCE-KEY-PLAN-REPORT-R0.md`. Its main concern: nothing on main backs up `<host>.env` yet. Durability before the first start depends on the claim C8 files provider, which moves is building, and that obligation is recorded as O1. |
| + |
| +**Judge:** |
| +1. **Create-only.** Is it airtight on every path, given the plan's ruling-8 table? The paths: create, backfill, retries, re-render, hibernation restore, move, relocation install and layout move. Can any path overwrite or rotate a key, or leave two processes of one instance holding different keys? |
| +2. **Durability before first start.** Is the argument sound on legacy boxes and on claim boxes? Is O1 the right obligation, and is it enough? |
| +3. **The backfill.** Is it safe, idempotent and complete? Is anything missed: hibernated instances, landing paths, nested layouts? |
| +4. **Secrecy.** Logs, errors, file modes, the console, CLI listings, move reports. |
| +5. **Concurrency.** Minting under `adminTokenMu` across `seedChild`, `spawnChildFrom`, nested `prepareAt` and `EnsureClaimAdminToken`. |
| +6. **Tests.** Does the code compile against main? Are the tests real, so that each fails without its guard? |
| +7. **The planner's five open questions.** Replicas, a key set in configuration, `report.token`, workers and sidecars, and restarts inside a claim session. Say which ones block the build. |
| + |
| +Give findings P1/P2/P3 with file:line. End with a verdict: ready to build, or not. |
diff --git a/docs/superpowers/plans/INSTANCE-KEY-PLAN-REPORT-R0.md b/docs/superpowers/plans/INSTANCE-KEY-PLAN-REPORT-R0.md| new file mode 100644 |
| index 000000000..757e57a4a |
| --- /dev/null |
| +++ b/docs/superpowers/plans/INSTANCE-KEY-PLAN-REPORT-R0.md |
| @@ -0,0 +1,62 @@ |
| +# Instance key provision (L21): plan report |
| + |
| +**Status:** the plan is written and committed. Nothing is pushed. |
| +- **Branch:** `instance-key-provision`, worktree `/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-key-provision` |
| +- **Commit:** 151c4d94 |
| +- **Plan:** `docs/superpowers/plans/2026-10-10-instance-key-provision.md` |
| +- **Tasks:** 5, then a final verification step. |
| + |
| +**How the code was checked.** Every code block in the plan was prototyped in a scratch worktree of 0d338c14 and passed these checks: |
| +- `go vet` on linux and darwin, for activator, host, moves, moves/archive and appconfig; |
| +- `go build` for windows and darwin, for activator and host; |
| +- `gofmt` clean. |
| + |
| +The tests were **not run**, because the shared-machine rules allowed only build and vet. The scratch worktree is deleted. |
| + |
| +## Rulings |
| + |
| +1. **Where the key lives.** It goes in `<host>.env`, minted on the box. This is the "overlay" in `mergeEnv`'s sense: the file that already holds `CARLOS_ADMIN_TOKEN`. It is not appconfig's instance overlay (`ApplyInstance`, rendered to `<host>.config.env`). Minting in the console is rejected: that layer is app-editable, and it sits *under* `<host>.env` in `mergeEnv`. Stage B's L21 section pointed at the wrong layer. |
| +2. **Format.** `base64.StdEncoding` of 32 `crypto/rand` bytes: 44 characters, padded. Every reader yields it verbatim. |
| +3. **Who mints.** Every launch path mints the key right after the admin token, under `adminTokenMu`: `seedChild`, `spawnChildFrom`, nested `prepareAt`, and the claim's `EnsureClaimAdminToken`. The scheduler tick does not. Creation needs no new step: no instance process starts before `doWake`'s seed or start. |
| +4. **Create-only.** A key of any shape is never rewritten. An empty value counts as absent. The key is appended, the file is `fchmod`ed to 0600, and the file and its directory are fsynced. |
| +5. **Configuration wins.** If the rendered `<host>.config.env` already supplies `CARLOS_INSTANCE_KEY`, the platform mints none. Minting would change the app's effective key. |
| +6. **Durability before the first start.** |
| + - Legacy boxes have no publisher on main, and region HA can't run there. The key's durability is the admin token's: disk plus the move archive. |
| + - On claim boxes, the launch source mints in `Select`, and `Materialize` calls C8's `Establish` before the app executes. C8 is bound (obligation O1) to publish the env bytes before returning. |
| + - If the bucket is unreachable, the wake fails and the app does not start. The key stays on disk, and the retry publishes the same bytes. |
| +7. **Backfill.** |
| + - **Who:** the agent, at startup, on legacy boxes only. It runs after layout recovery and before the layout pass and `svc.Start`. |
| + - **Which hosts:** ready activator instances only, and only where `<host>.env` already exists. It never creates the file, so a landing's empty path stays empty. |
| + - **Idempotent:** it re-runs on every start. |
| + - **Verifiable:** it logs one count line, which the rollout reads. |
| + - Claim boxes mint under the guard at each cold admission instead. |
| + - The "forced publish" is replaced by ruling 6: no process ever holds a key its generation's manifest lacks. |
| +8. **Never overwrite.** There is a table with a test per path: create, backfill, existing key, claim retry, re-render, hibernation restore, move, relocation install (obligation O3), and layout move. |
| +9. **Secrecy.** The key is never logged and never in an error. The file is 0600, and so is the launch file. The console never reads `<host>.env`, and `carlos env list` reads bucket config only. The moves env report uses HMAC only. Once published, the key sits in the bucket beside the database it seals. |
| +10. **Moves and relocation.** The key travels byte for byte in the sealed archive, and is placed only on an empty path. The env report flags a missing or different key. The moves probe never mints, because its pin would fail. Relocation install writes `EnvItem` verbatim (O3). |
| +11. **No switch.** An env var nobody reads is harmless. The claim launch identity changes once per host. Rollback is reverting the binary. |
| +12. **Scope.** Exec-backed activator instances only. Unit tenants, TCP-addr routes, sidecars and workers get no key, and `carlos.Running()` is false for them. |
| + |
| +## Obligations on other plans |
| + |
| +- **O1, the C8 provider:** `Establish` publishes `<host>.env`'s exact bytes before returning. An error fails the wake. |
| +- **O2, stage B pinner (ruling 25, `pin.go:250`):** `needKey` must also accept an `EnvItem` that carries `CARLOS_INSTANCE_KEY`. Otherwise every app that adopts the platform key goes `stuck` on every relocation, because it never writes `instance.key`. |
| +- **O3, stage B Task 8c:** add a test that relocation install keeps the published key. |
| +- **O4, stage B Task 10 item 9:** satisfied by this plan's rollout plus O1 and O2. |
| + |
| +## Open questions for Paul |
| + |
| +1. **Replicas** (#162) mirror their home but get their own key. Should a replica get its home's key? That needs a way to copy the key between boxes. |
| +2. **A key set in configuration.** Defer to it (as planned), or reserve the name? Reserving means refusing it in `carlos env set` / `secrets set` and stripping it at render, and the refusal message needs copy review. |
| +3. **`report.token`.** The L21 candidate contract included it, but the decision covers the key only. Should the platform provision it too? |
| +4. **Workers and sidecars.** Should they get their instance's key? Today they get none, and run as if off CARLOS. |
| +5. **Restarts inside a claim session.** A restart inside a running session (not a cold admission) can mint outside the C8 publish point. This only affects instances admitted before this build. Accept it, or forbid minting outside the launch source on claim boxes? |
| + |
| +## Concerns |
| + |
| +- **The "always continuous" backup of `<host>.env` does not exist on main.** `hostfiles` is imported by nothing, `EnvItem` exists only on `region-ha-1b-next-build`, and no C8 provider exists, so claim serving wakes are refused. L21's durability on claim boxes depends entirely on O1, in a plan that isn't written yet. |
| +- **O2 is a real hazard.** Without it, adopting the helper and enabling region HA together leaves every adopted app with a nonempty database stuck. |
| +- **Copying a database between instances moves data, not keys.** The seeded-instance and run specs (#259-261), and any `db push` between instances, copy a database without its key. Those specs need to say what happens to sealed data. |
| +- **The key in the data bucket.** Region HA puts the key in the same bucket as the database it seals. That follows from the decision, but is worth stating. |
| +- **Existing tests may need small updates.** Activator or host tests that compare a child's whole environment, or an env file's exact bytes, after a start may now see one more line. Task 2 says to update such assertions without weakening them, and to list them in the commit. |
| +- **Line numbers for stage B.** Citations into stage B code refer to `region-ha-1b-next-build` at bb955916, not main. |