# Census approval: pro-6214-service-runs

Approved by Paul (census owner), in session, 2026-10-24, at tip ffe8f40a. Re-sealed onto main 3d7cbe30 with no change to the approved claims.

Ruling `census-ruling-pro-6214-service-runs`, asked of the census owner on
#280 before the merge. Every row already carries it (#369). Branch
`pro-6214-service-runs`. Spec:
`docs/superpowers/specs/2026-10-24-service-runs-design.md` (Linear PRO-6214).

The branch adds a service role, `run`, so that a server (Tito's Fleet) can
start and follow `carlos run` runs without a person's token. It also adds
idempotent run create (`Idempotency-Key`, recorded in the instance record's
`run_keys`). No root read is added. The console units run in the console
(`cmd/console serve`, user `carlos-console`), and the root `carlos` binary
does not link `internal/console` (`TestRootBinaryLinksNoConsoleOrMail`). The
CLI units run in `carlos run` as its caller.

## What you are asked

- **Approvals (22): 14 changed units and 8 envelopes.** Each changed unit
  keeps its class and task and reads only the kinds it already read. 13 are
  `body` changes. 1 is `sites`: `migrateCLIAuth` has one more SQLite `Exec`,
  the new index. census-propose wrote these rows into
  `plan14-root-reads-pro-6214-service-runs.tsv`, and their table is under
  "Approvals" below.
- **Decisions (6): 5 new units and 1 removed unit.** census-propose cannot
  write these. They are hand-written in
  `plan14-root-reads-pro-6214-service-runs-decisions.tsv`, which loads
  before the approvals overlay. Each new unit is proposed as accepted legacy
  at the class of the row it mirrors:

  | decision | unit | proposed disposition | mirrors | reads | digest |
  |---|---|---|---|---|---|
  | new | `internal/console/apiruns.go` `Server.handleAPIRunKeyLookup` | legacy, accepted-legacy. **Authority: a run door** | `handleAPIRunGet` (census-ruling-carlos-run) | `file:Query@528`, which is `r.URL.Query()` (the key), not a file; then the instance record from the deployment bucket. Writes nothing | `635e57ad…076e` |
  | new | `internal/console/authstore.go` `Store.backfillCredentialIDs` | legacy, accepted-legacy (J4) | `migrateCLIAuth` (J4 root-input) | `file:Query@62`, `file:Exec@554` on the console's own `cli_tokens`; it sets an id only where none is set | `3fbb21ec…48b5` |
  | new | `internal/console/authstore.go` `newCredentialID` | legacy, accepted-legacy (J4) | `newStore` (email-signin), `Keymail.sealEnvelope` (console-invite-delivery) | `file:Read@78`, a 16-byte `crypto/rand.Read`: an identity, not a secret | `57049041…0cb5` |
  | new | `run.go` `runFileSum` | legacy, accepted-legacy | `runMetaFor` (census-ruling-carlos-run) | `file:Open@58` of a `--file` path the caller named, hashed to EOF; only with `--idempotency-key` | `745fa0d0…6991` |
  | new | `auth.go` `consoleClient.doMultipartHeaders` | legacy, accepted-legacy (J4) | `consoleClient.doMultipartMethod` (J4 root-input) | `file:ReadAll@1418` of the console's response body: `doMultipartMethod`'s body, moved, plus extra request headers that cannot override the content type or the bearer | `4682be87…c17b` |
  | removed | `auth.go` `consoleClient.doMultipartMethod` | H3 census-ruling removal | zddr3's and email-signin's form | still declared and reads nothing: it calls `doMultipartHeaders` with no headers | |

- **Record-write census (9B8): 6 entries re-digested, classes and kinds
  unchanged** (`internal/provision/service_runs_census_test.go`, hand-written
  as 9B8 still requires). They are `admitRun` (record-writer,
  `key:SetRecordRun`: it passes the key, digest and caller to `RunAdmit`,
  and the key is written by the same CAS), `handleAPIRunAck`, `Cancel` and
  `Expire` (refusal codes only), `auditRunEnd` (nonrecord, the requester
  line), and `registerCLIRoutes` (key-site: the run doors move to
  `svcrole.Exactly(svcrole.Run)`, plus one route for the key lookup).
  `RunAdmit` names no census identifier, so it is not a 9B8 site.

## The four units that carry authority

The census classes are all accepted legacy, because nothing here is root
authority. The branch does grant authority, though, and these four are where
it lives:

1. **The run doors.**
   - `Server.handleAPIRunCreate` (approval) and the new
     `Server.handleAPIRunKeyLookup` (decision). The follow doors
     `handleAPIRunGet`, cancel, expire and `handleAPIRunAck` reach a service
     only through `runGate`, which has no scanned site and is pinned by the
     `internal/console` envelope.
   - They admit a service principal only with role `run` and a credential
     id. The route sets are `svcrole.Exactly(Run)`, so Admin does not reach
     them.
   - A service runs only an argv that starts, word for word, with a prefix
     its credential was minted with. It sees and acts on only the runs its
     credential id started; any other answers 404.
   - `on_behalf_of` is recorded on the service's word, shape-checked as an
     email, and grants nothing.
2. **`provision.RunAdmit`** (approval).
   - Admission and the idempotency dedupe are one compare-and-swap.
   - Keys are pruned at 24 hours, each caller keeps its newest 32, and a
     key is stored hashed with its caller.
   - Only `RunAdmit` assigns `RunKeys` (`TestOnlyRunAdmitWritesRunKeys`).
3. **The mint, `Store.claimRawNewServicePrincipalScoped`** (approval).
   - It is the only writer of a run credential, and is reached from the two
     owner-gated mint doors. The API door is registered `svcrole.Set{}`, so
     no service credential can mint one.
   - A run credential needs an app and 1 to 32 valid command prefixes.
   - Every mint draws a fresh `credential_id`, so a revoked name minted
     again owns none of the old runs.
4. **The read, `Store.CLIPrincipal`** (approval).
   - A run row resolves only with an app, a `credential_id`, and a command
     list in the exact canonical form this console writes. Anything else
     does not resolve: it fails closed.

## Seal

The chain ends at main's `edge-asset-cache-2` (3d7cbe30): `de15b849…7069`.

- The decisions overlay adds five frozen declarations:
  `ec2d8daaec385b34b3be9c8c6430427c082ac36cd40150e1c6f520e20e3da72d`.
- The approvals overlay re-freezes fourteen:
  `86570d1326baad6d866b4066b6bf65218434b201fb1f2c05ebc6e3c79f9c5a98`.

Both lines are in `plan14-root-reads-chain.tsv`, after main's.

At the approved tip ffe8f40a the chain ended at `sec-host-imdsv2-module`
(`945934f5…6c6a`), and the seals were `bcad5454…89a2` and `b9f9c8c7…975c`.
Re-sealing changed only seals, fingerprints and the chain order, with one
exception: `cmd/console/main.go` `serve`, which main's sec-sidecar-create-gate
and sec-mail-user-boundary also changed. Its row now carries their two
rulings in its inherited basis, and its digest and data-literal offsets
moved (`@4601,@5702,@5939`, still three). Its claim, class and task are the
ones approved. The `.`, `cmd/console` and `internal/console` envelopes now
also cover main's code.

Re-running `make census-propose NAME=pro-6214-service-runs` rewrites the
section below and replaces this whole file. Restore the sections above
after a re-run.

## Approvals (written by census-propose)

Ruling `census-ruling-pro-6214-service-runs`. Every change below keeps the disposition it already has, so it needs an approval, not a decision.

| unit | change | read sites | what changed |
|---|---|---|---|
| `cmd/console/main.go` `serve` | body | unchanged (3) | the console (cmd/console serve, user carlos-console): reads CARLOS_RUN_KEYS, an environment variable, into Config.RunKeysEnabled; keyed run creates are refused 503 keys_not_ready unless it is on; the same three /data literals; no read added |
| `internal/console/apiruns.go` `Server.handleAPIRunCreate` | body | unchanged (1) | the console (cmd/console serve, user carlos-console): AUTHORITY (the run doors): also admits a run service (runCaller: role run with a credential id, after the unchanged shared gate and the route's svcrole.Exactly(Run), which Admin does not join); a service's argv must start word for word with one of its granted prefixes (runs.CommandGranted), else 403 command_not_granted before any object is written; on_behalf_of only from a service, one bare email of at most 254 bytes, never checked against membership, granting nothing; a keyed create (Idempotency-Key, only with CARLOS_RUN_KEYS=on) answers a key the record's run_keys holds for this caller before the record refusals and without reading a file (same digest: 202 replayed; another: 409); keyed files must declare and match their sha256; records by_service, by_credential, on_behalf_of and the scoped key hash; an open-run refusal is redacted for another caller's run; refusal codes; its one site is still rec.ActiveRun.Open(), a method, not a file; no read added (the record and request.json from the deployment bucket, as before) |
| `internal/console/apirunsfollow.go` `Server.handleAPIRunAck` | body | unchanged (1) | the console (cmd/console serve, user carlos-console): AUTHORITY (the run doors, via runGate): a run service acks only its own runs; refusal codes; RunClear unchanged and leaves run_keys as they are; its one site, run.Open(), a method, moves; no read added |
| `internal/console/apirunsfollow.go` `Server.handleAPIRunGet` | body | unchanged (1) | the console (cmd/console serve, user carlos-console): AUTHORITY (the run doors, via runGate): a run service reaches it only for a run whose request.json records its credential id, any other answering 404 as an unknown id; refusal codes; the same r.URL.Query() site; no read added |
| `internal/console/apirunsfollow.go` `Server.runTransitionRefused` | body | unchanged (1) | the console (cmd/console serve, user carlos-console): refusal codes only (run_started, run_ended and the shared codes); its one site, run.Open() on the record's run state, a method, moves; no read added |
| `internal/console/authstore.go` `Store.CLITokens` | body | unchanged (1) | the console (cmd/console serve, user carlos-console): its Query also selects cli_tokens.commands; the same Query site; no read added |
| `internal/console/authstore.go` `Store.claimRawNewAppToken` | body | unchanged (1) | the console (cmd/console serve, user carlos-console): the legacy app token's INSERT also writes a fresh credential_id (newCredentialID); the same Exec site; no read added |
| `internal/console/svcprincipal.go` `Store.CLIPrincipal` | body | unchanged (2) | the console (cmd/console serve, user carlos-console): AUTHORITY (the credential read): its cli_tokens QueryRow also selects credential_id and commands; a run row resolves only with an app, a credential_id and a command list that parses, passes runs.CheckCommandPrefixes and is byte-identical to the canonical JSON this console writes, else it does not resolve (fail closed, the deploy rungs rule's twin); any other role carrying commands does not resolve; the same QueryRow and last_used Exec sites; no read added |
| `internal/console/svcprincipal.go` `Store.ServicePrincipals` | body | unchanged (1) | the console (cmd/console serve, user carlos-console): its Query also selects cli_tokens.commands for the account's service listing; the same Query site; no read added |
| `internal/console/svcprincipal.go` `Store.claimRawNewServicePrincipalScoped` | body | unchanged (2) | the console (cmd/console serve, user carlos-console): AUTHORITY (the mint): the only writer of a run credential, from the two owner-gated mint doors (POST /api/cli/services/{account}, registered svcrole.Set{} so no service credential reaches it, and the settings page behind serviceOwner); a run credential must name an app and 1 to 32 valid command prefixes, takes no rungs, and other roles may not name commands; stores the list as canonical JSON in cli_tokens.commands and a fresh credential_id on every mint, including a reused revoked name; the same QueryRow and INSERT Exec sites; no read added |
| `internal/console/templates/account_settings.html` `external` | body | unchanged (1) | the console (cmd/console serve, user carlos-console): embedded UI: a Commands column, the minted line names a run credential's commands, and a commands input on the mint form; markup only; no read added |
| `internal/provision/run.go` `RunAdmit` | body | unchanged (1) | the console (cmd/console serve, user carlos-console): AUTHORITY (RunAdmit): still applied only by the create door's admitRun through SetRecordRun's unchanged read-CAS-write; prunes run_keys older than 24h on every admission; a keyed admission checks its key before every refusal (same digest: *RunKeyReplay; another: ErrRunKeyReused; neither changes the record); a keyed admission that succeeds appends {key, run_id, digest, caller, at} and keeps each caller's newest 32, in the same write that queues the run; refusals and the queued RunState unchanged; within internal/provision only RunAdmit assigns RunKeys (TestOnlyRunAdmitWritesRunKeys); its one site is still rec.ActiveRun.Open(), a method; no read added |
| `run.go` `runMetaFor` | body | unchanged (1) | the carlos CLI on the caller's own machine, as that caller: with --idempotency-key it also declares each --file's sha256 (runFileSum, a new unit); the same Stat of the path the caller named; no read added here |
| `internal/console/authstore.go` `Store.migrateCLIAuth` | sites | +file:Exec | the console (cmd/console serve, user carlos-console): one more Exec of the kind it already makes: SQLite DDL on its own database, the partial unique index cli_tokens_credential_id; the ALTER loop adds credential_id and commands (TEXT NOT NULL DEFAULT ''); then calls backfillCredentialIDs; additive; no new kind of read |

Envelopes re-pinned: `.`, `cmd/console`, `internal/console`, `internal/console/templates`, `internal/host`, `internal/provision`, `internal/runs`, `internal/svcrole`.

Seal: `ec2d8daaec385b34b3be9c8c6430427c082ac36cd40150e1c6f520e20e3da72d` → `86570d1326baad6d866b4066b6bf65218434b201fb1f2c05ebc6e3c79f9c5a98`.

Overlay: `internal/provision/testdata/plan14-root-reads-pro-6214-service-runs.tsv`, appended to `internal/provision/testdata/plan14-root-reads-chain.tsv`.
