| index fc75635b2..ce6460166 100644 |
| --- a/docs/superpowers/plans/2026-10-24-service-runs.md |
| +++ b/docs/superpowers/plans/2026-10-24-service-runs.md |
| @@ -350,12 +350,28 @@ amends `docs/superpowers/specs/2026-10-08-carlos-run-design.md` § 2 ruling 1. |
| 7. spool the files and verify each against its declared sha256 (`400` |
| `file_hash_mismatch`, nothing written); |
| 8. mint the id, write the objects, and admit with `Key` and `Digest`; |
| - 9. if admission returns `*RunKeyReplay` (a concurrent twin won), delete |
| - this request's objects and answer `202` with the winner's run, replayed; |
| + 9. if admission returns `*RunKeyReplay`, compare its `RunID` with the id this |
| + request minted: |
| + - **equal:** this request's own admission committed, and the CAS retry |
| + inside `updateRecord` found its own key. That is success: answer `202` |
| + as a fresh admission, and delete nothing; |
| + - **different:** a concurrent twin won. Delete ONLY this request's |
| + candidate prefix (`runs.Prefix(acct, app, mintedID)`), then answer `202` |
| + with the winner's run, replayed; |
| + |
| `ErrRunKeyReused` answers `409`; |
| 10. today's lost-admission settling (`apiruns.go:279-291`) is unchanged, and |
| is now also correct for keys, because the key commits with the |
| admission or not at all. |
| +- **The rollout gate (mixed versions).** A box running an older binary |
| + rewrites the record through a typed `Record` that doesn't know `run_keys`, |
| + and drops it. So keyed admission is OFF until every record writer preserves |
| + the field: |
| + - the console config gains `RunKeysEnabled bool`, from `CARLOS_RUN_KEYS=on`; |
| + - while it is off, a keyed create is refused `503` `keys_not_ready` before |
| + anything is written, never silently treated as unkeyed; |
| + - it is turned on only after the box fleet has rolled to a build that |
| + includes this task. The branch description and the landing note say so. |
| - `GET .../instances/{host}/runs?key=<k>`: a new route on the run set |
| (`Exactly(Run)`, plus people). It returns `{"run_id": ...}` from the |
| record's `RunKeys` for the caller's scoped key, or 404. |
| @@ -392,6 +408,17 @@ amends `docs/superpowers/specs/2026-10-08-carlos-run-design.md` § 2 ruling 1. |
| - `TestRunKeyLookup` |
| - `TestRunUnkeyedOrderUnchanged`: an unkeyed create against an open run is |
| still refused before the body is read. |
| + - `TestAPIRunCreateAdmissionWhoseAnswerWasLost`: extend the existing test to |
| + a keyed request. The store's conflict-after-commit gives a `RunKeyReplay` |
| + with this request's OWN id, the answer is a fresh `202`, and the run's |
| + objects still exist. |
| + - `TestRunKeysSurviveEveryRecordMutation`: a record carrying `run_keys`, |
| + passed through each of the six `ActiveRun` mutations in |
| + `internal/provision/run.go` and the move and import mutations, still |
| + carries them. |
| + - `TestKeyedCreateRefusedUntilKeysAreOn`: with `RunKeysEnabled` false, a |
| + keyed create gets `503` `keys_not_ready` and nothing is written. An unkeyed |
| + create is unaffected. |
| - [ ] **Step 2: Run them and watch them fail.** |
| `go test ./internal/runs/ ./internal/provision/ ./internal/console/ -run 'Key|Keyed|Digest'` |
| fails. |
| @@ -454,7 +481,9 @@ then squash to one commit, wait for CI to go green on the tip, and merge with |
| branch, because it grants authority (AGENTS.md delegation policy). Astra |
| reviews too, since the session is Opus. |
| |
| -After it lands and the console is rolled, Tito mints `fleet-runs` with |
| +After it lands, roll the console AND the boxes, confirm every box runs a |
| +build that includes Task 4, and only then set `CARLOS_RUN_KEYS=on` on the |
| +console. Then Tito mints `fleet-runs` with |
| `--role run --app titogo --commands "<the import and sync commands>"`. The |
| exact commands are settled with the titogo side of PRO-6214, outside this |
| plan. |