| 1 | # Census approval: pro-6214-service-runs |
| 2 | |
| 3 | 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. |
| 4 | |
| 5 | Ruling `census-ruling-pro-6214-service-runs`, asked of the census owner on |
| 6 | #280 before the merge. Every row already carries it (#369). Branch |
| 7 | `pro-6214-service-runs`. Spec: |
| 8 | `docs/superpowers/specs/2026-10-24-service-runs-design.md` (Linear PRO-6214). |
| 9 | |
| 10 | The branch adds a service role, `run`, so that a server (Tito's Fleet) can |
| 11 | start and follow `carlos run` runs without a person's token. It also adds |
| 12 | idempotent run create (`Idempotency-Key`, recorded in the instance record's |
| 13 | `run_keys`). No root read is added. The console units run in the console |
| 14 | (`cmd/console serve`, user `carlos-console`), and the root `carlos` binary |
| 15 | does not link `internal/console` (`TestRootBinaryLinksNoConsoleOrMail`). The |
| 16 | CLI units run in `carlos run` as its caller. |
| 17 | |
| 18 | ## What you are asked |
| 19 | |
| 20 | - **Approvals (22): 14 changed units and 8 envelopes.** Each changed unit |
| 21 | keeps its class and task and reads only the kinds it already read. 13 are |
| 22 | `body` changes. 1 is `sites`: `migrateCLIAuth` has one more SQLite `Exec`, |
| 23 | the new index. census-propose wrote these rows into |
| 24 | `plan14-root-reads-pro-6214-service-runs.tsv`, and their table is under |
| 25 | "Approvals" below. |
| 26 | - **Decisions (6): 5 new units and 1 removed unit.** census-propose cannot |
| 27 | write these. They are hand-written in |
| 28 | `plan14-root-reads-pro-6214-service-runs-decisions.tsv`, which loads |
| 29 | before the approvals overlay. Each new unit is proposed as accepted legacy |
| 30 | at the class of the row it mirrors: |
| 31 | |
| 32 | | decision | unit | proposed disposition | mirrors | reads | digest | |
| 33 | |---|---|---|---|---|---| |
| 34 | | 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` | |
| 35 | | 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` | |
| 36 | | 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` | |
| 37 | | 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` | |
| 38 | | 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` | |
| 39 | | 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 | | |
| 40 | |
| 41 | - **Record-write census (9B8): 6 entries re-digested, classes and kinds |
| 42 | unchanged** (`internal/provision/service_runs_census_test.go`, hand-written |
| 43 | as 9B8 still requires). They are `admitRun` (record-writer, |
| 44 | `key:SetRecordRun`: it passes the key, digest and caller to `RunAdmit`, |
| 45 | and the key is written by the same CAS), `handleAPIRunAck`, `Cancel` and |
| 46 | `Expire` (refusal codes only), `auditRunEnd` (nonrecord, the requester |
| 47 | line), and `registerCLIRoutes` (key-site: the run doors move to |
| 48 | `svcrole.Exactly(svcrole.Run)`, plus one route for the key lookup). |
| 49 | `RunAdmit` names no census identifier, so it is not a 9B8 site. |
| 50 | |
| 51 | ## The four units that carry authority |
| 52 | |
| 53 | The census classes are all accepted legacy, because nothing here is root |
| 54 | authority. The branch does grant authority, though, and these four are where |
| 55 | it lives: |
| 56 | |
| 57 | 1. **The run doors.** |
| 58 | - `Server.handleAPIRunCreate` (approval) and the new |
| 59 | `Server.handleAPIRunKeyLookup` (decision). The follow doors |
| 60 | `handleAPIRunGet`, cancel, expire and `handleAPIRunAck` reach a service |
| 61 | only through `runGate`, which has no scanned site and is pinned by the |
| 62 | `internal/console` envelope. |
| 63 | - They admit a service principal only with role `run` and a credential |
| 64 | id. The route sets are `svcrole.Exactly(Run)`, so Admin does not reach |
| 65 | them. |
| 66 | - A service runs only an argv that starts, word for word, with a prefix |
| 67 | its credential was minted with. It sees and acts on only the runs its |
| 68 | credential id started; any other answers 404. |
| 69 | - `on_behalf_of` is recorded on the service's word, shape-checked as an |
| 70 | email, and grants nothing. |
| 71 | 2. **`provision.RunAdmit`** (approval). |
| 72 | - Admission and the idempotency dedupe are one compare-and-swap. |
| 73 | - Keys are pruned at 24 hours, each caller keeps its newest 32, and a |
| 74 | key is stored hashed with its caller. |
| 75 | - Only `RunAdmit` assigns `RunKeys` (`TestOnlyRunAdmitWritesRunKeys`). |
| 76 | 3. **The mint, `Store.claimRawNewServicePrincipalScoped`** (approval). |
| 77 | - It is the only writer of a run credential, and is reached from the two |
| 78 | owner-gated mint doors. The API door is registered `svcrole.Set{}`, so |
| 79 | no service credential can mint one. |
| 80 | - A run credential needs an app and 1 to 32 valid command prefixes. |
| 81 | - Every mint draws a fresh `credential_id`, so a revoked name minted |
| 82 | again owns none of the old runs. |
| 83 | 4. **The read, `Store.CLIPrincipal`** (approval). |
| 84 | - A run row resolves only with an app, a `credential_id`, and a command |
| 85 | list in the exact canonical form this console writes. Anything else |
| 86 | does not resolve: it fails closed. |
| 87 | |
| 88 | ## Seal |
| 89 | |
| 90 | The chain ends at main's `edge-asset-cache-2` (3d7cbe30): `de15b849…7069`. |
| 91 | |
| 92 | - The decisions overlay adds five frozen declarations: |
| 93 | `ec2d8daaec385b34b3be9c8c6430427c082ac36cd40150e1c6f520e20e3da72d`. |
| 94 | - The approvals overlay re-freezes fourteen: |
| 95 | `86570d1326baad6d866b4066b6bf65218434b201fb1f2c05ebc6e3c79f9c5a98`. |
| 96 | |
| 97 | Both lines are in `plan14-root-reads-chain.tsv`, after main's. |
| 98 | |
| 99 | At the approved tip ffe8f40a the chain ended at `sec-host-imdsv2-module` |
| 100 | (`945934f5…6c6a`), and the seals were `bcad5454…89a2` and `b9f9c8c7…975c`. |
| 101 | Re-sealing changed only seals, fingerprints and the chain order, with one |
| 102 | exception: `cmd/console/main.go` `serve`, which main's sec-sidecar-create-gate |
| 103 | and sec-mail-user-boundary also changed. Its row now carries their two |
| 104 | rulings in its inherited basis, and its digest and data-literal offsets |
| 105 | moved (`@4601,@5702,@5939`, still three). Its claim, class and task are the |
| 106 | ones approved. The `.`, `cmd/console` and `internal/console` envelopes now |
| 107 | also cover main's code. |
| 108 | |
| 109 | Re-running `make census-propose NAME=pro-6214-service-runs` rewrites the |
| 110 | section below and replaces this whole file. Restore the sections above |
| 111 | after a re-run. |
| 112 | |
| 113 | ## Approvals (written by census-propose) |
| 114 | |
| 115 | Ruling `census-ruling-pro-6214-service-runs`. Every change below keeps the disposition it already has, so it needs an approval, not a decision. |
| 116 | |
| 117 | | unit | change | read sites | what changed | |
| 118 | |---|---|---|---| |
| 119 | | `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 | |
| 120 | | `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) | |
| 121 | | `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 | |
| 122 | | `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 | |
| 123 | | `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 | |
| 124 | | `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 | |
| 125 | | `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 | |
| 126 | | `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 | |
| 127 | | `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 | |
| 128 | | `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 | |
| 129 | | `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 | |
| 130 | | `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 | |
| 131 | | `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 | |
| 132 | | `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 | |
| 133 | |
| 134 | Envelopes re-pinned: `.`, `cmd/console`, `internal/console`, `internal/console/templates`, `internal/host`, `internal/provision`, `internal/runs`, `internal/svcrole`. |
| 135 | |
| 136 | Seal: `ec2d8daaec385b34b3be9c8c6430427c082ac36cd40150e1c6f520e20e3da72d` → `86570d1326baad6d866b4066b6bf65218434b201fb1f2c05ebc6e3c79f9c5a98`. |
| 137 | |
| 138 | Overlay: `internal/provision/testdata/plan14-root-reads-pro-6214-service-runs.tsv`, appended to `internal/provision/testdata/plan14-root-reads-chain.tsv`. |
| 139 | |