# `carlos db` for instances whose data lives in another region

**Status:** rev 7, 2026-10-23. Rev 7 (plan 3, 2026-10-23): the pull's cap and chunk size, from a measurement on dev
(§5.7); the request bound is unchanged.
Rev 6, 2026-10-22, changes no mechanism: it states four
things the plan review (`CODEX-FINDINGS-DB-REGION-FORWARD-PLANS.md`) found
the plans had to decide, each marked **(plan review N)**: what a result
names when a chunk cannot be confirmed (§5.4, 9), a box's `cancel` read
before it elects (§5.5 step 4, §5.8, 11), a retry's reservation (§5.7, 10),
and the order of the audit row, the creates and the serial bump, with
nothing but `cancel` once the request has ended, checked before each write
(§5.5 step 3, §9.3, 11 and 28). Round 2 of the plan review added DBF-32
(§10) for SQL that JSON would rewrite, and the console's refusal of it
(§5.5 step 3, 36).
Rev 5 answers Astra's round 4
(`CODEX-FINDINGS-DB-REGION-FORWARD-R4.md`), which found J1 to J5 fixed, with
four direct fixes and no new mechanism: flushed progress lines and the
backhaul header bound (codex 19), the result held until its chunks are out
(codex 20), restart reconciliation by refusal and a 5 s worker exit (codex
8), and the retention size wording (codex 7). Design only, nothing built. Written for Paul,
who approved the direction (§2). Astra accepted rev 5 in round 5, and Paul
approved building it on 2026-10-22, with the rulings in §16 (ruling 1:
ship through the deployment bucket, per-region bucket as plan 5). Review brief:
`CODEX-REVIEW-BRIEF-DB-REGION-FORWARD.md`. Rev 1 was `b35b3ab1`, rev 2
`2cf4a88f`, rev 3 `944a61fd`, rev 4 `c07629e4`. Rev 4 answered Astra's round 3
(`CODEX-FINDINGS-DB-REGION-FORWARD-R3.md`) by making a forwarded read one
synchronous HTTP request (§5.2): no follow, no cancel route, nothing the
console writes after the request ends, and the store's lifecycle rule as
the only deletion. Five invariants (§5.3) replace rev 3's seven, and every
finding is mapped to the invariant that closes it or to the synchronous
design that removes it. Each change is marked **(codex N)** where it lands,
and §18 lists what was declined. The coordinator's provisional answers to
the open questions are now Paul's rulings (§16, 2026-10-22).

**Companion specs:** `2026-09-02-carlos-db-design.md` (the db door, cited
below as "db spec"), `2026-10-08-carlos-run-design.md` (the run transport,
"run spec"), `2026-09-11-isolated-exec-design.md` (the tenant unit sandbox).

## 1. The problem

`carlos db query`, `carlos db pull` and their `--at` forms restore a copy of
an instance's database on the member's own machine. The CLI fetches the
replica's LTX files by coordinate through the console's db door
(`internal/console/apidb.go`). The console reads them from the data bucket
of its own region.

The console refuses every instance whose record `Region` differs from its
own `CARLOS_REGION` (`resolveReplica`, `ErrResidency`,
`dbResidencyRefusal`). That refusal is correct for the console: each
region's box role is granted its own region's data bucket and no other
(`infra/modules/region-host/main.tf`, `host_buckets`), and the console runs
on the ie box.

On 2026-10-21 all twelve instances of one customer account moved from ie to
uk with the offline operator move. The customer can no longer run
`carlos db query` against any of them. Every instance in a region other than
the console's has the same gap, and self-serve moves and nearest-region
placement will make that the common case.

## 2. The direction, and what it changes

Paul approved this direction: the console forwards a db read to a box in the
region where the data lives. That box reads its own region's replica, runs
the read-only operation there, and returns only the result. The console
never reads another region's bucket. There is no console per region.

Two earlier rulings bend under it, and this spec says so rather than
working around them:

- **db spec §9, "no path that runs anything on the box".** A forwarded query
  runs member SQL on a box. §8 is how that stays safe.
- **db spec §6 rejected "tenant database bytes in the deployment bucket".**
  A forwarded read has to come back to the console through the deployment
  bucket (§5.1). This spec puts only encrypted bytes there, sealed to a key
  that exists only in the member's CLI (§5.6), until the store's lifecycle
  rule expires them (§5.7).
  In phase 1 that is only a
  query's rows. Phase 2's pull would send the planned replica files, which
  are a pull's result, under ruling 1 (§16, Paul, 2026-10-22). A per-region
  bucket that keeps this ciphertext in the data's region is a follow-up
  (plan 5, §14).

One earlier ruling holds unchanged: the run spec's "the console still never
connects to a box". The transport is the run's.

## 3. What exists today (facts this design rests on)

1. **The CLI restores locally.** `db.go` restores the replica through
   `dbdoor.RestoreAsOf` over a read-only client (`internal/dbdoor/client.go`)
   into a private 0700 directory, checks it with `lease.CheckDB`, then
   either copies it to `--out` or runs one query. The query runs with
   `mode=ro`, `query_only`, `temp_store(2)`, `SQLITE_LIMIT_ATTACHED` 0 and
   the one-statement and pragma guard `checkQuerySQL`. The console never
   executes tenant SQL.
2. **`--at` depends on listing times.** litestream's timestamp plan keeps a
   file only if its `CreatedAt` is before the target. The legacy door lists
   S3 `LastModified`, which has second resolution, so the target is
   truncated to its second. The claim-mode door lists no times, and `--at`
   refuses there (`dbdoor.ErrNoTimes`).
3. **`carlos run` already reaches any region's boxes.** Its transport is
   objects in the deployment bucket under the app's subtree
   (`internal/runs`): `request.json`, `out/<seq>` chunks of 256 KiB,
   `result.json`, written create-only where it matters. The console bumps
   `control/serial`; boxes poll it every 2 s. The console's follow door
   pages chunks to the CLI, and its sweep (`internal/console/runsweep.go`)
   expires, closes lost runs, and deletes old ones. A run is admitted with a
   CAS on the instance record, because a run needs the instance to itself.
4. **The deployment bucket is writable by every platform box**, so an
   unsigned control object is a request any box could have written. The
   host-retry grant (`internal/release/hostretry.go`) answers that with a
   root-signed envelope: `release.SignRootEnvelope` on the console,
   `VerifyRootEnvelope` against `CARLOS_RELEASE_PUBKEY` on the box. Scoped
   release keys are refused.
5. **Sealing to a public key exists.** `internal/appconfig/crypto.go`
   seals with ECDH P-256, HKDF-SHA256 and AES-256-GCM, with bound associated
   data (the v2 envelope), using only the standard library and
   `golang.org/x/crypto/hkdf`, which `go.mod` already carries.
6. **Boxes publish capabilities.** `control/edges/<box>.json` carries
   `capabilities` (`region-ha-1`, `offline-move-1`, `offline-stage-1`).
   `edgesfile.ReadLiveSnapshot` joins each file with the box's status
   snapshot for liveness. The offline move's doors refuse a region holding
   a box without the capability, and each box publishes the capability only
   when its armed worker can act on it.
7. **The box role can read its region's replicas.** In
   carlosframework/carloku-infrastructure, `region-uk.tf` passes
   `data_bucket_arn = aws_s3_bucket.data_uk.arn` to the `region-host`
   module (ref v0.21.0; ie is at v0.28.8). At both refs `host_buckets`
   grants `s3:GetObject` on `data_bucket_arn/*` and `s3:ListBucket` on the
   bucket, plus read and write on the deployment bucket, which stays in
   eu-west-1 and is shared by every region. Sibling regions get push-only
   `moves/*`. So a uk box can read uk replicas, and nothing else can. No
   IAM change is needed for this design.
8. **Tenant processes already run sandboxed.** The claim-bound unit
   template (`internal/host/claimunit_template.go`) and the `carlos-app@`
   units use `ProtectSystem=strict`, `TemporaryFileSystem=/data:ro`,
   `InaccessiblePaths=/etc/carlos`, `PrivateTmp`, `NoNewPrivileges`, an
   empty capability set and an `IPAddressDeny` list that covers the
   metadata service. `carlos-app@` sockets are bound by root systemd
   (`SocketUser=carlos`), so the agent can start a sandboxed unit with no
   privilege of its own by connecting to its socket.
9. **The agent runs as `carlos`** (`carlos-edge.service`, `User=carlos`).
   The console runs as `carlos-console`.

## 4. Decisions

| # | Decision | Section |
|---|---|---|
| D1 | Phase 1 forwards `query` and `query --at`. `pull` and `pull --at` forward in phase 2, under ruling 1 (§16). The claim-mode door and the seeds `--from` door stay refused. | §6 |
| D2 | Any live box in the record's region that publishes `db-read-1` and is not draining may serve. The first to create the read's `picked` marker serves it. The instance's lease holder gets no preference. | §7 |
| D3 | Transport is the run's objects in the deployment bucket and a serial bump, but the read is one synchronous HTTP request: the console waits on the box's objects inside it and streams them to the CLI. The request is a root-signed envelope. No record write. Every object is a create-only write under `db-reads/`, never replaced or deleted by the protocol; the store's lifecycle rule is the only deletion (§5.2, §5.3). | §5 |
| D4 | Results are sealed to an ephemeral key the CLI makes for each read. The console and the deployment bucket only ever hold the result as ciphertext. The console does see the SQL, as the envelope's signer. | §5.6 |
| D5 | The box splits the work. The agent (`carlos`) verifies, resolves, plans and streams objects. Everything that decodes tenant bytes (LTX, SQLite, the member's SQL) runs in a new sandboxed unit, `carlos-dbread@.service`, as a `DynamicUser` with no network. | §8 |
| D6 | The worker gets the planned LTX files and nothing else. Its scratch is its own private `/var/tmp`, which systemd removes when the unit stops. | §8.3 |
| D7 | Rollout: boxes publish `db-read-1`; the console forwards only with `CARLOS_DB_FORWARD=1` and a capable region. Dormant by default. | §11 |
| D8 | Three plans here. Two follow-ups are listed, not written: plan 4 moves in-region reads to the box path after phase 3 (ruling 5); plan 5 moves this traffic to a per-region bucket (ruling 1). | §14 |

## 5. Transport and authentication

### 5.1 Why this transport

Three candidates exist in the tree:

- **Deployment-bucket objects** (`carlos run`, restart scope, the offline
  move's stager and sealer reports). Established, provider-neutral, works
  for a box with no inbound path, and already crosses regions.
- **Edge backhaul** (`internal/edge/homemap.go`, `mesh.go`). A box-to-box
  HTTPS hop for tenant traffic. Its HMAC key proves only "a box in this
  deployment" and is deliberately allowed to buy nothing but
  `X-Forwarded-For` belief. Using it for db reads would give that key a new
  authority across the IAM region boundary, and the console would start
  connecting to boxes, which the run spec ruled out.
- **The fleet store door** (`internal/edge/storedoor.go`). Serves bucket
  objects to fleet boxes on the tunnel hostname. Wrong direction, wrong
  principal.

Decision: deployment-bucket objects. The cost is latency (a pickup is
seconds, not milliseconds) and a LIST per serial move on capable boxes
(§5.8). Both are acceptable for a command a person runs by hand.

### 5.2 One read, one request

Rounds 1 to 3 kept finding defects in one place: rev 1 to rev 3 made a read
a durable object with a life of its own. It could be followed, cancelled,
expired or declared lost by the console after the fact, and its admission
slots outlived the request that took them. Every rule added to manage that
life made a new edge (codex 3, 4, 5, 7, 13, 14, 16, 17). Rev 4 removes the
life: **a forwarded read is one synchronous HTTP request.** The CLI's
request to the console stays open for the whole read, the console waits on
the box's objects inside that request, and when the request ends the read
is over for everyone. Nothing resumes it.

**Why this is possible (the numbers).** Nothing between the CLI and the
console cuts a long request:

- **The edge.** The public listeners set `ReadHeaderTimeout` only, and say
  why: "No ReadTimeout/WriteTimeout, those would cut long-lived streams"
  (`internal/edge/edge.go:3354` to `3358`). The reverse proxies to a unix
  or TCP upstream, which is how the console is reached, set no
  `ResponseHeaderTimeout` and no overall timeout, only a 90 s idle bound on
  pooled connections (`unixProxy`, `tcpProxy`, `edge.go:2377` to `2421`).
- **The console.** `cmd/console/main.go:586` to `590` sets
  `ReadHeaderTimeout` 10 s and `ReadTimeout` 5 minutes, and no
  `WriteTimeout`. `ReadTimeout` bounds reading the request; the read door
  reads a body of a few kilobytes and then, as the import door does
  (`internal/console/apiinstanceimport.go:479` to `483`), sets its own read
  deadline through `http.ResponseController` so the connection is held for
  the read's whole bound and no longer.
- **The CLI.** The ordinary console client has a 30 s timeout
  (`auth.go:403`), which is why `carlos ship` already uses its own context
  of 15 minutes (`apimode.go:4425`). The forwarded read does the same: its
  own client with no overall timeout and a context of 23 minutes.
- **Streaming precedent.** The db door already streams a long response with
  a per-chunk write deadline and an idle watchdog (`handleAPIDBGet`,
  `dbStream`, `internal/console/apidb.go`).

The longest a read can hold the request is the pickup window (2 minutes
with skew) plus the owner's deadline (20 minutes), so 22 minutes, and the
CLI's 23-minute context is the outer bound. To keep an idle connection
alive through home routers and NATs that drop silent TCP, the console
writes and flushes a progress line at least every 15 s (§5.5, codex 19).

### 5.3 The invariants

| # | Invariant |
|---|---|
| **J1 One request** | A read exists for the console and the CLI only inside one open HTTP request. When the request ends, by a result, a timeout, a cancel or a dropped connection, the read is over: no route can follow, resume, cancel or end it later, and the console writes nothing about it afterwards except one best-effort `cancel`. |
| **J2 Write once** | Every object is a conditional create (`If-None-Match: *`, `release.Store.Put` with an empty `ifMatch`) and is never replaced or deleted by the protocol. The console writes `request.json`, the queue entry and `cancel`; boxes write `picked`, chunks and `result.json`. |
| **J3 One owner** | The first create of `picked` owns the read. A box whose create answers an error or a conflict reads `picked` back and owns the read only if it carries this box's own attempt nonce. Only the owner runs the read. |
| **J4 The store's lifecycle rule is the only deletion** | A rule on the `db-reads/` prefix expires every object. It acts long after J1 has ended every reader, so it never races one. |
| **J5 Admission is held by what does the work** | The console's per-person and per-console counts are held by the open request and released when its handler returns. A box's read slot and disk reservation are held until its worker unit has stopped. |

What closes each open or earlier finding:

| Finding | Closed by |
|---|---|
| codex 3 (progress ack read as the end) | removed by the synchronous design: there are no acks |
| codex 4 (late writes after `lost`) | removed by the synchronous design and J1: the console never writes `lost`; a box's late chunks or result are objects nobody reads, and J4 removes them |
| codex 5 (cleanup reopens the election) | J2, J4: `picked` is never deleted by the protocol, and the request is refused on age long before the lifecycle rule acts |
| codex 7 (stored ciphertext) | J4, with the honest timing in §5.7 |
| codex 8 (disk) | J5 |
| codex 13 (completion deletes the evidence a read ended) | removed by the synchronous design: nothing needs that evidence after the request ends |
| codex 14 (cleanup removes the identity to follow) | removed by the synchronous design: there is no follow |
| codex 15 (the db gate on follow routes) | removed by the synchronous design: there are no follow routes. The one route runs the full gate once, at the start, and a `record-changed` result comes back in the same response |
| codex 16 (lazy expiry stranded between two writes) | removed by the synchronous design (the console never writes `picked` or a result), and J3 for the box's own ambiguous `picked` |
| codex 17 (abandoned reads hold admission) | J5: an abandoned read is a closed connection, and the handler's return releases its counts |
| codex 18 (the forgery claim contradicts sealing) | withdrawn: §5.6 states the real trust boundary |

### 5.4 Objects

All objects live under `db-reads/`, so one lifecycle rule covers them (J4).
`<id>` is `dbr_` and 26 lowercase base32 characters (128 random bits),
minted the way `runs.MintID` mints run ids, with its own `ValidID`. Every
object is create-only (J2).

| Key | Writer | Purpose |
|---|---|---|
| `db-reads/<id>/request.json` | console | The request: a root envelope (§5.5). |
| `db-reads/queue/<region>/<YYYYMMDDHH>/<unix>-<id>` | console | An empty pointer, written after the request. Boxes of `<region>` list the current and previous hour's prefix and skip any entry whose `<unix>` (the request's `issued_at`) is outside the pickup window without reading it. |
| `db-reads/<id>/picked` | a box | The owner (J3): box name, boot id, a random attempt nonce and the owner's deadline. |
| `db-reads/<id>/out/<seq>` | the owner | One sealed chunk of output (§5.6). |
| `db-reads/<id>/result.json` | the owner | How the read ended. Written after every chunk is confirmed. |
| `db-reads/<id>/cancel` | console | Best effort, after the request ended without a result, so the owner stops early. Never read by the console. |

Rev 2 kept these under `apps/<acct>/<app>/` for app purge; one prefix is
what lets one rule expire everything, and the rule is what removes them.

There is no write to the instance record. A run takes a record CAS because
it needs the instance to itself. A db read touches neither the instance nor
its lease, needs no exclusivity, and is frequent.

**Ambiguous creates on the box.** A box whose `picked` create answers an
error or a conflict reads `picked` back: its own nonce means it owns the
read, anything else means it steps away, and a read that still fails is
treated as not owned (J3, codex 16). The box keeps each sealed chunk's
bytes until its create is confirmed; a retry that answers "already exists"
reads the object back and compares bytes, equal meaning its earlier attempt
landed. It writes `result.json` only when every chunk is confirmed. A chunk
it cannot confirm before its deadline is never named: the box stops the
read and writes a `failed` result with `interrupted` whose `last_seq` is
the last confirmed chunk, so the result names only objects that exist
**(plan review 9)**.

### 5.5 The request and its response

The envelope is unchanged from rev 3:

```json
{
  "purpose": "carlos-db-read-v1",
  "v": 1,
  "id": "dbr_...",
  "account": "<sqid>",
  "app": "shop",
  "host": "shop.example.com",
  "incarnation": "<hostclaim.Incarnation of the record>",
  "region": "uk",
  "op": "query",
  "sql": "SELECT count(*) FROM orders",
  "at": "2026-10-07T14:00:00Z",
  "reply_key": "<base64 P-256 public key, uncompressed>",
  "by": "member@example.com",
  "issued_at": "2026-10-22T09:00:00Z"
}
```

- Signed with `release.SignRootEnvelope` under the console's root key. A
  console that holds only a scoped key cannot sign, and so cannot forward:
  it answers with today's residency sentence.
- `purpose` is a fixed string so the envelope cannot be confused with any
  other root-signed payload. The box decodes it into the exact type with
  `VerifyRootEnvelope`, which requires canonical JSON.
- The box accepts an envelope only when `id` matches the key it was read
  from and the queue entry that named it, `region` matches the queue key and
  the box's own `CARLOS_REGION`, and `issued_at` is no more than 60 s old
  and no more than 60 s in the future.
- `at` is empty for the newest copy. `op` is `query` in phase 1; phase 2
  adds `pull`.
- The envelope carries no bucket, no prefix and no key. The box derives the
  replica prefix itself (§8.2).

**The sequence.**

1. The CLI calls the existing list door, as today. A new console with
   forwarding on answers a capable cross-region instance with 409 and an
   additive body: `{"error": DBF-14, "forward": 1, "region": "uk",
   "forward_ops": ["query"]}`. An old CLI prints the sentence. A new CLI
   switches to the forwarded form.
2. The CLI runs `checkQuerySQL` locally, makes an ephemeral P-256 key, and
   sends one request, `POST /api/cli/apps/{account}/{app}/db/{host}/reads`,
   with `op`, `sql`, `at` and `reply_key`.
3. The console runs `dbGate`'s order (person only, membership with a
   byte-identical 404, the db budget, configuration, the record, the class
   rail, pooled, fleet), refuses a moving record, checks `checkQuerySQL`
   again, checks capability (§7.1), and takes its admission counts (J5).
   Any refusal so far is an ordinary status and JSON error, and so is SQL
   holding U+FFFD, which is what JSON decoding makes of bytes that are not
   UTF-8 (the CLI refuses such SQL first, with DBF-32) **(plan review 36)**.
   Then it mints the id, writes the audit row (§9.3), creates the request
   and the queue entry, bumps the serial, and starts a `200` response of
   newline-delimited JSON. From here every outcome is a line in that body.
   The console checks that the request is still open immediately before
   each of those four writes, and the serial bump runs on the request's own
   context. A request that ends before `request.json` leaves at most the
   audit row; one that ends after it leaves `request.json`, perhaps the
   queue entry, and `cancel`, and never a serial bump or a `200`
   **(plan review 11, 28, 33)**.
4. Each capable box lists its region's queue for this hour and the last
   when the serial moves, and every 30 s if it does not. For an in-window
   entry it has not decided, it reads and verifies the request, skips it if
   `cancel` exists **(plan review 11)**, skips it while the box is draining
   (§7.1), checks its own admission (§5.7), waits a random 0 to 500 ms,
   checks draining again, and creates `picked`.
5. The owner runs §8.2's identity bracket around planning and download,
   runs the query in a worker, and writes sealed chunks as the output
   arrives. It checks for `cancel` every 5 s. When the worker ends, it
   confirms every chunk and creates `result.json`.
6. The console, inside the same request, reads `picked` until it appears.
   Then, about once a second, it reads `result.json` **first** and lists
   `out/` **second**, the order the run's follow door uses for the same
   reason (`internal/console/apirunsfollow.go:101` to `106`): a listing
   taken before the result could miss a chunk the box confirmed just
   before writing it **(codex 20)**. It writes to the response, in order:
   `{"admitted": <id>}` at once, `{"picked": <box>}` once, `{"chunk":
   <seq>, "data": <base64 sealed bytes>}` for each chunk in sequence order
   with no gaps, `{"wait": <phase>}` whenever 15 s pass with nothing else
   to send, and finally `{"result": <result.json>}`.
   - **The result line comes last, and only when complete (codex 20).** It
     is written only after every chunk from 1 to the result's open
     `last_seq` has been written to the response. A result whose chunks are
     not all listed yet is held, and the console keeps listing once a
     second until they are, or until the request's own deadline ends it
     with DBF-12. The console cannot open the sealed summary; the CLI still
     checks the bytes against it (step 8).
   - **Every line is flushed (codex 19).** Each line is written under a
     fresh write deadline and then pushed out with
     `http.ResponseController.Flush`, under the same deadline; a write or
     flush error ends the request as a stalled client (step 7). Go buffers
     small writes in the response writer, and `dbStream` flushes only at the
     end, so without an explicit flush the wait lines would sit in the
     console for minutes. The response's first line, `admitted`, is flushed
     with the status and headers, so the headers leave at once.
   - **Nothing downstream buffers it.** The response is
     `Content-Type: application/x-ndjson` with `Cache-Control: no-store`
     and no `Content-Length`. The edge's compressor does not gzip that
     type (`compressibleType`, `internal/edge/compress.go:128`), and its
     gzip writer passes `Flush` through when it does compress
     (`compress.go:319`). The edge's reverse proxy flushes a response with
     no `Content-Length` immediately (`httputil.ReverseProxy`), and
     `no-store` keeps the cache recorder out.
   - **Backhaul.** A request that reaches the console through another
     region's edge travels over backhaul, whose transport waits at most 30 s
     for response headers (`backhaulHeaderTimeout`,
     `internal/edge/edge.go:2827`, used at `2958`). So the console bounds
     everything before the `200` (the gate, the record read, the capability
     read and the two creates) with a 20 s context, refusing with today's
     "could not read the instance. Try again." on overrun, and flushes the
     `200` and `admitted` the moment the creates land. After the headers,
     backhaul sets no further timeout.
7. The console ends the response **without** a result line, and the CLI
   says DBF-3 or DBF-12, when:
   - no `picked` has appeared 2 minutes after `issued_at` (DBF-3);
   - the owner's deadline plus 1 minute passes with no result (DBF-12);
   - the CLI's connection closes, or a write to it stalls for 30 s.
   In each case the console then creates `cancel`, best effort, and
   returns. Its admission counts are released by the return (J5).
8. The CLI buffers every chunk (**codex 6**). On a `done` result it opens
   the sealed summary, checks the buffered bytes against its count and
   SHA-256, and only then writes the rows to stdout and today's status
   line to stderr. Any other result maps to its sentence (§10). A response
   that ends without a result prints nothing to stdout.

**Ctrl-C** closes the connection. The console sees its context end, creates
`cancel` and returns; the owner, if any, stops within 5 s. There is no
cancel route.

**A console restart** ends every read in flight: their connections close,
and each CLI says DBF-12. Nothing on the console side needs recovering,
because nothing outlived the request (J1). A box keeps running a read whose
console went away until it sees `cancel` or its own deadline; its output is
read by nobody and expires (J4).

### 5.6 Sealing, the result and the trust boundary

- One ephemeral P-256 key pair per read, made by the CLI and held only in
  its memory. The public half travels in the request.
- Each chunk is sealed with the appconfig v2 construction (ECDH P-256,
  HKDF-SHA256, AES-256-GCM, a fresh ephemeral sender key per seal), with its
  own HKDF info string, `carlos-db-read-v1`, and associated data binding the
  read id, the chunk's sequence number, its stream and whether it is the
  last chunk. A chunk moved to another read, reordered, or relabelled as
  last fails its tag. The result's sealed summary carries the plaintext
  byte count and SHA-256, so a dropped or added chunk fails the check.
- **What this buys.** The deployment bucket and the console only ever hold
  the rows as ciphertext. Nobody without the CLI's private key can read
  them, including whoever holds the bucket. A replayed envelope produces
  output only the original CLI could open, and that CLI is gone (J1).
- **What it does not buy, plainly (codex 18).** Sealing proves who can
  *read* an answer, not who *wrote* it: the recipient's key is public, and
  each seal uses a fresh sender key, so no seal carries an author. Any
  party that can write the deployment bucket, which is every platform box
  (`infra/modules/region-host/main.tf`, `host_buckets`), could create
  `picked`, chunks and a matching `result.json` for a read before the real
  box does, and the CLI would accept that answer as consistent. The CLI's
  checks prove the answer is complete and unaltered since it was sealed;
  they do not prove which box sealed it. Rev 3's claim that a non-owner
  "cannot change an answer the CLI shows" was wrong and is withdrawn. This
  is the same trust every box already has over `carlos run` output and over
  everything else in the deployment bucket, and the in-region door trusts
  the bucket the same way. Authenticated authorship would need per-box
  signing keys, which exist only in claim mode; §6 leaves claim mode alone.

```json
{
  "state": "done",
  "code": "",
  "box": "carlos-flagship-uk",
  "last_seq": 12,
  "started": "...",
  "ended": "...",
  "sealed": "<sealed summary>"
}
```

`state` is `done`, `failed`, `refused` or `cancelled`. `code` is a fixed
vocabulary the console may read: `nothing-replicated`, `history-gone`,
`check-failed`, `changed-twice`, `too-large`, `no-room`, `sql-error`,
`timeout`, `output-cap`, `record-changed`, `moving`, `interrupted`. The
sealed summary carries the transaction id and time of the copy, the oldest
time for `history-gone`, the restored size and cap for `too-large`,
SQLite's own error text for `sql-error`, and the byte count and SHA-256 of
the output. The CLI maps codes to today's sentences where one exists
(`dbRestoreError`), and to §10's rows otherwise.

### 5.7 Limits and storage

| Limit | Value | Enforced by |
|---|---|---|
| Pickup window | 60 s from `issued_at`, plus 60 s of skew | box (refuses older envelopes); console stops waiting after 2 minutes |
| Planning and restore | 10 min | worker unit and agent context |
| Query | 5 min wall clock | worker |
| Owner deadline | 20 min from `picked`, stated in it | agent context, worker `RuntimeMaxSec`; console stops waiting 1 minute after it |
| The request | at most 22 minutes; the CLI's own context is 23 | console, CLI |
| Restored size | 2 GiB by default, `CARLOS_DB_READ_MAX_BYTES` on the box | agent, from the plan, before any byte moves |
| Disk | a reservation per read (below) | agent |
| Output | 64 MiB (`runs.MaxOutputBytes`, written "64 MB" in copy as the run's docs do); the read ends `output-cap` | agent; the CLI's buffer is bounded by the same number |
| Chunk | 256 KiB or 1 s, whichever comes first (`runs.ChunkBytes`, `runs.ChunkEvery`) | agent |
| Pull | at most 2 GiB of planned LTX files (measured on dev, 2026-10-23: relay rate 3.94 MB/s, the slowest of three runs; the rule gives 3.25 GiB, above the 2 GiB ceiling); `CARLOS_DB_PULL_MAX_BYTES` on a box can only lower it; the read ends `too-large` | agent, from the plan, before any byte moves; the CLI's spool is bounded by the same number |
| Pull chunk | 1 MiB or 1 s, whichever comes first | agent |
| Reads per box | 2, plus one connection kept for the readiness probe (`MaxConnections=3`) | agent, systemd |
| Open reads per person | 2 | console, held by the open request (J5) |
| Open reads per console | 16 | console, held by the open request, 429 with `Retry-After` |
| Request budget | the db door's existing 600 per token per minute | console |

**Admission (codex 17).** The console's counts are taken in step 3 and
released by a `defer` in the same handler, so they are exactly the reads
whose requests are open. An abandoned read is a closed connection; its
handler returns within 30 s (the write stall bound) or at its next wait
line, and the slot is free. No timer, sweep or restart is involved.

**Stored ciphertext (codex 7).** The protocol deletes nothing. Every object
of a read stays in the deployment bucket until the lifecycle rule removes
it, and that timing is the store's, not ours. On S3 the rule (current
versions expire 1 day after creation, noncurrent versions 1 day after they
become noncurrent, and expired delete markers removed) makes an object
eligible for expiry about a day after creation, with eligibility rounded to
the next midnight UTC, and the noncurrent version eligible a day after
that. S3 then removes eligible objects asynchronously, usually within a day
or two, with no hard deadline. So the honest statement is: **a read's
ciphertext lives until lifecycle expiry, typically two to four days after
the read, with no guaranteed upper bound.** The size per read is the
plaintext limit plus sealing overhead **(codex 7)**: the output cap limits
the rows to 64 MiB of plaintext in phase 1; each chunk object stores its
sealed envelope as raw bytes (not base64), which adds under 100 bytes per
256 KiB chunk (a 65-byte sender key, a 12-byte nonce, a 16-byte tag and a
short prefix), so a full answer stores about 64.03 MiB of ciphertext. The
request, `picked`, `result.json`, the queue entry and `cancel` add a few
kilobytes. Base64 appears only on the wire to the CLI. Plaintext rows are
never in the bucket. A deployment that needs a hard deadline needs a different
reclamation mechanism (version-aware deletes), which this design does not
have.

**Disk reservations (codex 8).** The agent is the only process that starts
workers, so it keeps one reservation table under one mutex. Before `picked`
it reserves the floor (5 GiB); after planning it grows that reservation to
the plan's LTX bytes plus its restored size. Each step succeeds only if the
filesystem holding `/var/tmp` keeps at least 5 GiB and 10% free after
subtracting every other open reservation; otherwise the read ends `no-room`.
A reservation is released only when its worker **unit** has stopped, since
systemd removes the unit's private `/var/tmp` at stop. The agent learns
that from `systemctl list-units 'carlos-dbread@*'` (readable without
privilege). It admits a new read, or a probe, only when the number of
active, activating or deactivating `carlos-dbread@` units is no more than
the number of workers it is itself running. Any other unit is an orphan
whose scratch is not in the table, so the agent refuses work until it is
gone. There is no timeout escape: an orphan worker stops within 5 s of
losing its connection (§8.3), and `RuntimeMaxSec` stops it in the end
regardless.

A read's one retry (a planned file that vanished, §8.2) is a second
worker. Its first attempt's reservation stays in the table, under its own
key, until that attempt's unit has stopped, and the retry dials only once
the unit count says so; the retry takes a fresh reservation from the floor.
Closing the first connection is not the unit's stop **(plan review 10)**.

**Restart reconciliation (codex 8).** A restarted agent's table is empty,
and it cannot know the sizes of workers it did not start. So it reconciles
by refusal, against the units actually running: at start, and before every
admission, it lists `carlos-dbread@` units, and while any is active,
activating or deactivating that it is not itself running, it takes no read
and no probe. It never guesses a size for an orphan and never admits beside
one. Its first admission therefore happens only after every unit from
before the restart has stopped and systemd has removed its scratch.

### 5.8 Cost

Each capable box does one LIST of its region's current and previous hour of
queue entries per serial move and every 30 s, and two GETs per new
in-window entry (the request, and `cancel` before electing, plan review 11).
A read adds two console PUTs, one `picked`, one PUT per chunk and one
result, and the console's own GETs and LISTs about once a second while the
request is open. There are no deletes. The October bill was LIST and GET
polling, so plan 1 measures the LIST rate on dev before the flagship roll
and reports it.

### 5.9 Replay and forgery

| Attack | What stops it |
|---|---|
| A box writes its own request | No root signature: refused and logged once. |
| A box writes its own queue entry for a real request | The box reads the request and requires the queue entry's region, hour and time to match the envelope. |
| An envelope copied to another id or region | The id and region are inside the signature and must match the keys. |
| The same envelope picked again | `picked` exists until lifecycle expiry, days after the 60 s window has closed (J2, J4); a second create fails on every box, before or after a restart **(codex 5)**. Past the window the envelope is refused on age. |
| A box serves a read for another region | It refuses any envelope whose region is not its own `CARLOS_REGION`, and IAM refuses the bucket anyway. |
| A request that names a bucket or prefix | There is no such field. The box derives the prefix (§8.2). |
| A compromised box forges an answer | Not prevented. §5.6 states the boundary **(codex 18)**. |

## 6. Which operations forward

| Operation | Phase | Reason |
|---|---|---|
| `query` | 1 | This is the regression the customer hit. The result is rows, usually small, and only rows leave the region. |
| `query --at` | 1 | Same path with a target time. The box lists the replica the way the console's door does today (`listReplicaLevel`, `LastModified` as `CreatedAt`), so the copy stops at the same transaction an in-region read would. Plan 1 tests that parity. |
| `pull` | 2 | The result is the whole database. It rests on ruling 1 (§16, Paul, 2026-10-22: a whole copy's ciphertext rests in the deployment bucket's region until lifecycle expiry, typically days, until plan 5's per-region bucket), and a measured size cap. In phase 2 the box ships the planned LTX files, sealed, in the same single request, and the CLI restores and checks them locally with today's code. The box never restores for a pull. |
| `pull --at` | 2 | As `pull`, with the target time. |
| Claim-mode door (`apidbclaim.go`) | not planned | Claims are not active anywhere. The claim door reads exact object versions through `hostops`'s versioned boundary and signed file handles, and lists no times. A box-side version of that belongs to claim activation (#250), which must add times anyway. A host-claim console keeps today's residency refusal, as it refuses `carlos run`. |
| Seeds `--from` (`resolveDBSource`) | not planned | It shares the source checks but restores into a new instance, a different flow. Cross-region seeding is its own design. |
| Fleet and pooled instances | refused, as today | Their data is not in a platform region bucket. |

Phase 1 reaches every cross-region instance a member can read today in its
own region, for queries. Until phase 2, `pull` answers DBF-8 for a
cross-region instance.

## 7. Who serves

### 7.1 Capability and selection

A box publishes `db-read-1` in its edges file only when the agent's armed
db-reads loop says so, the way `offline-move-1` comes from the armed moves
worker (`armedOfflineView`). The loop answers yes only when all of these
hold:

- hibernation is configured (`CARLOS_HIBERNATE_BUCKET` set);
- `CARLOS_REGION` is set;
- `CARLOS_RELEASE_PUBKEY` is set, so it can verify envelopes;
- `CARLOS_DB_READS` is not `0` (an operator off switch for one box);
- `CARLOS_HOST_CLAIM` is not set;
- the loop is running, and its last **readiness probe** passed **(codex 9)**.

The readiness probe connects to the worker socket and runs the worker
protocol's `hello` exchange, which starts a real `carlos-dbread@` instance
and gets back the worker's protocol version and its view of its own
sandbox (no network interface but loopback, `/data` empty, `/etc/carlos`
unreadable). The loop probes at start and every 5 minutes, and after any
read whose worker failed to start. A failed probe withdraws the capability
on the next edges-file pass. A socket file whose service is missing,
masked, broken or the wrong version therefore never advertises anything.

**A probe never competes with reads (codex 9).** The socket allows three
connections (`MaxConnections=3`): the agent runs at most two reads and at
most one probe, so a probe always has a connection and a busy box is never
mistaken for a broken one. The agent counts its own connections, so it
never opens a fourth. A probe is skipped, not failed, while orphan workers
block admission (§5.7); the last result stands until it can run. When a
read's worker fails to start, the agent probes as soon as that connection
is closed. Rev 2 shared two connections between reads and probes, so two
reads at once made the probe fail and withdrew the capability.

The console reads `edgesfile.ReadLiveSnapshot` before admitting a read and
refuses up front only on complete evidence **(codex 10)**. That function
returns a nil error when status snapshots cannot be loaded and reports the
gap through `Complete` and `BoxesComplete` instead, so the rule reads those
fields, never the error alone. Incomplete evidence comes first and wins
over both refusals:

1. `Complete` is false, or `BoxesComplete` is false (an unreadable edges
   file, an unreadable or missing status snapshot, a fresh status snapshot
   with no edges file): the request is written and the pickup window
   decides. A person is waiting, and the console stops waiting after 2
   minutes (DBF-3).
2. Both are true, and no file in the record's region publishes `db-read-1`
   on a non-draining box: DBF-1, no request written.
3. Both are true, capable non-draining boxes exist, and none is `Fresh`:
   DBF-2, no request written.

Rev 2 checked `Complete` alone before DBF-1, so a fresh box whose edges
file was missing could be refused as "no server can read databases".

Any capable, live, non-draining box in the region may serve. There is no
assignment by the console. Region HA (plan 2b, dormant) can put several
boxes in a region, and they all read the same region bucket, so the
create-only `picked` marker is the whole election. The 0 to 500 ms jitter
spreads the race, and a box that fails admission (§5.7: no free read, the
disk floor, or an orphan worker) does not enter it.

### 7.2 Why not the lease holder

A sleeping instance holds no lease, so there is no holder to ask. An awake
instance's holder is busy serving it, and the read does not touch the
instance, so there is nothing to gain by sending it there. Choosing by lease
would also tie the read to the instance's lifecycle, which is exactly what
"never the live database" rules out.

### 7.3 When no box serves

| Situation | Answer |
|---|---|
| No capable box in the region | DBF-1 at once |
| Capable boxes, none live | DBF-2 at once |
| Live but nobody picks in the window (all busy, all failing admission, agent stuck) | DBF-3, when the console stops waiting 2 minutes after the request |
| The owning box dies or restarts mid-read | DBF-12, when the console stops waiting 1 minute after the owner's deadline, up to 22 minutes after the request (§5.5) |

## 8. Read-only and isolation on the box

### 8.1 The split

| Half | Runs as | Does | Never does |
|---|---|---|---|
| Agent (`carlos agent`, `carlos-edge.service`) | `carlos` | lists requests; verifies envelopes; creates `picked`; reads the record; derives the prefix; lists the replica and plans the restore from the listing; reads the last planned file's 100-byte header for the size check; streams planned files from the region bucket into the worker; re-checks the record before the query runs; seals and writes output; writes the result; keeps the disk reservation table | opens SQLite, decodes LTX pages, runs SQL, writes files on disk, reads anything under `/data/instances`, takes or reads the lease |
| Worker (`carlos system db-worker`, `carlos-dbread@.service`) | a `DynamicUser` | receives the plan and the files over its socket; writes them into its private `/var/tmp` as a litestream file replica; restores to the agent's target transaction; runs `lease.CheckDBContext`; runs the one statement with today's guards; streams rows back | anything with a network, the box's files, the bucket, other tenants' files |

The rule is: everything that decodes page data or SQL runs in the worker.
The member's SQL, the replica's pages and the restored database all do. The
agent does object input and output and planning from listings, which it
already does to wake instances. Its one decode is the last planned file's
fixed 100-byte LTX header, for the size check, with the bounds
`dbdoor.PlanRestore` already applies (a valid page size, a non-zero commit,
TXIDs that match the listing).

### 8.2 The identity bracket

The replica prefix is keyed by host alone (`activator.HibernatePrefix`), so
a host deleted and recreated, in the same account or another, reuses it.
Checking the record once before the read would let a request admitted for
the old instance read the new one's replica **(codex 1)**. So the agent
brackets every byte it reads with two identical checks, and no output
exists until the second one passes.

**The check.** The agent reads the record fresh from the bucket and refuses
(result `refused`) unless:

- the record exists and its incarnation is the envelope's (`record-changed`);
- the class rail passes (`dbClassRefusal`, moved where both console and box
  can call it);
- it is not pooled and not a fleet placement;
- `rec.Region` equals the box's `CARLOS_REGION` (`record-changed`);
- `rec.Move` is nil (`moving`);
- `rec.Host` is the envelope's host and passes `provision.ValidHostShape`.

**The order.**

1. Check A, then plan from the listing.
2. Stream every planned file into the worker. The worker restores and runs
   `lease.CheckDBContext`, then answers `ready` with the transaction it holds. It
   runs no SQL yet.
3. Check B, the same check again, after the last byte has been read from
   the bucket.
4. Only if check B passes does the agent tell the worker `run`. The first
   output chunk can exist only after this point.

A failed check B ends the read `record-changed`, stops the worker and writes
no chunk. A retried plan (today's "changed while it was being read" retry)
runs both checks again around its own download.

**Why two checks are enough.** The incarnation is
`hostclaim.Incarnation(account, app, host, CreatedAt)`. A deleted and
recreated host, including one recreated in another account, gets a new
incarnation, and an incarnation never comes back once its record is gone.
So a record that shows the same incarnation at A and at B was the same
instance for the whole download, and the files read in between were that
instance's. Pinning object versions would add nothing that this does not
already give in non-claim mode (§18).

The replica prefix is `activator.HibernatePrefix(<the box's own
CARLOS_HIBERNATE_PREFIX>, rec.Host)` in the box's own
`CARLOS_HIBERNATE_BUCKET`, read through `lease.ReadReplicaClient` and held
only as the narrow `replicaReader` interface (`LTXFiles`, `OpenLTXFile`), so
no write method is reachable. Nothing in the request chooses a bucket, a
prefix, a key or a level beyond what the plan picks.

The agent lists only level directories. It never reads `lock.json`, never
calls the leaser, and never wakes the instance.

### 8.3 The worker unit

`carlos-dbread.socket`, bound by root systemd:

- `ListenStream=/run/carlos-dbread/worker.sock`, `SocketUser=carlos`,
  `SocketMode=0600`, `Accept=yes`, `MaxConnections=3` (two reads and one
  probe, §7.1, codex 9).

`carlos-dbread@.service`, one instance per connection, starting from the
claim-bound template's hardening (§3 fact 8) with these differences:

- `StandardInput=socket` and `StandardOutput=socket`, so the accepted
  connection is the worker's stdin and stdout, and `StandardError=journal`
  for its own diagnostics **(codex 2)**. With `Accept=yes` systemd passes
  the connection as a file descriptor and does not put it on stdin unless
  told to; the claim-bound template's `StandardOutput=file:` line is not
  copied. All protocol traffic, both ways, is on that one connection;

- `DynamicUser=yes` instead of `User=carlos`, so the worker owns no file on
  the box and can read nothing `carlos` owns;
- `PrivateNetwork=yes` and `RestrictAddressFamilies=AF_UNIX`, so no route to
  the metadata service, the bucket or anything else exists, rather than a
  deny list;
- `TemporaryFileSystem=/data:ro /var/lib:ro /opt/carlos:ro /run/carlos:ro`
  with `BindReadOnlyPaths=/opt/carlos/carlos`, and
  `InaccessiblePaths=/etc/carlos /run/carlos-control`;
- `PrivateTmp=yes`: its `/tmp` and `/var/tmp` are its own. The worker writes
  only under `/var/tmp`, which is on disk on Debian 13, where `/tmp` is a
  tmpfs. systemd deletes both when the unit stops, however it stops;
- `MemoryMax=512M`, `CPUQuota=100%`, `TasksMax=16`, `LimitFSIZE` set to
  the restored-size cap (it bounds each file; the agent's reservation in
  §5.7 bounds the total, codex 8), `RuntimeMaxSec=20min`;
- `SystemCallFilter=@system-service`, `NoNewPrivileges=yes`, empty
  `CapabilityBoundingSet`, `ProtectProc=invisible`.

`carlos system db-worker` refuses to run unless its stdin is a connected
unix stream socket (checked with `getsockopt(SO_TYPE)` and `SO_PEERCRED`,
whose peer must be the `carlos` user), so it cannot be started by hand or
pointed at a file. Plan 1's acceptance test drives it only through the
installed units.

The worker protocol is length-framed messages with a fixed maximum frame
size: `hello` (the readiness probe, §7.1), a header naming the plan's files,
their sizes, the target transaction and the byte limits, then the files,
then `ready` from the worker, then `run` or `stop` from the agent, then
framed output and a final status. The worker refuses a file larger than its
header said and any write past the header's totals.

**The worker stops when its agent goes (codex 8).** From the moment it
starts, the worker keeps one goroutine reading its connection. EOF or an
error there cancels the one context that restore, `lease.CheckDBContext`
(`internal/lease/check.go:44`, never `CheckDB`, which supplies a background
context) and the query all run under, and the worker exits. Not every
step honours a context promptly: litestream's file replica client opens a
plain file without checking it
(`litestream@v0.5.14/file/replica_client.go:135`), and the LTX compactor's
page reads and writes ignore it (`ltx@v0.5.1/compactor.go:177`, `199`). So
the watcher does not rely on cancellation alone: after cancelling, it
waits 5 s for the worker to finish, and if it has not, it ends the process
itself with `os.Exit` **(codex 8)**. A process exit stops a compaction
wherever it is. A worker blocked in a write to a dead connection gets an
error at once. So an
agent restart ends every worker it had within seconds, systemd stops each
unit and removes its private `/var/tmp`, and only then does the restarted
agent's orphan count (§5.7) reach zero and admit work. While the agent
reads the output, it writes nothing to the worker, so the watcher sees
only EOF or the agent's own `stop`.

The socket and service files are written and enabled by
`ensureSystemUnits`, the same root path that installs the other units, on
the next adopt pass or `update-platform-binary.sh`.

Within the worker, the query keeps every guard the CLI has: `mode=ro`,
`query_only`, `temp_store(2)`, `SQLITE_LIMIT_ATTACHED` 0 and
`checkQuerySQL`. The database is opened with `immutable=1` as well, since
nothing else ever writes the file. The guards and the query function move
from `db.go` into a shared package (`internal/dbquery`), so the CLI and the
worker run the same code and the output format cannot drift.

### 8.4 Why a tenant cannot reach another tenant's data

1. The console admits only a member of the account, for an instance of that
   account's app, and signs exactly that host into the envelope.
2. The box derives the prefix from that host, the fresh record and its own
   config, and nothing else.
3. The worker receives only the files the agent sent. It has no network, so
   no credentials from the metadata service and no bucket. Its filesystem
   view hides `/data`, `/etc/carlos` and `/run/carlos`, and its user owns
   nothing. ATTACH is off. If a SQLite flaw let the statement open a path,
   the sandbox leaves it nothing to open but its own scratch.
4. The output is sealed to the requesting CLI's key.

### 8.5 Why the live database is never touched

The agent reads bucket objects only. The worker cannot see `/data`. Nothing
calls the activator, the leaser or `lock.json`. A read of a busy instance's
replica can race litestream's compaction, which is today's "changed while it
was being read, twice" case, and is retried the same way.

## 9. Console and CLI behaviour

### 9.1 The one new console route

```
POST /api/cli/apps/{account}/{app}/db/{host}/reads
```

It is `svcrole.Set{}` (people only) and runs the db door's full gate once,
at the start (§5.5 step 3). A host-claim console answers it with today's
residency sentence. Rev 3's follow and cancel routes are gone (J1), and so
is rev 2's acknowledgement pair. Rev 3 needed a second, smaller gate for
follow calls so that a `record-changed` result could still be fetched after
the instance was deleted or reclassified (codex 15). With one request there
is nothing to fetch later: the full gate ran when the read began, and the
box's `record-changed` result arrives as the last line of the same
response.

### 9.2 Mixed versions

| CLI | Console | What happens |
|---|---|---|
| old | old | today's residency sentence |
| old | new, forwarding off or region not capable | today's sentence, or DBF-1 or DBF-2 |
| old | new, forwarding on and region capable | DBF-14: update carlos |
| new | old | today's sentence; no forward marker, so the CLI prints it as is |
| new | new | the forwarded read |

A console with `CARLOS_DB_FORWARD` unset answers byte-identically to main.

### 9.3 Audit

One `db-read` activity row at admission, in the run's manner, with the
detail DBF-21 or DBF-22. It is written before the request and the queue
entry, while the request is open, so a read a box can see always has its
row; a row the console cannot write refuses the read. A request that ends
after the row and before the `200` leaves the row of an admission, and at
most the request, the queue entry and `cancel`; never a serial bump
**(plan review 11)**. It records the SQL's length in bytes and the first
16 hex digits of its SHA-256, not its text (ruling 3). A member
who kept the statement can match it to the row; the row itself holds none
of the data a query's literals can carry. That differs from `carlos run`,
which records argv, and keeps the in-region rule that SQL never reaches the
audit log. The SQL itself stays in the request object, in the deployment
bucket, until the lifecycle rule expires it with the rest of the read, on
the timing §5.7 states: typically two to four days, with no hard bound.

## 10. Errors and copy

Every new user-facing sentence is below. Error and status strings follow
Paul's voice rule (ruling 4): lower case at the start and after
a full stop, no em dashes. Docs prose keeps the CLI page's own sentence
case. None of this is approved yet; the copy-review gate runs before any of
it is written into code.

| ID | Where | Text |
|---|---|---|
| DBF-1 | console 409, no capable box in the region | `%s keeps its data in region %s. no server in that region can read databases yet, so carlos db cannot read it for now.` |
| DBF-2 | console 409, capable boxes but none live | `%s keeps its data in region %s. no server in that region is answering right now. try again in a few minutes.` |
| DBF-3 | CLI, nobody picked the read in 60 s | `no server in region %s picked up the read within 60 seconds. try again in a few minutes.` |
| DBF-4 | CLI, `too-large` | `%s's database is %s once restored. carlos db can query at most %s in region %s.` |
| DBF-5 | CLI, `no-room` | `the server in region %s does not have room to restore %s's database right now. try again later.` |
| DBF-6 | CLI stderr, `output-cap` | `the result was cut off at 64 MB. narrow the query with WHERE or LIMIT and run it again.` |
| DBF-7 | CLI, `timeout` | `the query ran for 5 minutes and was stopped. narrow it and run it again.` |
| DBF-8 | CLI, `pull` before phase 2 | `%s keeps its data in region %s. carlos db pull cannot fetch a copy from there yet. carlos db query can read it.` |
| DBF-9 | console 429, open-read bounds | `too many database reads are open. wait a minute and try again.` |
| DBF-10 | console 409, record moving | `%s is moving between regions. read its database once the move has finished.` |
| DBF-11 | CLI stderr while waiting | `reading a copy of %s's database in region %s.` |
| DBF-12 | CLI, the response ended without a result (the box stopped or never finished, the console restarted, the connection dropped), or `interrupted` | `the read of %s's database in region %s did not finish, so nothing was printed. try again.` |
| DBF-13 | CLI, a chunk or summary fails to open or add up | `the answer from region %s did not check out, so carlos db showed none of it. try again.` |
| DBF-14 | console 409 with the forward marker, read by an old CLI | `%s keeps its data in region %s. this version of carlos cannot read it from there. run carlos update, then try again.` |
| DBF-15 | CLI, `record-changed` | `%s changed while the read was waiting. try again.` |
| DBF-16 | CLI, `moving` from the box | as DBF-10 |
| DBF-21 | audit detail, query | `forwarded a database query to region %s. the query was %d bytes, sha256 %s (cli)` |
| DBF-22 | audit detail, query with `--at` | `forwarded a database query to region %s, as of %s. the query was %d bytes, sha256 %s (cli)` |
| DBF-30 | `docs/site/cli.md`, `carlos db`, replacing the paragraph that starts "If the app has more than one instance" | see below |
| DBF-32 | CLI, a query that is not UTF-8 text or holds U+FFFD, refused before it is sent: JSON would rewrite it, and the box must run the bytes typed or none **(plan review 36)** | `this query cannot be sent to region %s, because it holds bytes that are not UTF-8 text or the character U+FFFD. fix the query and run it again.` |
| DBF-31 | `docs/site/cli.md`, `carlos system`, the worker | `carlos system db-worker runs on platform boxes only. systemd starts it for each forwarded database query. you never run it yourself.` |

DBF-13 is true as written only because the CLI buffers the whole answer and
checks it before writing anything (§5.5, codex 6).

DBF-30:

> If the app has more than one instance, name one with `--instance`. Only
> instances that sleep have a replica. An instance on your own fleet or in a
> pool cannot be read this way, and the command says which.
>
> When an instance keeps its data in another region than the console's,
> `carlos db query` runs in that region. A server there restores the copy,
> runs your query and sends back only the rows. The rows are encrypted for
> your terminal on the way, so nothing in between can read them, and
> nothing is printed until all of them have arrived and checked out. There,
> the copy can be at most 2 GB once restored, the query runs for at most 5
> minutes, and the rows can come to at most 64 MB. `carlos db pull` cannot
> fetch a copy from another region yet.

Unchanged and reused: today's status line (`dbQueryStatus`), the
`dbRestoreError` sentences for `nothing-replicated`, `history-gone`,
`check-failed` and `changed-twice`, SQLite's own error text for
`sql-error`, `stopped` for Ctrl-C, the guard refusals of `checkQuerySQL`,
and the residency sentence when forwarding is off.

DBF-17 to DBF-20 and DBF-23 to DBF-29 are left free for phase 2's pull rows.

## 11. Rollout

### 11.1 Switches and gates

- **Box:** `db-read-1` (§7.1). Published only while the armed loop's
  readiness probe passes. `CARLOS_DB_READS=0` withdraws one box.
- **Console:** `CARLOS_DB_FORWARD=1` turns forwarding on. Unset, every
  answer is byte-identical to main. The console also needs the root key.
- **Store:** the `db-reads/` lifecycle rule (J4) is in place on the
  deployment bucket before any console turns forwarding on. On the flagship
  that is a change to the `deployment-buckets` module, a release tag, a ref
  bump in carloku-infrastructure and a targeted apply. That is production
  infrastructure, so it needs Paul's scoped approval under AGENTS.md's rule
  before it is applied; plan 2 prepares the plan for that approval. A
  self-hosted deployment sets the same rule (MinIO calls it ILM); without
  it, `db-reads/` objects are never removed.
- **CLI:** none. The forward marker negotiates (§9.2).
- **Version floor:** none beyond the capability. A box that predates it
  publishes no capability and is never asked.

### 11.2 Mixed fleet

A region with one capable box and one old box works: only the capable box
lists the queue. A region with no capable box gets DBF-1. A box that loses
the capability mid-read finishes the read it holds, within its deadline,
and takes no new one.

### 11.3 Roll order

Paul's order (dev boxes, dev console, check, flagship boxes, flagship
console), with the lifecycle rule first on each deployment's bucket. Boxes
publish an idle capability. The dev console turns `CARLOS_DB_FORWARD=1` and
the check is a forwarded query from a dev console to a dev box in another
region, plus the LIST-rate measurement (§5.8) and a note of real database
sizes (ruling 2).

### 11.4 Tests

Acceptance is production-wired. Stand-ins are allowed in unit tests and
labelled so; they are not acceptance.

| # | Test | Wiring |
|---|---|---|
| T1 | A forwarded query end to end: console in region ie, agent in region uk, two MinIO buckets on one server with separate credentials. The console's credentials cannot read the uk bucket (a MinIO policy), so any console read of it fails the test. | real console server, real agent loop, real store; CI runs it with the worker reached through a test socket, labelled non-acceptance |
| T2 | The same on the root runner with the real `carlos-dbread.socket` and `carlos-dbread@.service` installed by the real `ensureSystemUnits`, so the socket is the worker's stdin and stdout as §8.3 says (codex 2) | acceptance gate for plan 1 |
| T3 | Sandbox witness on the root runner, from inside the worker: open `/data/registry.db`, `/etc/carlos/host.env` and another instance's directory; connect to `169.254.169.254:80` and to any TCP address; write outside `/var/tmp`; `ATTACH` and `VACUUM INTO` with the attach limit removed (a mutation); `load_extension`. Every one fails. The private tmp is gone after a normal end, a cancel and `kill -9` of the agent mid-read. | real units |
| T4 | Forgery and replay: unsigned, wrong key, scoped key, envelope moved to another id and to another region, a forged queue entry, the same envelope offered again before and after the agent restarts (`picked` holds, codex 5), a chunk sealed to another key, a chunk reordered, the last chunk dropped. And the boundary §5.6 states, as a documented negative: a writer that creates `picked`, chunks and a matching result first is accepted by the CLI (codex 18). | real verify and seal code, real store |
| T5 | Two capable boxes in one region: exactly one runs each read, across 1,000 reads with random jitter. A box whose `picked` create commits on the server but answers an error reads it back and runs the read; a box whose create answers an error when another box owns it steps away (codex 16). | real store conditional create, a store wrapper that loses answers |
| T6 | Parity: the forwarded query and the in-region query print byte-identical stdout for a fixture database (NULL, tabs, newlines, blobs, times). `--at` stops at the same transaction as the in-region door for the same replica and time, including a write in the same second as the target. | real restore, both paths |
| T7 | The long request through the real path: a read that holds the request for 21 minutes passes through the real edge and the real console server (with its 5-minute `ReadTimeout`) and returns its result; a CLI context of 23 minutes is the outer bound. During a quiet restore the real CLI receives the `admitted` line within a second and a wait line at least every 15 s, both through the local edge and through backhaul from another edge, whose 30 s header timeout the request survives (codex 19). | real edges, real console, real CLI binary |
| T8 | Mixed versions: the five rows of §9.2, with the forwarding-off 409 compared byte for byte against main. | real console |
| T9 | Record changes between request and pick, during planning, during download and between download and `run`: moved, recreated in the same account, recreated in another account, class changed. Each ends with its code, the code reaches the CLI in the same response (codex 15), and no chunk is ever written (codex 1). | real records, a store wrapper that pauses the download |
| T10 | Approved copy digests for every row in §10, in the `approved_copy_test.go` pattern. | |
| T11 | CLI buffering: a dropped or bad last chunk, a summary hash mismatch, and a response that ends without a result each leave stdout empty (codex 6). | real CLI binary through `carlosTestEnv` |
| T12 | Disk: two concurrent reads on one box whose free space fits one of them: one runs, the other ends `no-room`. An agent killed mid-read: the restarted agent takes no read and no probe while the old worker unit is active, its worker stops on EOF, and the restarted agent admits work only once the unit and its scratch are gone, without guessing a size for it. A disconnect during local compaction, after the download, still ends the worker within 5 s (codex 8). | real agent, real units, root runner |
| T13 | Readiness: a present socket with the service missing, masked, failing to start and speaking another protocol version each withdraws `db-read-1` within one probe and edges pass. A box running two reads keeps the capability, and its probe succeeds on the third connection (codex 9). | real units, root runner |
| T14 | Console evidence: status snapshots unreadable, one snapshot missing, an edges file unreadable, an empty edges listing beside a fresh status snapshot. Each admits the read; only complete evidence (both flags) gives DBF-1 or DBF-2 (codex 10). | real `ReadLiveSnapshot` over a real store |
| T15 | Admission (codex 17): two reads whose CLIs disconnect before pickup, and two whose CLIs disconnect after it, each free their person's and the console's count when the handler returns, without a console restart; a third read from the same person is then admitted. A console restart mid-read leaves nothing to recover. | real console |
| T16 | Late writes are harmless (codex 4, 13, 14): a box that keeps writing chunks and its result after the console's request has ended (by timeout and by disconnect) changes nothing any reader sees, and no protocol path issues a delete or a replacing write (a store wrapper fails the test on any `Delete` or non-create `Put` under `db-reads/`). | real console, real agent, a versioned MinIO bucket |
| T17 | The lifecycle rule: the module's rule covers `db-reads/` with current and noncurrent expiry and expired delete markers (a module test). | module test |
| T18 | The result overtaking the last chunk: a store wrapper that lets the box commit its last chunk and `result.json` between the console's listing and its result read, in both orders. The console never writes the result line before every chunk through `last_seq`, and the read succeeds (codex 20). | real console, real store |

Mutation checks that must turn a test red: drop envelope verification (T4);
take the host or prefix from anywhere but the record and config (T1 with a
second tenant); remove `PrivateNetwork` (T3); skip the summary hash check
(T11); skip check B (T9); add any protocol delete or replacing write (T16);
release the console's counts anywhere but the handler's return (T15); skip
the read-back of an ambiguous `picked` (T5); drop the orphan count (T12);
give probes no connection of their own (T13); remove the read-deadline
extension or any flush (T7); list chunks before reading the result, or
write the result line before its last chunk (T18); drop the worker's 5 s
exit (T12).

## 12. Provider boundary

- **Core:** the transport is `release.Store` objects, so it runs on MinIO and
  any S3-compatible store. Every protocol write is a conditional create,
  which `release.Store.Put` already supports with an empty `ifMatch`, on S3
  and on MinIO. The box reads each planned file with a GET and writes chunks
  with PUTs; it never uses a server-side copy. Signing is ed25519, sealing
  is the standard library plus `x/crypto/hkdf`. The sandbox is systemd,
  which every box already needs.
- **One store endpoint per box (codex 11).** Every S3 client in a box process
  takes its endpoint from the one `CARLOS_S3_ENDPOINT`
  (`internal/s3compat/s3compat.go`), the deployment store through
  `s3compat.Options` and the replica client through `lease`'s `s3Endpoint`.
  So a box reaches its region's data bucket and the deployment bucket on the
  same server, as it already must to wake and to move instances. This
  design supports exactly that shape: separate buckets per region, with
  separate credentials, on one endpoint (§18).
- **Expiry.** The lifecycle rule is S3 lifecycle configuration, which MinIO
  supports as ILM. It is a deployment requirement (§11.1), not something the
  code can detect, and there is no fallback deleter: the store's rule is the
  only deletion (J4).
- **AWS adapter:** IAM is what makes the console unable to read another
  region's bucket on the flagship. A self-hosted deployment gets the same
  split from per-bucket credentials on its store.
- **What a non-AWS deployment loses:** nothing it has today. The capability,
  the liveness join and the box's identity use no AWS API. Multi-box regions
  are AWS-only today (H26), but nothing here depends on them; one box per
  region is enough.
- **Build:** the worker and its socket code are Linux-only, with
  `//go:build linux` and a `!linux` stub, so the CLI still cross-compiles for
  darwin and windows.

## 13. Census

**Plan 14 root-read census (codex 12).** The scanner reads every production
package and matches `Open`, `QueryContext` and the rest whatever user runs
them (`internal/provision/plan14_root_reads_test.go:33`). Its only classes
are `authority`, `hint`, `diagnostic` and `legacy` (line 478), and `legacy`
needs `accepted-legacy;rev3=<ruling>` as its basis. "Runs as a
`DynamicUser`" is a fact for the ruling's footing, not a class. So each plan
proposes, for a ruling on #280:

| Declaration | Callers and identity | Proposed class and basis |
|---|---|---|
| `db.go` `runQueryContext` (main: `legacy`, `rev3=J4`, `basis=root-input`, `plan14-root-reads.tsv:202`) | moved out of `db.go` | retired through the overlay's removal row, the form the H3 carlos-run removal used |
| `internal/dbquery` `RunContext` (the moved body, unchanged) | the CLI on a member's machine, as the member; the worker on a box, as its `DynamicUser`. Neither is root, and neither decides anything for root | `legacy`, `accepted-legacy;rev3=<ruling>;basis=root-input`, the J4 footing main gave the same body |
| agent db-reads loop: the queue listing, the request read, the `picked` read-back, the record reads of checks A and B, the replica listing and GETs | the agent, `carlos`, `carlos-edge.service` | `legacy` with J1 (record and route reads) and J4 (object input), as the offline move's agent units took |
| worker: the scratch writes and reads, the file replica restore, `CheckDBContext`, the query, the connection watcher | `carlos-dbread@`, `DynamicUser` | `legacy` with J4 |
| agent readiness probe and admission (`Statfs` on `/var/tmp`, `systemctl list-units`) | the agent, `carlos` | `diagnostic` for the probe; `legacy` J4 for `Statfs` and the unit listing |

(The census's J1 and J4 are its own dispositions, not this spec's
invariants of the same names.)

The overlay re-pins the source envelopes of every changed package (`.`,
`internal/dbquery`, `internal/dbdoor`, `internal/host`, `internal/dbreads`)
and updates the legacy seal under the same ruling. The census owner may rule
other classes; the spec's requirement is only that every new and moved
declaration has one of the four, with both callers of the shared runner
named.

- **Root writes.** `ensureSystemUnits` writes two new unit files. That is a
  root-written artifact and joins the Plan 14 write census as an overlay, as
  every new unit has.
- **9B8 record-write census.** No record writer is added. The new `Put`
  sites (request, queue entry, `picked`, chunks, result, cancel) are
  `nonrecord`, all conditional creates. There is no `Delete` site.
  `registerCLIRoutes` changes as a `key-site` for the one route.
- **Claim routes.** The route joins
  `internal/console/testdata/claim-routes.txt`, refused on a host-claim
  console like the run's.
- **go.mod.** No new module, so no root-envelope overlay for it.

## 14. Plans

1. **Box side and wire, dormant.** `internal/dbreads` (ids, keys, the
   envelope, sealing, the result, limits); `internal/dbquery` (the query
   runner and guards moved out of `db.go`, with the CLI switched to it and
   no behaviour change); `dbdoor` planning over any `replicaReader`; the
   agent's db-reads loop with the identity bracket, conditional creates and
   the `picked` read-back, admission (reservations and the orphan count) and
   the readiness probe; `carlos system db-worker`, its protocol and its
   connection watcher; the two units in `ensureSystemUnits`; the
   `db-read-1` capability. Gates: T2, T3, T4, T5, T9, T12, T13, the parity
   half of T6 that needs no console. Census overlay. Nothing reaches a box
   unless a console asks.
2. **Console and CLI for `query` and `query --at`.** The one route and its
   streamed response, the read-deadline extension, the forward marker, the
   evidence rule for capability and liveness, admission held by the
   request, audit, the CLI's buffered forwarded path, DBF copy,
   `docs/site/cli.md`, and the `deployment-buckets` lifecycle rule with the
   approval plan for applying it. Gates: T1, T6, T7, T8, T10, T11, T14,
   T15, T16, T17, T18, copy review, census overlay. Roll per §11.3.
3. **`pull` and `pull --at`.** On ruling 1 (§16), which Paul confirmed
   with the storage statement on 2026-10-22. Sealed LTX files in the same single
   request, local restore and `CheckDBContext` in the CLI, and the pull copy
   rows. A 2 GiB pull in one request is the case most likely to need a
   longer request bound; plan 3 measures it before choosing.

Two follow-ups are listed here and not written:

4. **Plan 4 (follow-up): in-region reads onto the box path** (ruling 5).
   Written once phase 3 has run on the flagship. It is not designed here
   beyond one constraint this spec already keeps: the box path is
   region-agnostic, so the console can send an in-region read through it
   unchanged.
5. **Plan 5 (follow-up): per-region db-read bucket** (ruling 1). After the
   feature works, `db-reads/` traffic moves from the shared deployment
   bucket to a bucket in each data region, so that ciphertext and request
   SQL never rest outside the data's region. It needs its own design for
   the console's write grant into each region's bucket, the boxes' grant on
   their own, the queue and serial signal, and the lifecycle rule per
   bucket. Plans 1 to 3 keep the protocol's keys relative to one store
   (`db-reads/...`) so that plan 5 changes where the store is, not what is
   in it.

## 15. Review focus: the five likeliest failure modes

1. **The worker reaches more than its copy.** A unit option missed or
   overridden (network, `/data`, `/etc/carlos`, the metadata service, the
   socket on stdin and stdout), or a SQLite path (ATTACH, `VACUUM INTO`, an
   extension, a temp file) that opens something. Any one of these turns a
   member's SQL into the box role, which reads every tenant in the region.
2. **The prefix or host comes from the request, or the identity bracket
   leaks.** Any path where an envelope field, rather than the fresh record
   and the box's config, picks the bucket, the prefix or the host is a
   cross-tenant read. So is any byte read outside checks A and B, or any
   output written before check B.
3. **Forgery and replay.** An unsigned or wrongly scoped request accepted,
   domain separation from other root envelopes missing, the region not
   bound, or a queue entry trusted without its request.
4. **Something outliving the request.** Any state, route, timer or write
   on the console side that survives the request's end, any protocol delete
   or replacing write, or an admission count released anywhere but the
   handler's return. Each one reopens a finding J1 to J5 closed.
5. **The long request.** A timeout on the real path that the spec missed
   (a proxy, a client, a keep-alive), wait lines that stall behind a slow
   reader, or a 2 GiB pull that does not fit the request bound.

## 16. Rulings

The coordinator answered rev 1's five questions provisionally on
2026-10-22. Paul ruled on 2026-10-22, after Astra accepted rev 5: ruling 1
as below, and rulings 2 to 5 confirmed as written (he raised no objection).
All five are now Paul's.

1. **Pull through the deployment bucket (Paul, 2026-10-22).** Ship the
   feature through the deployment bucket as designed, for `query`,
   `query --at`, `pull` and `pull --at`. The true storage statement stands
   and is what Paul ruled on: **a read's whole ciphertext, up to its size
   cap, lives in the deployment bucket (eu-west-1 on the flagship) until
   lifecycle expiry, typically two to four days, with no guaranteed upper
   bound** (§5.7). For a query that is at most 64 MiB of rows; for a pull
   it is the planned replica files, up to the pull's cap. The SQL rests in
   the request object on the same timing (§9.3). The earlier provisional
   bound ("at most 64 MiB at a time for at most 1 hour") does not hold and
   is withdrawn. A short or hard bound would need version-aware deletes,
   with `s3:DeleteObjectVersion` on the deployment bucket for the console's
   role, which this design does not add.
   **Follow-up, after the feature works: a per-region bucket for this
   traffic** (plan 5, §14), so that ciphertext and request SQL never rest
   outside the data's region. Plans 1 to 3 do not build it and do not wait
   for it; they keep every `db-reads/` object key relative to one store so
   that plan 5 can move the store without changing the protocol.
2. **Limits stand.** 2 GiB restored, 5 minutes, 64 MB of rows, two reads per
   box. Real database sizes on the flagship are still unmeasured; plan 2's
   dev check reports them.
   The pull's cap, 2 GiB of planned LTX files (§5.7, rev 7, measured on dev
   2026-10-23), is put to Paul as a limit: **pending his confirmation.**
3. **Audit.** The row records the SQL's length and a SHA-256 prefix, not its
   text (§9.3). The SQL itself rests in the request object until expiry.
4. **Copy casing.** New strings use Paul's voice rule: lower case after a
   full stop (§10). The existing db door strings that capitalise are left
   as they are; changing them is a separate copy review.
5. **One path.** In-region reads move to the box path after phase 3 (§14).

## 17. What this does not do

- No console per region and no console-to-box connection.
- No resumable reads: a read that loses its connection is run again.
- No change to in-region reads (until the fourth plan), the claim-mode
  door, seeds `--from`, fleet or pooled instances.
- No IAM change. No new read authority for any role. The one infrastructure
  change is a lifecycle rule on the deployment bucket.
- No per-store S3 endpoints.
- No authenticated authorship of answers (§5.6).
- No `carlos db shell` and no browser view.

## 18. Declined review findings

**Round 1** (`CODEX-FINDINGS-DB-REGION-FORWARD.md`). Every citation was
checked against `main` at `5f341f33` and holds. Findings 2 to 10 and 12 are
accepted in full. Two are accepted in part, and rounds 2 and 3 agreed with
both declines:

- **Codex 1, the "stronger guarantee" (pin object versions to the checked
  incarnation): declined.** The identity bracket (§8.2) closes the finding.
  Pinning versions needs `s3:GetObjectVersion` and `s3:ListBucketVersions`
  on the box's data bucket, which the flagship's uk, us and au boxes do not
  have at module ref v0.21.0, and a versioned reader that today lives only
  behind the claim boundary. Claim activation can revisit it.
- **Codex 11, independently configured deployment and regional-data
  endpoints: declined.** No box path supports regional data on a separate
  store server today, because every S3 client in the process reads one
  `CARLOS_S3_ENDPOINT`. Per-store endpoints, if wanted, are a platform-wide
  change with its own design.

**Round 2** (`CODEX-FINDINGS-DB-REGION-FORWARD-R2.md`). Nothing declined.

**Round 3** (`CODEX-FINDINGS-DB-REGION-FORWARD-R3.md`). Every citation was
checked and holds, including that the worker must call
`lease.CheckDBContext` (`internal/lease/check.go:44`), not `CheckDB`; §8.3
and §13 now say so. Nothing is declined. Rev 4 does not patch findings 13,
14, 16 and 17 or the partly-holding invariants one by one: it removes the
durable read they all came from (§5.2), and §5.3 maps every finding to the
invariant that closes it or to the synchronous design that removes it.
Finding 7's fix is the honest timing statement, not a new mechanism; a hard
bound would need version-aware deletes, which is noted for Paul in §16
rather than built. Finding 18 is fixed by withdrawing the claim and stating
the trust boundary (§5.6), not by adding per-box signing.

**Round 4** (`CODEX-FINDINGS-DB-REGION-FORWARD-R4.md`). Every citation was
checked and holds, including litestream's file client and the LTX
compactor ignoring their contexts, and the backhaul transport's 30 s header
timeout. Nothing is declined. All four fixes reuse what the spec already
had: the run's result-before-list order, `http.ResponseController`, the
orphan refusal, and a process exit.
