| 1 | # Instance key provision (L21): plan report |
| 2 | |
| 3 | **Status:** the plan is written and committed. Nothing is pushed. |
| 4 | - **Branch:** `instance-key-provision`, worktree `/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-key-provision` |
| 5 | - **Commit:** 151c4d94 |
| 6 | - **Plan:** `docs/superpowers/plans/2026-10-10-instance-key-provision.md` |
| 7 | - **Tasks:** 5, then a final verification step. |
| 8 | |
| 9 | **How the code was checked.** Every code block in the plan was prototyped in a scratch worktree of 0d338c14 and passed these checks: |
| 10 | - `go vet` on linux and darwin, for activator, host, moves, moves/archive and appconfig; |
| 11 | - `go build` for windows and darwin, for activator and host; |
| 12 | - `gofmt` clean. |
| 13 | |
| 14 | The tests were **not run**, because the shared-machine rules allowed only build and vet. The scratch worktree is deleted. |
| 15 | |
| 16 | ## Rulings |
| 17 | |
| 18 | 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. |
| 19 | 2. **Format.** `base64.StdEncoding` of 32 `crypto/rand` bytes: 44 characters, padded. Every reader yields it verbatim. |
| 20 | 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. |
| 21 | 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. |
| 22 | 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. |
| 23 | 6. **Durability before the first start.** |
| 24 | - 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. |
| 25 | - 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. |
| 26 | - 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. |
| 27 | 7. **Backfill.** |
| 28 | - **Who:** the agent, at startup, on legacy boxes only. It runs after layout recovery and before the layout pass and `svc.Start`. |
| 29 | - **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. |
| 30 | - **Idempotent:** it re-runs on every start. |
| 31 | - **Verifiable:** it logs one count line, which the rollout reads. |
| 32 | - Claim boxes mint under the guard at each cold admission instead. |
| 33 | - The "forced publish" is replaced by ruling 6: no process ever holds a key its generation's manifest lacks. |
| 34 | 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. |
| 35 | 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. |
| 36 | 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). |
| 37 | 11. **No switch.** An env var nobody reads is harmless. The claim launch identity changes once per host. Rollback is reverting the binary. |
| 38 | 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. |
| 39 | |
| 40 | ## Obligations on other plans |
| 41 | |
| 42 | - **O1, the C8 provider:** `Establish` publishes `<host>.env`'s exact bytes before returning. An error fails the wake. |
| 43 | - **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`. |
| 44 | - **O3, stage B Task 8c:** add a test that relocation install keeps the published key. |
| 45 | - **O4, stage B Task 10 item 9:** satisfied by this plan's rollout plus O1 and O2. |
| 46 | |
| 47 | ## Open questions for Paul |
| 48 | |
| 49 | 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. |
| 50 | 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. |
| 51 | 3. **`report.token`.** The L21 candidate contract included it, but the decision covers the key only. Should the platform provision it too? |
| 52 | 4. **Workers and sidecars.** Should they get their instance's key? Today they get none, and run as if off CARLOS. |
| 53 | 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? |
| 54 | |
| 55 | ## Concerns |
| 56 | |
| 57 | - **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. |
| 58 | - **O2 is a real hazard.** Without it, adopting the helper and enabling region HA together leaves every adopted app with a nonempty database stuck. |
| 59 | - **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. |
| 60 | - **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. |
| 61 | - **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. |
| 62 | - **Line numbers for stage B.** Citations into stage B code refer to `region-ha-1b-next-build` at bb955916, not main. |
| 63 | |