| 1 | # Instance key provision (L21): plan report, rev 3 |
| 2 | |
| 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. |
| 4 | - **Branch:** `instance-key-build`, worktree `/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-key-provision` |
| 5 | - **Plan:** `docs/superpowers/plans/2026-10-10-instance-key-provision.md`, rev 3, commit 3947e1d9 |
| 6 | - **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`. |
| 7 | |
| 8 | ## Dispositions (round 2) |
| 9 | |
| 10 | | # | Finding | Disposition | |
| 11 | |---|---|---| |
| 12 | | R2-1 | A configured key still bypasses the refusal (configured A, own B) | Accepted: source (c) dropped; configured keys need an adoption record too | |
| 13 | | 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 | |
| 14 | | 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 | |
| 15 | | R2-4 | No publication on the running session; O6 not gated | Accepted: `hostfiles.PlatformRelay` onto the session's publisher; O6 a gate in O4 | |
| 16 | | R2-5 | Moves leave the record behind | Accepted: O7 with the exact lists; O6 does not ship before O7 | |
| 17 | | note | The record is tenant-forgeable | Answered by ruling 19 (trusted-helper assertion) | |
| 18 | | R1-6 | The merge option | Closed: withdrawn by Astra | |
| 19 | |
| 20 | **Counts:** 5 accepted, 0 declined. One note answered, one round 1 finding closed. |
| 21 | |
| 22 | ## What changed in the code |
| 23 | |
| 24 | - `instancekey`: the record is now version 2 and writer-bound (`Binding`, `ParseAdoption`, `Binds`). New: the sources (`SourcePlatform`, `SourceConfig`, `SourceOf`) and `VerifyDelivered` for the install. |
| 25 | - `ClaimLaunchSnapshot` gains `InstanceKeySHA256` and `InstanceKeySource`, read from the same two files as the env; the identity is unchanged. |
| 26 | - 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. |
| 27 | - New `hostfiles.PlatformRelay` stages a platform item on a running publisher without blocking the caller. |
| 28 | - 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. |
| 29 | |
| 30 | ## How it was checked |
| 31 | |
| 32 | - 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. |
| 33 | - `go build` for windows, darwin and linux/arm64 of activator, host, hostfiles, instancekey and appconfig, and `go vet` on darwin: clean. |
| 34 | - **Tests run**, niced: |
| 35 | - all new tests; |
| 36 | - the activator `Seed`, `Start`, `Prepare`, `Control`, `Schedule`, `Claim` and `LaunchEnvironment` families, with the sandbox off for sockets; |
| 37 | - the host claim and env report tests; |
| 38 | - all of `instancekey`, `hostfiles`, `moves` and `moves/archive`. |
| 39 | |
| 40 | 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`. |
| 41 | - **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. |
| 42 | - Scratch copies are deleted. The shared `$TMPDIR/gocache` is untouched. |
| 43 | |
| 44 | ## The rastrillo addendum |
| 45 | |
| 46 | 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. |
| 47 | |
| 48 | ## Top concerns |
| 49 | |
| 50 | 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. |
| 51 | 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. |
| 52 | 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. |
| 53 | 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. |
| 54 | 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. |
| 55 | |
| 56 | ## Rev 3.1 (Astra round 3) |
| 57 | |
| 58 | Round 3 found no P1. Rev 3.1 fixes the two remaining findings; both are accepted. |
| 59 | |
| 60 | - **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. |
| 61 | - **R3-2 (P3), the dependency order.** The follow-ups now say O7 ships before O6. |
| 62 | - **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. |
| 63 | |
| 64 | **How it was checked:** |
| 65 | - 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. |
| 66 | - `go vet` and gofmt were clean. Builds for darwin, windows and linux/arm64 were clean. |
| 67 | - The new tests were run, sandbox off for sockets: |
| 68 | - `TestReplicaLaunchIsHandedNoInstanceKey`: real replica Start and Seed, with a configured key, a pre-existing env key and both, plus a home-row control case; |
| 69 | - `TestNestedReplicaPrepareIsHandedNoInstanceKey`: run and seed roles; |
| 70 | - `TestClaimReplicaLaunchIsHandedNoInstanceKey`. |
| 71 | |
| 72 | They pass with the activator and host launch families. With the strip removed, the replica launch test fails in all three cases. |
| 73 | - Scratch copies are deleted, and `$TMPDIR/gocache` is kept. |
| 74 | |
| 75 | **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. |
| 76 | |
| 77 | ## Rev 4 (Astra round 4, Paul's ruling 26) |
| 78 | |
| 79 | 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). |
| 80 | |
| 81 | - **The strip is reverted.** A replica's env is composed like any other, so a replica sharing its home's configured key keeps it. |
| 82 | - **`replicaKeyGate`,** an agent-side check, runs on every replica launch (Start, Seed, the nested prepare and the claim launch source): |
| 83 | - a delivered key must match the home key's digest; |
| 84 | - a keyless replica starts only with a control socket, where the helper refuses at step 3; |
| 85 | - with no socket, the agent refuses the launch itself, so the app never reaches the helper's mint fallback. |
| 86 | - **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. |
| 87 | - **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. |
| 88 | - **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. |
| 89 | |
| 90 | **How it was checked:** |
| 91 | - **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. |
| 92 | - **Tests through a real launch:** the replica tests run the test binary as the app, executing rastrillo e3ea08de's resolution and its documented caller. |
| 93 | - 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. |
| 94 | - With a socket (sandbox off): the helper refuses with `ErrNoInstanceKey`. |
| 95 | - With the gate disabled, the app reports "minted". That is Astra's failure, so the test catches it. |
| 96 | - **Other paths tested:** the nested prepare (run and seed), the claim launch source, and the env-var parser. |
| 97 | - **Regression runs:** the activator launch families, run with the sandbox off, pass. So do the targeted host tests and all of `instancekey` and `hostfiles`. |
| 98 | - Scratch copies are deleted, and `$TMPDIR/gocache` is kept. |
| 99 | |
| 100 | ## Rev 5 (Astra round 5, Paul's rulings 27 and 28) |
| 101 | |
| 102 | All three findings are accepted: two P2s and a P3, all about replicas. |
| 103 | |
| 104 | - **R5-1, ruling 27:** a configured key is never changed in place. |
| 105 | - **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. |
| 106 | - **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`). |
| 107 | - **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. |
| 108 | - **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. |
| 109 | - **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). |
| 110 | - **R5-3:** `ParseHomeKeys` errors name the entry's position and the reason, never its text. The agent's logging path is tested. |
| 111 | - **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. |
| 112 | |
| 113 | **How it was checked:** |
| 114 | - **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. |
| 115 | - **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`. |
| 116 | - Scratch copies are deleted, and `$TMPDIR/gocache` is kept. |
| 117 | |
| 118 | ## Rev 5.1 (Astra round 6) |
| 119 | |
| 120 | Both P2s are accepted. Task 1's code is unchanged; it is built and reviewed on this branch. |
| 121 | |
| 122 | - **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. |
| 123 | - **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. |
| 124 | - **Red check:** with rev 5's second read restored, the app ran with the stale key. |
| 125 | - **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`. |
| 126 | - 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. |
| 127 | - The procedure keeps replicas stopped throughout, and checks the file before the key changes. |
| 128 | - **Coordinator notes:** |
| 129 | - 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`. |
| 130 | - Rollout step 3 has the mixed-version moves readiness note: roll every box before any configured key matters. |
| 131 | - **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. |
| 132 | |
| 133 | **How it was checked:** |
| 134 | - **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. |
| 135 | - **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`. |
| 136 | - Scratch copies are deleted, and `$TMPDIR/gocache` is kept. |
| 137 | |