# 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.
