# Instance key provision (L21): plan report, rev 3

**Status:** rev 3 of the plan is written and committed. It answers Astra's round 2 (`CODEX-PLAN-INSTANCE-KEY-R2.md`) under Paul's rulings R-A to R-F, which are the plan's rulings 19 to 24. Nothing is pushed.
- **Branch:** `instance-key-build`, worktree `/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-key-provision`
- **Plan:** `docs/superpowers/plans/2026-10-10-instance-key-provision.md`, rev 3, commit 3947e1d9
- **Tasks:** 7, then the final verification. Task 5 is now the adoption verb and its publication, and is mounted nowhere on main. Task 7 lost `ConfiguredInstanceKey`.

## Dispositions (round 2)

| # | Finding | Disposition |
|---|---|---|
| R2-1 | A configured key still bypasses the refusal (configured A, own B) | Accepted: source (c) dropped; configured keys need an adoption record too |
| R2-2 | Configuration evidence neither identifies nor pins the key | Accepted: the record names the key by digest; the pin carries it; the install verifies it |
| R2-3 | An adoption record outlives its writer | Accepted: the record binds host, incarnation, session and generation from the agent's launch state; every admission starts with none |
| R2-4 | No publication on the running session; O6 not gated | Accepted: `hostfiles.PlatformRelay` onto the session's publisher; O6 a gate in O4 |
| R2-5 | Moves leave the record behind | Accepted: O7 with the exact lists; O6 does not ship before O7 |
| note | The record is tenant-forgeable | Answered by ruling 19 (trusted-helper assertion) |
| R1-6 | The merge option | Closed: withdrawn by Astra |

**Counts:** 5 accepted, 0 declined. One note answered, one round 1 finding closed.

## What changed in the code

- `instancekey`: the record is now version 2 and writer-bound (`Binding`, `ParseAdoption`, `Binds`). New: the sources (`SourcePlatform`, `SourceConfig`, `SourceOf`) and `VerifyDelivered` for the install.
- `ClaimLaunchSnapshot` gains `InstanceKeySHA256` and `InstanceKeySource`, read from the same two files as the env; the identity is unchanged.
- The adoption verb (`MountInstanceKeyAdoption`) takes its writer, key digest and source from an `AdoptionLaunch` that O6 builds per launch. It is no longer mounted on legacy launches.
- New `hostfiles.PlatformRelay` stages a platform item on a running publisher without blocking the caller.
- O2' for stage B is now (a) or (b'), with the pinned key in `PinObject.InstanceKey`. It replaces stage B's own rev 3 `KeyEvidence` reader.

## How it was checked

- Every Go block in Tasks 1 to 7 was built in a scratch copy, then rebuilt task by task in a second copy, with `go vet` and `gofmt -l` clean after each task. Both copies ended byte-identical. The plan's blocks were then extracted back out of the document and compared with the scratch files, byte for byte.
- `go build` for windows, darwin and linux/arm64 of activator, host, hostfiles, instancekey and appconfig, and `go vet` on darwin: clean.
- **Tests run**, niced:
  - all new tests;
  - the activator `Seed`, `Start`, `Prepare`, `Control`, `Schedule`, `Claim` and `LaunchEnvironment` families, with the sandbox off for sockets;
  - the host claim and env report tests;
  - all of `instancekey`, `hostfiles`, `moves` and `moves/archive`.

  All passed. The full `go test ./internal/host/ -run Claim` also passed, in 1112 seconds. It exceeds go test's default 10-minute timeout on this shared box because of an existing 30,000-delivery test (`TestClaimCompletedProducersBoundStopAndRetry`, 1015 seconds), so the plan's sandbox note now says to pass `-timeout 30m`.
- **O2' was compiled and run against stage B** at 919662d2, in a scratch copy with `internal/instancekey` added. `go build ./internal/...` and `go vet` were clean, and `go test ./internal/hostrelocate/` passed: 17 evidence cases, plus every existing pinner test.
- Scratch copies are deleted. The shared `$TMPDIR/gocache` is untouched.

## The rastrillo addendum

The helper reports on every process start (at most once per process), and the body adds `"source": "platform" | "config"`, copied from a new agent-set `$CARLOS_INSTANCE_KEY_SOURCE`. It is in the plan's "Ask for rastrillo #66" section, ready to post.

## Top concerns

1. **The helper cannot know the source by itself.** A platform key and a configured key both arrive as `$CARLOS_INSTANCE_KEY`. The plan has the agent deliver `CARLOS_INSTANCE_KEY_SOURCE` beside it (agent-owned, part of O6) for the helper to echo, and the agent checks the echo against its own launch state. If Paul would rather not add a variable, the `source` field could be dropped from the request, because the agent already knows the source.
2. **Nothing writes a record until O6 and O7 land.** The verb is built and tested but mounted nowhere: legacy launches have no writer session to bind to, and claim launches have no control socket. Platform-key and configured-key apps therefore do not relocate until claim Plan 9 Group D (O6) and the moves change (O7) ship, in that order. That is the explicit refusal of ruling 23.
3. **A configured key's digest is published.** A `config` record puts the SHA-256 of a configured secret in the data bucket. That reveals nothing for a random key; a weak, hand-typed key could be guessed against it. It is listed as a follow-up.
4. **Stage B has its own diverging O2' in plan text.** Its rev 3 (919662d2) has the `KeyEvidence` reader, with presence-only config evidence and the `export` parser. Stage B's planner must replace that step with this plan's O2' and rewrite its ruling 55, as O2' says.
5. **One report lost to box loss inside the publisher's bound.** A box lost within that bound (a second by default) after the first adoption loses that report. The pin then refuses until a later session re-reports. That is safe, but worth knowing.

## Rev 3.1 (Astra round 3)

Round 3 found no P1. Rev 3.1 fixes the two remaining findings; both are accepted.

- **R3-1 (P2), the replica exclusion at delivery.** Ruling 25: `fileEnvFor`, the composition every launch path uses, drops `CARLOS_INSTANCE_KEY` for a replica row. That covers Start, Seed, the nested prepare, `LaunchEnvironment` and the claim snapshot. The helper's step 2 then has nothing to return, so no rastrillo change is needed. It is in the new Task 3b; Task 4's snapshot uses it and names no key for a replica.
- **R3-2 (P3), the dependency order.** The follow-ups now say O7 ships before O6.
- **Effect on stage B:** O2' and its test are unchanged, and nothing stage B consumes changes. The moves env report for a replica no longer lists the key, on the source and the target alike.

**How it was checked:**
- The plan's Go blocks were extracted from the document, rebuilt into a tree and compared byte for byte with the tested scratch tree. They matched.
- `go vet` and gofmt were clean. Builds for darwin, windows and linux/arm64 were clean.
- The new tests were run, sandbox off for sockets:
  - `TestReplicaLaunchIsHandedNoInstanceKey`: real replica Start and Seed, with a configured key, a pre-existing env key and both, plus a home-row control case;
  - `TestNestedReplicaPrepareIsHandedNoInstanceKey`: run and seed roles;
  - `TestClaimReplicaLaunchIsHandedNoInstanceKey`.

  They pass with the activator and host launch families. With the strip removed, the replica launch test fails in all three cases.
- Scratch copies are deleted, and `$TMPDIR/gocache` is kept.

**Residual:** a replica on a claim launch before O6 has no control socket. It sees step 4, as before L21, and is still handed no key.

## Rev 4 (Astra round 4, Paul's ruling 26)

Round 4 found two P2s in rev 3.1's replica strip; both are accepted. Paul decided that a replica gets its home's key (ruling 26, which replaces rulings 17 and 25).

- **The strip is reverted.** A replica's env is composed like any other, so a replica sharing its home's configured key keeps it.
- **`replicaKeyGate`,** an agent-side check, runs on every replica launch (Start, Seed, the nested prepare and the claim launch source):
  - a delivered key must match the home key's digest;
  - a keyless replica starts only with a control socket, where the helper refuses at step 3;
  - with no socket, the agent refuses the launch itself, so the app never reaches the helper's mint fallback.
- **Pre-roll check.** Rollout step 1 is new: it compares each replica's key digest with its home's before any box is upgraded, and records matches in `CARLOS_REPLICA_HOME_KEYS`. Rollout step 6's wording is corrected.
- **New obligation O8 (replica placements, #162).** A replica record names no home, the home-to-replica mirror is the app's own, and no box can read another region's data bucket. So handing a keyless replica its home key's value needs a placement change. Until O8 lands, the fallback is the gate's reliable refusal.
- **Effect on stage B:** O2' and its test are unchanged. `SnapshotClaimLaunch` can now return `ErrReplicaKey` for a replica, and `LaunchEnvironment` is as in rev 3 again.

**How it was checked:**
- **Code blocks:** the plan's blocks, rebuilt from the document, match the tested scratch tree byte for byte. `go vet` and gofmt are clean, and builds for darwin, windows and linux/arm64 are clean.
- **Tests through a real launch:** the replica tests run the test binary as the app, executing rastrillo e3ea08de's resolution and its documented caller.
  - With no control socket: a keyless replica is refused and no app runs; the home's key runs and seals with it; a differing or unlisted key is refused.
  - With a socket (sandbox off): the helper refuses with `ErrNoInstanceKey`.
  - With the gate disabled, the app reports "minted". That is Astra's failure, so the test catches it.
- **Other paths tested:** the nested prepare (run and seed), the claim launch source, and the env-var parser.
- **Regression runs:** the activator launch families, run with the sandbox off, pass. So do the targeted host tests and all of `instancekey` and `hostfiles`.
- Scratch copies are deleted, and `$TMPDIR/gocache` is kept.

## Rev 5 (Astra round 5, Paul's rulings 27 and 28)

All three findings are accepted: two P2s and a P3, all about replicas.

- **R5-1, ruling 27:** a configured key is never changed in place.
  - **The procedure.** The rollout's operator note gives the coordinated procedure for a configured-key change on an app with replicas: stop the replicas, drop their list entries, change the key, sync, re-check, then start them.
  - **The safety net.** A replica's configured key must match its list entry, which only the pre-roll check writes. It must also match what the bucket's configuration renders now (`ReplicaConfigKey` over `appconfig.ConfiguredKeyDigest`).
  - **Why the fresh read matters.** It is cheap, it needs no O8, and it closes Astra's case: the home updated while the replica's box had not synced yet. With it removed, the test fails.
  - **Residual.** A change only on the home (a hand-edited home env, or a key configured per host) is documented by a test and left to the procedure, or to O8.
- **R5-2, ruling 28:** O8 delivers platform-minted keys only. A home whose key exists only in configuration is deferred, and its keyless replica is refused (tested).
- **R5-3:** `ParseHomeKeys` errors name the entry's position and the reason, never its text. The agent's logging path is tested.
- **IK-1: unchanged.** No IK-1 symbol changes in name, signature or behaviour. Only `ParseHomeKeys`'s error text changed, and stage B does not use it. O2' is unchanged.

**How it was checked:**
- **Code blocks:** the plan's blocks, rebuilt from the document, match the tested scratch tree byte for byte. `go vet` and gofmt are clean, and builds for darwin, windows and linux/arm64 are clean.
- **Tests:** the activator launch families, run with the sandbox off, pass. So do the targeted host tests and all of `instancekey`, `hostfiles`, `appconfig`, `moves` and `moves/archive`.
- Scratch copies are deleted, and `$TMPDIR/gocache` is kept.

## Rev 5.1 (Astra round 6)

Both P2s are accepted. Task 1's code is unchanged; it is built and reviewed on this branch.

- **R6-1, ruling 29:** the replica gate decides on the launch's own read. `instanceEnvLayer` returns the env and its key's layer from one read, every gate call passes that layer, and the gate reads no env file.
  - **The interleaving test** (`TestReplicaGateDecidesOnTheLaunchesOwnRead`) has a hook rewrite `<host>.config.env` between the launch's read and the gate, as a config sync can. The launch is refused.
  - **Red check:** with rev 5's second read restored, the app ran with the stale key.
- **R6-2, ruling 30: read on each launch.** The list moves from the agent's environment (never re-read by a running agent) to `/etc/carlos/replica-home-keys`.
  - The pre-roll check writes the list whole and renames it into place. The agent reads it at every replica launch and keeps no copy, so no restart is needed. A restart would bounce every instance on the box.
  - The procedure keeps replicas stopped throughout, and checks the file before the key changes.
- **Coordinator notes:**
  - Task 1's blocks are now the branch's code, from c568c13b to e8487c3a: the canonical-only `ParseAdoption`, the `ParseEnv` test, the two doc comments and `TestEmptyOwnInstanceKeyShadowsNothing`.
  - Rollout step 3 has the mixed-version moves readiness note: roll every box before any configured key matters.
- **IK-1:** unchanged by this revision. O2' and its test were re-run against stage B's tip b9a753f0 with the reviewed `instancekey`, and pass.

**How it was checked:**
- **Code blocks:** rebuilt from the document on top of the branch, they match the tested tree byte for byte, and Task 1's blocks equal the branch's files. `go vet` and gofmt are clean, and builds for darwin, windows and linux/arm64 are clean.
- **Tests:** the activator launch families, run with the sandbox off, pass. So do the targeted host tests and all of `instancekey`, `hostfiles`, `appconfig`, `moves` and `moves/archive`.
- Scratch copies are deleted, and `$TMPDIR/gocache` is kept.
