carlos / platform Public

docs: service runs — own-admission replay is success, keys gated until every record writer keeps them (PRO-6214)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 2f815830feba4df4a6e547ff3afb7d149d260179 parent 70c5e1c
2 files changed, +39 −4
  • docs/superpowers/plans/2026-10-24-service-runs.md +32 −3
  • docs/superpowers/specs/2026-10-24-service-runs-design.md +7 −1
diff --git a/docs/superpowers/plans/2026-10-24-service-runs.md b/docs/superpowers/plans/2026-10-24-service-runs.md
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.
diff --git a/docs/superpowers/specs/2026-10-24-service-runs-design.md b/docs/superpowers/specs/2026-10-24-service-runs-design.md
index 28fb41607..2472482ee 100644
--- a/docs/superpowers/specs/2026-10-24-service-runs-design.md
+++ b/docs/superpowers/specs/2026-10-24-service-runs-design.md
@@ -195,7 +195,10 @@ the create response can't tell "started" from "never arrived". So:
- `GET .../instances/{host}/runs?key=<key>` returns the run id from the
record, or 404.
- **Refusals are not remembered** (my call). Only an admitted run is.
- - A 4xx means nothing ran.
+ - A 4xx means THIS attempt admitted nothing. An earlier attempt with the
+ same key may already have run: the shared gates (401, the rate budget's
+ 429) come before the key lookup, so the caller confirms with
+ `GET ...?key=` before treating a 4xx on a retry as "never ran".
- A retry with the same key is evaluated afresh: a `run_open` refusal can
succeed once the other run has finished, which is what the caller asked
for.
@@ -205,6 +208,9 @@ the create response can't tell "started" from "never arrived". So:
than 24 hours, and to the newest 128 (the builder may tune both and must
document them). A caller must not retry a key older than that. Fleet retries
within minutes.
+- **Mixed versions:** an older box binary rewriting the record would drop
+ `run_keys`. Keyed creates stay off (`503` `keys_not_ready`) until the console
+ is told, by config, that every record writer preserves the field.
- **A known limit:** deleting an instance deletes its record, `run_keys` with
it. A retry that arrives after the host was deleted and created again is a
fresh run on the new incarnation. It is still only a command the credential