carlos / platform Public

db-region-forward: plan 1, the box side and the wire, dark

Seventeen tasks: the copy record and gate, internal/dbquery, the dbreads
wire (ids, envelope, sealing, result), dbdoor planning over any reader,
the worker and carlos system db-worker, admission, the agent (check,
election, the owned read, the loop and probe), host wiring, the T6
parity half, the root-runner gates T2/T3/T12/T13, DBF-31, and the census.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev c0e688364dc86847fd7655a9b4e05f00271acc63 parent fe27bd9
1 file changed, +7018 −0
  • docs/superpowers/plans/2026-10-22-db-region-forward-plan-1-box.md +7018 −0
diff --git a/docs/superpowers/plans/2026-10-22-db-region-forward-plan-1-box.md b/docs/superpowers/plans/2026-10-22-db-region-forward-plan-1-box.md
new file mode 100644
index 000000000..2fc56420a
--- /dev/null
+++ b/docs/superpowers/plans/2026-10-22-db-region-forward-plan-1-box.md
@@ -0,0 +1,7018 @@
+# `carlos db` region forward, plan 1: the box side and the wire, dark Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Build everything a box needs to serve a forwarded `carlos db query` (the wire format, the sealed result, the agent's db-reads loop with its identity bracket and election, the sandboxed `carlos-dbread@` worker and its units, and the `db-read-1` capability), shipped dark: nothing runs unless a console asks, and no console asks until plan 2.
+
+**Architecture:** A new leaf-ish package `internal/dbreads` holds the wire (ids, object keys, the root-signed request envelope, sealing, the result) and the box engine (`Agent`: listing the region's queue, verifying, electing an owner with a create-only `picked`, the identity bracket, admission, chunked sealed output). Everything that decodes tenant bytes runs in `carlos system db-worker` (`dbreads.ServeWorker`), started per connection by a root-bound socket unit `carlos-dbread.socket` as a `DynamicUser` with no network. `internal/dbquery` takes the query runner and guards out of `db.go` so the CLI and the worker run the same code. `internal/dbdoor` gains planning over any replica reader, listed exactly as the console door lists. `internal/host` only wires: units, arming, the capability, the nudge.
+
+**Tech Stack:** Go 1.26, litestream v0.5.14 (`CalcRestorePlan`, the file replica client), superfly/ltx v0.5.1, modernc.org/sqlite v1.55.0, `crypto/ecdh` + `golang.org/x/crypto/hkdf` + AES-256-GCM, ed25519 root envelopes (`internal/release`), systemd socket activation, `golang.org/x/sys/unix`.
+
+**Spec:** `docs/superpowers/specs/2026-10-22-db-region-forward-design.md` (rev 5, accepted by Astra in round 5; Paul approved building it on 2026-10-22 with the rulings in §16). Read §3, §5, §7, §8, §11.4, §12, §13 and §15 before starting. Every task argues from the spec; where this plan and the spec disagree, the spec wins and the plan is the bug.
+
+**Branch:** `db-region-forward-1-box`, cut from amadan `main`. The worktree is made with the superpowers:using-git-worktrees skill. Plans 2 and 3 branch from `main` after this plan has merged.
+
+## Global Constraints
+
+Every task's requirements include this section.
+
+- **Production-wired acceptance.** Acceptance tests go through real constructors, the real units installed by the real `ensureSystemUnits`, the real store and the real verify and seal code. A stand-in (an in-process worker over a pipe, a fake dialer, a store wrapper) is allowed in unit tests only, and every such test says so in its doc comment with the words "not acceptance". Plan 1's acceptance gates are spec T2, T3, T4, T5, T9, T12, T13 and the parity half of T6.
+- **Targeted local runs; CI runs the full suite.** Locally run only the touched packages with `-run`, for example `go test ./internal/dbreads/ -run 'TestEnvelope' -count=1`. Never run `make test`, never a whole `internal/console` or `internal/host` package. Build, `go vet`, `gofmt -l` and the cross-compiles do run locally. A green CI run on the branch tip is the merge gate.
+- **Cross-compile gate.** CI builds darwin and windows. Any file using Linux-only syscalls (`SO_PEERCRED`, `Statfs`, `systemctl`) carries `//go:build linux` with a `_other.go` sibling tagged `//go:build !linux` that returns an "unsupported" error. Before every push run `GOOS=darwin GOARCH=arm64 go build ./...` and `GOOS=windows GOARCH=amd64 go build ./...`.
+- **Copy needs review before it ships, with no em dashes.** No user-facing string reaches code or docs until Paul approves it in the copy review (the `copy-review` skill). The record is `docs/superpowers/plans/2026-10-22-db-region-forward-copy.md`, gated by `approved_db_forward_copy_test.go`. No em dashes anywhere; a full stop and a new sentence, continuing in lower case. Internal error fragments, log lines and diagnostics are not copy rows (Paul, 2026-10-16).
+- **No tenant names.** Examples, fixtures and comments use `shop`, `shop.example.com`, `member@example.com`, regions `ie` and `uk`.
+- **Go cache.** Leave `GOCACHE`, `GOMODCACHE` and `GOTMPDIR` at their defaults. If the sandbox makes `~/.cache/go-build` read-only, run the go command with the sandbox off; never invent a cache path. Unix-socket tests fail inside the sandbox ("socket: operation not permitted"); run them with the sandbox off.
+- **Census overlays per branch, loaded LAST.** This branch adds `internal/provision/testdata/plan14-root-reads-db-region-forward-1.tsv`, loaded in `TestPlan14RootReadCensus` after every overlay main loads at the time of the merge, with the previous seal checked first. At main `5f341f33` the last overlay is `console-brand-name` (after `plans-2-box-limits`, `email-signin`, `design-fleet-internal-engines`, and earlier `securelisten-337` and `ci-speed-activator`); re-read the test after every merge of main and load this overlay after whatever is last then. Rows carry `rev3=PROPOSED-db-region-forward-1`; the ruling is asked of the census owner on #280.
+- **Merge main first.** Before asking for review and again immediately before merging: `git fetch origin && git merge origin/main`, rebuild the census overlay on the merged tree, push, and wait for CI on that tip (`amadan ci status carlos/platform -branch db-region-forward-1-box`). Merge only with `git merge-base --is-ancestor origin/main HEAD && amadan branch merge carlos/platform db-region-forward-1-box -expect <tip> -message "..."`.
+- **Test weights.** A new top-level test in `internal/console` or the root package that takes a second or more gets a row in `.amadan/test-weights.tsv` (Task 17 says how, without a full local run).
+- **No CI-only scripts under `scripts/`.** CI steps live in `.amadan/ci.d/`.
+- **New host tests call `t.Parallel()`** unless they touch env (`t.Setenv`), cwd, umask or package vars.
+- **Scratch** goes under `$TMPDIR` or `t.TempDir()`, and is removed by name, never by pattern.
+- **Wire shapes are additive.** Nothing this plan writes changes an existing object's shape. The edges file gains one capability string.
+- **Spec numbers, verbatim.** Pickup window 60 s from `issued_at` plus 60 s of skew; planning and restore 10 min; query 5 min; owner deadline 20 min from `picked`; restored size 2 GiB (`CARLOS_DB_READ_MAX_BYTES`); output 64 MiB (`runs.MaxOutputBytes`); chunk 256 KiB or 1 s (`runs.ChunkBytes`, `runs.ChunkEvery`); two reads per box plus one probe connection (`MaxConnections=3`); disk floor 5 GiB and 10% free; cancel checked every 5 s; queue listed on every serial move and every 30 s; probe at start, every 5 minutes and after a worker that failed to start; worker exits 5 s after losing its connection.
+
+## Review Focus
+
+The five input classes the spec implies but no gate test names, most likely first. Each has its test in the owning task.
+
+1. **SQL text that JSON escapes or rewrites.** `<`, `>`, `&`, U+2028 and invalid UTF-8 in `sql`. A person expects the box to run exactly the bytes they typed or be told no. `json.Marshal` rewrites invalid UTF-8 to U+FFFD, so the signed SQL would differ from the typed SQL. `Sign` refuses invalid UTF-8; the rest round-trips byte for byte through `Sign` and `Verify`. Test: Task 4, `TestEnvelopeSQLRoundTripsByteForByte`.
+2. **The hour boundary.** A request issued at 10:59:50 is listed at 11:00:20 under the previous hour's queue prefix. A person expects it to be picked. Test: Task 12, `TestPassFindsAnEntryFromThePreviousHour`.
+3. **A planned file that vanishes or changes size mid-download.** Litestream's compaction removes or rewrites L0 files. A person expects one quiet re-plan (with both identity checks again) and then `changed-twice`, never a half-restored copy and never a chunk. Test: Task 11, `TestReadRePlansOnceThenSaysChangedTwice`.
+4. **Output at the cap.** Exactly 64 MiB, one byte over, and one row bigger than a chunk. A person expects the first 64 MiB, `output-cap`, and a summary whose count and hash cover exactly the kept bytes. Test: Task 11, `TestChunkWriterCapIsExact`.
+5. **`systemctl` missing or failing.** The unit listing cannot be read. A person expects the box to take no read and skip (not fail) its probe, so a transient listing failure neither admits beside an orphan nor withdraws the capability. Test: Task 9, `TestAdmissionFailsClosedWhenUnitsCannotBeListed`, and Task 12, `TestProbeIsSkippedWhileAdmissionIsBlocked`.
+
+## Census impact
+
+- **Plan 14 root-read census:** one overlay, `plan14-root-reads-db-region-forward-1.tsv`, `rev3=PROPOSED-db-region-forward-1`:
+ - `db.go` `runQueryContext` keeps its name as a one-line wrapper that reads nothing, so main's legacy row is retired by an `@remove` plus an `@removed ... H3 census-ruling-db-region-forward-1 (PROPOSED, awaiting the census owner on #280): ...` row, the form the carlos-run and email-signin overlays used.
+ - New legacy rows (class `legacy`, task `accepted-legacy`): `internal/dbquery` `RunContext` (the moved body plus the `Immutable` open option; both callers named), every flagged unit in `internal/dbreads` (agent units at J1 for record reads and J4 for object input; worker units at J4), `internal/dbdoor` `ListLevel` and `PlanFrom` and `RestoreLocal`, the host units that list `carlos-dbread@` units and `Statfs` `/var/tmp` (J4), and the two unit files `internal/host/units/carlos-dbread.socket` and `carlos-dbread@.service` as `external` units (the form `carlos-instance-cmd@.service` took).
+ - One `diagnostic` row: the agent's readiness probe.
+ - Envelopes re-pinned: `.`, `internal/dbquery` (new), `internal/dbreads` (new), `internal/dbdoor`, `internal/host`, `internal/host/units`, `internal/console`. Frozen declarations are added, so the seal moves; main's seal is checked first.
+- **9B8 record-write census:** new `nonrecord` entries for the `Put` sites in `internal/dbreads` (`picked`, chunks, `result.json`), all conditional creates. No `Delete` site. No record writer.
+- **go.mod:** unchanged, so no root-envelope overlay for it.
+
+## Copy strings
+
+Plan 1 creates the whole deck from spec §10 (every row `pending`) so its gate pins the full inventory, and builds one row:
+
+| id | file | text |
+|---|---|---|
+| DBF-31 | docs/site/cli.md | carlos system db-worker runs on platform boxes only. systemd starts it for each forwarded database query. you never run it yourself. |
+
+Rows DBF-1 to DBF-16, DBF-21, DBF-22 and DBF-30 are plan 2's to review and build.
+
+## File structure
+
+| File | Responsibility |
+|---|---|
+| `internal/dbquery/dbquery.go` (create) | The read-only query runner, its guards and its cell format, moved from `db.go`. |
+| `internal/dbquery/dbquery_test.go` (create) | Guard table, immutable open, output format. |
+| `db.go` (modify) | Thin wrappers onto `internal/dbquery`. |
+| `internal/dbreads/limits.go` (create) | Every number from spec §5.7, named once. |
+| `internal/dbreads/ids.go` (create) | `dbr_` ids and every `db-reads/` key, with parsers. |
+| `internal/dbreads/envelope.go` (create) | The request envelope: `Request`, `Sign`, `Verify`, `QueueMatches`. |
+| `internal/dbreads/seal.go` (create) | Reply keys, chunk and summary sealing. |
+| `internal/dbreads/result.go` (create) | `Result`, `Picked`, `Summary`, states and codes. |
+| `internal/dbreads/class.go` (create) | `ClassRefusal`, moved from the console. |
+| `internal/dbreads/protocol.go` (create) | Worker framing and message types, shared by agent and worker. |
+| `internal/dbreads/worker.go` (create) | `ServeWorker`: the worker's whole job, portable. |
+| `internal/dbreads/worker_linux.go`, `worker_other.go` (create) | `RunWorker`: stdin socket and peer checks, the process exit. |
+| `internal/dbreads/admission.go` (create) | Reservations, the read and connection counts, the orphan rule. |
+| `internal/dbreads/agent.go` (create) | `Agent`: config, `Run`, `Pass`, `Probe`, `Capable`, election. |
+| `internal/dbreads/read.go` (create) | One owned read: the identity bracket, planning, streaming, check B, output, result. |
+| `internal/dbreads/chunks.go` (create) | `chunkWriter` and the create-only put with read-back. |
+| `internal/dbdoor/plan_from.go` (create) | `ReplicaReader`, `ListLevel`, `PlanFrom`, `RestoreLocal`. |
+| `internal/dbdoor/plan.go` (modify) | `RestorePlan` gains `Planned` and `At`; shared tail with `PlanFrom`. |
+| `internal/console/apidb.go` (modify) | `listReplicaLevel` and `dbClassRefusal` delegate to the moved code. |
+| `internal/host/units/carlos-dbread.socket`, `carlos-dbread@.service` (create) | The worker's units. |
+| `internal/host/systemunits.go` (modify) | Installs and arms the two units. |
+| `internal/host/dbreads.go` (create) | Arming, the env gate, the capability, the nudge. |
+| `internal/host/dbreads_linux.go`, `dbreads_other.go` (create) | Dialing the socket, listing units, `Statfs`. |
+| `internal/host/edgesfile.go`, `agent.go` (modify) | Publish `db-read-1`; arm the loop; nudge on serial moves. |
+| `system.go` (modify) | `carlos system db-worker`. |
+| `approved_db_forward_copy_test.go` (create) | The copy gate. |
+| `docs/superpowers/plans/2026-10-22-db-region-forward-copy.md` (create) | The copy record. |
+| `internal/console/dbreads_parity_test.go` (create) | The parity half of T6. |
+| `internal/host/dbreads_rootrunner_linux_test.go` (create) | T2, T3, T12, T13 on the root runner. |
+| `internal/provision/testdata/plan14-root-reads-db-region-forward-1.tsv` (create), `plan14_root_reads_test.go`, `db_region_forward_census_test.go` (create) | Census. |
+| `docs/superpowers/notes/census-proposal-db-region-forward-1.md` (create) | The census proposal for #280. |
+
+---
+
+### Task 1: The copy record and its gate
+
+**Files:**
+- Create: `docs/superpowers/plans/2026-10-22-db-region-forward-copy.md`
+- Create: `approved_db_forward_copy_test.go`
+
+**Interfaces:**
+- Consumes: `parseCopyTable`, `checkMovesInventory`, `checkMovesNoEmDash`, `checkRunBuilt`, `builtRows`, `checkRunApplied`, `reportAll`, `copyRow` (root package test helpers in `approved_moves_copy_test.go` and `approved_run_copy_test.go`).
+- Produces: `dbForwardCopyIDs` (the pinned inventory) and `dbForwardCopyBuilt` (empty here; Task 16 and plans 2 and 3 append to it).
+
+- [ ] **Step 1: Write the record**
+
+Create `docs/superpowers/plans/2026-10-22-db-region-forward-copy.md`:
+
+```markdown
+# `carlos db` region forward: copy record
+
+Every user-facing string the db region forward plans write (spec
+`2026-10-22-db-region-forward-design.md` §10): console refusals, the CLI's
+sentences, the audit details and the docs prose. The gate is
+`approved_db_forward_copy_test.go` at the repo root. It reuses the moves
+gate's parser, so the format is the moves record's: one table per heading,
+columns id, file, text, status.
+
+The rules:
+
+- A row is `pending` until Paul approves it in the copy review (the
+ `copy-review` skill). His edits are applied to the text here verbatim and
+ the row becomes `approved`.
+- No pending string reaches code or docs. The task that writes a row's
+ text adds its id to `dbForwardCopyBuilt` in the gate in the same commit,
+ and from then on the gate fails unless the row is `approved` and its
+ text is in the file it names.
+- In a Go file the text is one string literal, or one `fmt.Sprintf`,
+ `fmt.Errorf` or `errors.New` format, equal to the row with a format verb
+ where the row has a `<placeholder>`. No trailing newline inside the
+ literal.
+- A table cell holds one line, so a row's text writes a paragraph break as
+ `\n\n`. In `docs/site/cli.md` each paragraph of a row is one line.
+- No em dashes. A full stop and a new sentence instead, continuing in lower
+ case. Docs prose keeps the CLI page's sentence case.
+- Ids are not renumbered. DBF-17 to DBF-20 and DBF-23 to DBF-29 are kept
+ for plan 3's pull rows.
+
+Placeholders: `<host>` the instance's hostname; `<region>` a placement code
+such as uk; `<size>` and `<cap>` byte sizes as the CLI prints them; `<n>` a
+count of bytes; `<sha>` the first 16 hex digits of a SHA-256; `<time>` an
+RFC 3339 time.
+
+## Console (plan 2)
+
+| id | file | text | status |
+|---|---|---|---|
+| DBF-1 | internal/console/apidbforward.go | <host> keeps its data in region <region>. no server in that region can read databases yet, so carlos db cannot read it for now. | pending |
+| DBF-2 | internal/console/apidbforward.go | <host> keeps its data in region <region>. no server in that region is answering right now. try again in a few minutes. | pending |
+| DBF-9 | internal/console/apidbforward.go | too many database reads are open. wait a minute and try again. | pending |
+| DBF-10 | internal/console/apidbforward.go | <host> is moving between regions. read its database once the move has finished. | pending |
+| DBF-14 | internal/console/apidbforward.go | <host> keeps its data in region <region>. this version of carlos cannot read it from there. run carlos update, then try again. | pending |
+| DBF-21 | internal/console/apidbforward.go | forwarded a database query to region <region>. the query was <n> bytes, sha256 <sha> (cli) | pending |
+| DBF-22 | internal/console/apidbforward.go | forwarded a database query to region <region>, as of <time>. the query was <n> bytes, sha256 <sha> (cli) | pending |
+
+## CLI (plan 2)
+
+| id | file | text | status |
+|---|---|---|---|
+| DBF-3 | dbforward.go | no server in region <region> picked up the read within 60 seconds. try again in a few minutes. | pending |
+| DBF-4 | dbforward.go | <host>'s database is <size> once restored. carlos db can query at most <cap> in region <region>. | pending |
+| DBF-5 | dbforward.go | the server in region <region> does not have room to restore <host>'s database right now. try again later. | pending |
+| DBF-6 | dbforward.go | the result was cut off at 64 MB. narrow the query with WHERE or LIMIT and run it again. | pending |
+| DBF-7 | dbforward.go | the query ran for 5 minutes and was stopped. narrow it and run it again. | pending |
+| DBF-8 | dbforward.go | <host> keeps its data in region <region>. carlos db pull cannot fetch a copy from there yet. carlos db query can read it. | pending |
+| DBF-11 | dbforward.go | reading a copy of <host>'s database in region <region>. | pending |
+| DBF-12 | dbforward.go | the read of <host>'s database in region <region> did not finish, so nothing was printed. try again. | pending |
+| DBF-13 | dbforward.go | the answer from region <region> did not check out, so carlos db showed none of it. try again. | pending |
+| DBF-15 | dbforward.go | <host> changed while the read was waiting. try again. | pending |
+| DBF-16 | dbforward.go | <host> is moving between regions. read its database once the move has finished. | pending |
+
+## Docs
+
+| id | file | text | status |
+|---|---|---|---|
+| DBF-30 | docs/site/cli.md | 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.\n\nWhen 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. | pending |
+| DBF-31 | docs/site/cli.md | carlos system db-worker runs on platform boxes only. systemd starts it for each forwarded database query. you never run it yourself. | pending |
+```
+
+- [ ] **Step 2: Write the gate**
+
+Create `approved_db_forward_copy_test.go`:
+
+```go
+// approved_db_forward_copy_test.go
+package main
+
+import (
+ "os"
+ "regexp"
+ "strconv"
+ "strings"
+ "testing"
+)
+
+// The copy gate of the db region forward plans (spec 2026-10-22 §10), the
+// region HA 2b gate over this record:
+//
+// - TestDBForwardCopyRecordIsComplete: the record names exactly
+// dbForwardCopyIDs;
+// - TestDBForwardCopyHasNoEmDash;
+// - TestDBForwardBuiltCopyIsApproved: an id in dbForwardCopyBuilt is an
+// approved row. No pending string reaches code or docs;
+// - TestDBForwardBuiltCopyIsApplied: a built row's text is in the file it
+// names.
+const dbForwardCopyRecordPath = "docs/superpowers/plans/2026-10-22-db-region-forward-copy.md"
+
+var dbForwardCopyIDPattern = regexp.MustCompile(`^DBF-[0-9]+$`)
+
+// dbForwardCopyIDs is the record's inventory, pinned: DBF-1 to DBF-16,
+// DBF-21, DBF-22, DBF-30 and DBF-31. Plan 3 adds its pull rows here.
+var dbForwardCopyIDs = func() []string {
+ ids := make([]string, 0, 20)
+ for i := 1; i <= 16; i++ {
+ ids = append(ids, "DBF-"+strconv.Itoa(i))
+ }
+ return append(ids, "DBF-21", "DBF-22", "DBF-30", "DBF-31")
+}()
+
+// dbForwardCopyBuilt is every id whose text the code or docs carry. The task
+// that writes a row's text adds its id here in the same commit, once Paul has
+// approved the row.
+var dbForwardCopyBuilt = []string{}
+
+func mustParseDBForwardRecord(t *testing.T) []copyRow {
+ t.Helper()
+ raw, err := os.ReadFile(dbForwardCopyRecordPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ rows, err := parseCopyTable(string(raw), dbForwardCopyRecordPath, dbForwardCopyIDPattern, "a DBF-<n>")
+ if err != nil {
+ t.Fatal(err)
+ }
+ return rows
+}
+
+func TestDBForwardCopyRecordIsComplete(t *testing.T) {
+ reportAll(t, checkMovesInventory(mustParseDBForwardRecord(t), dbForwardCopyIDs))
+}
+
+func TestDBForwardCopyHasNoEmDash(t *testing.T) {
+ reportAll(t, checkMovesNoEmDash(mustParseDBForwardRecord(t)))
+}
+
+func TestDBForwardBuiltCopyIsApproved(t *testing.T) {
+ reportAll(t, checkRunBuilt(mustParseDBForwardRecord(t), dbForwardCopyBuilt))
+}
+
+func TestDBForwardBuiltCopyIsApplied(t *testing.T) {
+ rows := builtRows(mustParseDBForwardRecord(t), dbForwardCopyBuilt)
+ for i := range rows {
+ rows[i].Text = strings.ReplaceAll(rows[i].Text, `\n`, "\n")
+ }
+ reportAll(t, checkRunApplied(rows, "."))
+}
+```
+
+- [ ] **Step 3: Run the gate**
+
+Run: `go test . -run 'TestDBForward' -count=1`
+Expected: PASS (four tests). A FAIL from `TestDBForwardCopyRecordIsComplete` means a row id is missing or extra; fix the record, not the inventory.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add docs/superpowers/plans/2026-10-22-db-region-forward-copy.md approved_db_forward_copy_test.go
+git commit -m "db forward: copy record (all rows pending) and its gate"
+```
+
+- [ ] **Step 5: Ask for DBF-31's review**
+
+Invoke the `copy-review` skill with DBF-31 only (its context: the `carlos system` section of the CLI page; nobody runs the verb by hand). Apply Paul's text verbatim to the record, set the row to `approved`, and commit the record alone: `git commit -m "db forward: DBF-31 approved"`. Task 16 waits for this; every other task does not.
+
+---
+
+### Task 2: `internal/dbquery`, the query runner both callers share
+
+**Files:**
+- Create: `internal/dbquery/dbquery.go`
+- Create: `internal/dbquery/dbquery_test.go`
+- Modify: `db.go:384-684` (the runner, the guard and the tokenizer move out; thin wrappers stay)
+
+**Interfaces:**
+- Produces: `dbquery.RunContext(ctx context.Context, w io.Writer, path, sqlText string, opt dbquery.Options) error`; `dbquery.Options{Immutable bool}`; `dbquery.CheckSQL(q string) error`; `dbquery.Cell(v any) string`; `dbquery.ErrOneStatement`, `dbquery.ErrPragmaChange`.
+- Keeps (in `db.go`, for its callers and tests): `runQuery`, `runQueryContext`, `checkQuerySQL`, `dbQueryCell`, `errOneStatement`, `errPragmaChange` as one-line wrappers.
+
+- [ ] **Step 1: Write the failing test**
+
+Create `internal/dbquery/dbquery_test.go`:
+
+```go
+package dbquery
+
+import (
+ "bytes"
+ "context"
+ "database/sql"
+ "errors"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ _ "modernc.org/sqlite"
+)
+
+func fixtureDB(t *testing.T) string {
+ t.Helper()
+ path := filepath.Join(t.TempDir(), "shop.db")
+ db, err := sql.Open("sqlite", path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer db.Close()
+ for _, q := range []string{
+ `CREATE TABLE orders (id INTEGER PRIMARY KEY, note TEXT, data BLOB)`,
+ `INSERT INTO orders (note, data) VALUES ('a tab', x'00ff'), (NULL, NULL), ('two
+lines', 'x')`,
+ } {
+ if _, err := db.Exec(q); err != nil {
+ t.Fatal(err)
+ }
+ }
+ return path
+}
+
+func TestRunContextFormatsLikeTheCLI(t *testing.T) {
+ t.Parallel()
+ path := fixtureDB(t)
+ for _, opt := range []Options{{}, {Immutable: true}} {
+ var out bytes.Buffer
+ if err := RunContext(context.Background(), &out, path, "SELECT id, note FROM orders ORDER BY id", opt); err != nil {
+ t.Fatalf("%+v: %v", opt, err)
+ }
+ want := "id\tnote\n1\ta\\ttab\n2\tNULL\n3\ttwo\\nlines\n"
+ if out.String() != want {
+ t.Fatalf("%+v: got %q, want %q", opt, out.String(), want)
+ }
+ }
+}
+
+func TestRunContextRefusesWritesAndAttach(t *testing.T) {
+ t.Parallel()
+ path := fixtureDB(t)
+ for _, opt := range []Options{{}, {Immutable: true}} {
+ for _, q := range []string{
+ "DELETE FROM orders",
+ "ATTACH DATABASE 'x.db' AS x",
+ "VACUUM INTO 'copy.db'",
+ } {
+ if err := RunContext(context.Background(), &bytes.Buffer{}, path, q, opt); err == nil {
+ t.Fatalf("%+v: %q ran", opt, q)
+ }
+ }
+ }
+}
+
+func TestCheckSQLTable(t *testing.T) {
+ t.Parallel()
+ for _, c := range []struct {
+ q string
+ want error
+ }{
+ {"SELECT 1", nil},
+ {"SELECT 1; SELECT 2", ErrOneStatement},
+ {"SELECT ';' AS x", nil},
+ {"PRAGMA table_info(orders)", nil},
+ {"PRAGMA query_only = 0", ErrPragmaChange},
+ {"EXPLAIN QUERY PLAN PRAGMA writable_schema=1", ErrPragmaChange},
+ {"SELECT pragma FROM t", nil},
+ } {
+ if got := CheckSQL(c.q); !errors.Is(got, c.want) && !(got == nil && c.want == nil) {
+ t.Errorf("CheckSQL(%q) = %v, want %v", c.q, got, c.want)
+ }
+ }
+}
+
+func TestImmutableOpenAddsTheFlagOnly(t *testing.T) {
+ t.Parallel()
+ if got := dsn("/x/copy.db", Options{}); strings.Contains(got, "immutable") {
+ t.Fatalf("the CLI's open changed: %s", got)
+ }
+ if got := dsn("/x/copy.db", Options{Immutable: true}); !strings.Contains(got, "mode=ro&immutable=1&") {
+ t.Fatalf("the worker's open lacks immutable=1: %s", got)
+ }
+}
+```
+
+- [ ] **Step 2: Run it to verify it fails**
+
+Run: `go test ./internal/dbquery/ -count=1`
+Expected: FAIL, the package does not exist.
+
+- [ ] **Step 3: Move the code**
+
+Create `internal/dbquery/dbquery.go`. Move, unchanged except for names and the `dsn` helper, the bodies of `runQueryContext`, `dbQueryCell`, `errOneStatement`, `errPragmaChange`, `dbReadPragmas`, `checkQuerySQL`, `sqlTokenKind` and its constants, `sqlToken`, `utf8BOM`, `sqlTokens`, `isSQLIdentByte`, `splitFirstStatement` and `skipQuoted` from `db.go` (lines 384 to 684 at main `5f341f33`). The renamed exported surface and the one new helper:
+
+```go
+// Package dbquery is the one read-only query runner `carlos db query` uses:
+// on a member's machine (the CLI, db.go) and inside the db-read worker on a
+// box (internal/dbreads, spec 2026-10-22 §8.3). It moved out of db.go so the
+// guards, the open flags and the output format cannot drift between the two.
+package dbquery
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+ "time"
+
+ "modernc.org/sqlite"
+ sqlitelib "modernc.org/sqlite/lib"
+)
+
+// Options are the only difference between the two callers. Immutable adds
+// immutable=1 to the open: the worker's copy is a file nothing else ever
+// writes (spec §8.3).
+type Options struct {
+ Immutable bool
+}
+
+// ErrOneStatement and ErrPragmaChange are the two refusals before SQLite sees
+// anything. Their text is the CLI's, unchanged.
+var (
+ ErrOneStatement = errors.New("carlos db query runs one statement at a time. Run each statement on its own")
+ ErrPragmaChange = errors.New("this pragma changes how the copy is read, so carlos db query does not run it. Reading a pragma's value is fine")
+)
+
+// dsn is the open string. temp_store(2) keeps sorts and temp b-trees in
+// memory, so a large query cannot spill plaintext pages to a file outside the
+// private directory.
+func dsn(path string, opt Options) string {
+ mode := "mode=ro"
+ if opt.Immutable {
+ mode = "mode=ro&immutable=1"
+ }
+ return "file:" + path + "?" + mode + "&_pragma=query_only(1)&_pragma=busy_timeout(5000)&_pragma=temp_store(2)"
+}
+
+// RunContext opens path read-only, both ways (mode=ro and query_only), runs
+// sqlText, and writes a header row then one tab-separated line per row. It
+// is db.go's runQueryContext with the open string from dsn.
+func RunContext(ctx context.Context, w io.Writer, path, sqlText string, opt Options) error {
+ if err := CheckSQL(sqlText); err != nil {
+ return err
+ }
+ db, err := sql.Open("sqlite", dsn(path, opt))
+ if err != nil {
+ return err
+ }
+ defer db.Close()
+ conn, err := db.Conn(ctx)
+ if err != nil {
+ return err
+ }
+ defer conn.Close()
+ // mode=ro covers only main, and query_only is a pragma the statement
+ // list itself can switch off. ATTACH opens with the connection's
+ // read-write flags, so it is the way around both. VACUUM INTO attaches
+ // internally. No attached databases at all closes every one of those.
+ if _, err := sqlite.Limit(conn, sqlitelib.SQLITE_LIMIT_ATTACHED, 0); err != nil {
+ return err
+ }
+ rows, err := conn.QueryContext(ctx, sqlText)
+ if err != nil {
+ return err
+ }
+ defer rows.Close()
+ cols, err := rows.Columns()
+ if err != nil {
+ return err
+ }
+ if len(cols) > 0 {
+ if _, err := fmt.Fprintln(w, strings.Join(cols, "\t")); err != nil {
+ return err
+ }
+ }
+ vals := make([]any, len(cols))
+ ptrs := make([]any, len(cols))
+ for i := range vals {
+ ptrs[i] = &vals[i]
+ }
+ line := make([]string, len(cols))
+ for rows.Next() {
+ if err := rows.Scan(ptrs...); err != nil {
+ return err
+ }
+ for i, v := range vals {
+ line[i] = Cell(v)
+ }
+ if _, err := fmt.Fprintln(w, strings.Join(line, "\t")); err != nil {
+ return err
+ }
+ }
+ return rows.Err()
+}
+
+// Cell renders one value: NULL spelled out, tabs and newlines escaped.
+func Cell(v any) string {
+ var s string
+ switch x := v.(type) {
+ case nil:
+ return "NULL"
+ case []byte:
+ s = string(x)
+ case time.Time:
+ s = x.UTC().Format(time.RFC3339Nano)
+ default:
+ s = fmt.Sprint(x)
+ }
+ return strings.NewReplacer("\\", `\\`, "\t", `\t`, "\n", `\n`, "\r", `\r`).Replace(s)
+}
+```
+
+`CheckSQL` is `checkQuerySQL`'s body verbatim, returning `ErrOneStatement` and `ErrPragmaChange`; keep every doc comment that travels with the moved code.
+
+In `db.go`, delete the moved declarations and the now unused imports (`database/sql`, `modernc.org/sqlite`, `sqlitelib`), and add:
+
+```go
+// The query runner and its guards live in internal/dbquery (spec 2026-10-22
+// §8.3), so the CLI and the db-read worker run one copy of them. These
+// names stay for db.go's callers and tests.
+var (
+ errOneStatement = dbquery.ErrOneStatement
+ errPragmaChange = dbquery.ErrPragmaChange
+)
+
+func checkQuerySQL(q string) error { return dbquery.CheckSQL(q) }
+
+func dbQueryCell(v any) string { return dbquery.Cell(v) }
+
+func runQuery(w io.Writer, path, sqlText string) error {
+ return runQueryContext(context.Background(), w, path, sqlText)
+}
+
+// runQueryContext reads nothing itself: the open and the query are
+// dbquery.RunContext's.
+func runQueryContext(ctx context.Context, w io.Writer, path, sqlText string) error {
+ return dbquery.RunContext(ctx, w, path, sqlText, dbquery.Options{})
+}
+```
+
+- [ ] **Step 4: Run the tests**
+
+Run: `go test ./internal/dbquery/ -count=1 && go test . -run 'TestQuery|TestCheckQuerySQL|TestDBQuery|TestRunQuery' -count=1`
+Expected: PASS. The root package's existing `db_test.go` tests run unchanged through the wrappers; if a name in that `-run` matches nothing, list the tests with `grep -n '^func Test' db_test.go` and run those names.
+
+- [ ] **Step 5: Commit**
+
+```bash
+gofmt -l db.go internal/dbquery
+git add internal/dbquery db.go
+git commit -m "dbquery: the carlos db query runner and guards, shared by the CLI and the worker"
+```
+
+---
+
+### Task 3: `internal/dbreads` ids, keys and limits
+
+**Files:**
+- Create: `internal/dbreads/limits.go`
+- Create: `internal/dbreads/ids.go`
+- Create: `internal/dbreads/ids_test.go`
+
+**Interfaces:**
+- Produces: the constants below; `NewID(r io.Reader) (string, error)`, `MintID() (string, error)`, `ValidID(string) bool`, `ValidRegion(string) bool`; `RequestKey`, `PickedKey`, `OutPrefix`, `ResultKey`, `CancelKey` (each `func(id string) string`); `ChunkKey(id string, seq int64) string`; `ParseChunkKey(key, outPrefix string) (int64, bool)`; `QueueRegionPrefix(region string) string`; `QueueHourPrefix(region string, t time.Time) string`; `QueueKey(region string, issued time.Time, id string) string`; `type QueueEntry struct{ Region, Hour, ID string; Issued time.Time }`; `ParseQueueKey(key string) (QueueEntry, bool)`; `InWindow(issued, now time.Time) bool`.
+
+- [ ] **Step 1: Write the failing test**
+
+Create `internal/dbreads/ids_test.go`:
+
+```go
+package dbreads
+
+import (
+ "bytes"
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestIDsAreDbrAnd26Base32(t *testing.T) {
+ t.Parallel()
+ id, err := NewID(bytes.NewReader(make([]byte, 16)))
+ if err != nil || id != "dbr_aaaaaaaaaaaaaaaaaaaaaaaaaa" || !ValidID(id) {
+ t.Fatalf("NewID = %q, %v", id, err)
+ }
+ minted, err := MintID()
+ if err != nil || !ValidID(minted) {
+ t.Fatalf("MintID = %q, %v", minted, err)
+ }
+ for _, bad := range []string{"", "run_aaaaaaaaaaaaaaaaaaaaaaaaaa", "dbr_AAAAAAAAAAAAAAAAAAAAAAAAAA", "dbr_aaaa", "queue", "dbr_aaaaaaaaaaaaaaaaaaaaaaaaa/"} {
+ if ValidID(bad) {
+ t.Errorf("ValidID(%q) = true", bad)
+ }
+ }
+}
+
+func TestKeysLiveUnderOnePrefix(t *testing.T) {
+ t.Parallel()
+ id := "dbr_aaaaaaaaaaaaaaaaaaaaaaaaaa"
+ for _, k := range []string{RequestKey(id), PickedKey(id), ChunkKey(id, 1), ResultKey(id), CancelKey(id),
+ QueueKey("uk", time.Date(2026, 10, 22, 9, 0, 5, 0, time.UTC), id)} {
+ if !strings.HasPrefix(k, "db-reads/") {
+ t.Errorf("%s is outside db-reads/", k)
+ }
+ }
+ if got := ChunkKey(id, 12); got != "db-reads/"+id+"/out/00000012" {
+ t.Fatalf("ChunkKey = %s", got)
+ }
+ if seq, ok := ParseChunkKey(ChunkKey(id, 12), OutPrefix(id)); !ok || seq != 12 {
+ t.Fatalf("ParseChunkKey = %d %v", seq, ok)
+ }
+ for _, bad := range []string{OutPrefix(id) + "0000001", OutPrefix(id) + "-0000001", OutPrefix(id) + "00000000", OutPrefix(id) + "0000001x"} {
+ if _, ok := ParseChunkKey(bad, OutPrefix(id)); ok {
+ t.Errorf("ParseChunkKey(%q) accepted", bad)
+ }
+ }
+}
+
+func TestQueueKeyRoundTrips(t *testing.T) {
+ t.Parallel()
+ id := "dbr_bbbbbbbbbbbbbbbbbbbbbbbbbb"
+ issued := time.Date(2026, 10, 22, 10, 59, 50, 0, time.UTC)
+ key := QueueKey("uk", issued, id)
+ if key != "db-reads/queue/uk/2026102210/1792666790-"+id {
+ t.Fatalf("QueueKey = %s", key)
+ }
+ e, ok := ParseQueueKey(key)
+ if !ok || e.Region != "uk" || e.Hour != "2026102210" || !e.Issued.Equal(issued) || e.ID != id {
+ t.Fatalf("ParseQueueKey = %+v %v", e, ok)
+ }
+ for _, bad := range []string{
+ "db-reads/queue/uk/2026102210/x-" + id,
+ "db-reads/queue/uk/20261022/1792666790-" + id,
+ "db-reads/queue/../2026102210/1792666790-" + id,
+ "db-reads/queue/uk/2026102210/01792666790-" + id,
+ "db-reads/queue/uk/2026102210/1792666790-run_bbbbbbbbbbbbbbbbbbbbbbbbbb",
+ } {
+ if _, ok := ParseQueueKey(bad); ok {
+ t.Errorf("ParseQueueKey(%q) accepted", bad)
+ }
+ }
+}
+
+func TestInWindowIs60sPlus60sOfSkew(t *testing.T) {
+ t.Parallel()
+ now := time.Date(2026, 10, 22, 9, 0, 0, 0, time.UTC)
+ for _, c := range []struct {
+ issued time.Time
+ want bool
+ }{
+ {now, true},
+ {now.Add(-120 * time.Second), true},
+ {now.Add(-121 * time.Second), false},
+ {now.Add(60 * time.Second), true},
+ {now.Add(61 * time.Second), false},
+ } {
+ if got := InWindow(c.issued, now); got != c.want {
+ t.Errorf("InWindow(%s) = %v", c.issued.Sub(now), got)
+ }
+ }
+}
+```
+
+- [ ] **Step 2: Run it to verify it fails**
+
+Run: `go test ./internal/dbreads/ -count=1`
+Expected: FAIL, the package does not exist.
+
+- [ ] **Step 3: Write `limits.go` and `ids.go`**
+
+Create `internal/dbreads/limits.go`:
+
+```go
+// Package dbreads is a forwarded `carlos db` read (spec
+// docs/superpowers/specs/2026-10-22-db-region-forward-design.md): the wire
+// the console, the box and the CLI share, and the box's half of the work.
+// Every object lives under db-reads/ in the deployment bucket and is a
+// conditional create; the store's lifecycle rule is the only deletion (J2,
+// J4).
+package dbreads
+
+import (
+ "time"
+
+ "github.com/carlosframework/platform/internal/runs"
+)
+
+// Capability is what a box publishes in its edges file while its armed
+// db-reads loop's last readiness probe passed (spec §7.1).
+const Capability = "db-read-1"
+
+// Purpose is the envelope's fixed purpose and the sealing's HKDF info: one
+// string, so nothing else signed by the root or sealed by this package can be
+// mistaken for a db read.
+const Purpose = "carlos-db-read-v1"
+
+// Prefix is the one prefix every db read object lives under.
+const Prefix = "db-reads/"
+
+const (
+ OpQuery = "query"
+ OpPull = "pull" // plan 3
+
+ PickupWindow = 60 * time.Second // from issued_at (§5.7)
+ Skew = 60 * time.Second
+ ConsoleWaitsPicked = 2 * time.Minute
+ PlanBudget = 10 * time.Minute
+ QueryTimeout = 5 * time.Minute
+ OwnerDeadline = 20 * time.Minute
+ OwnerGrace = time.Minute
+ RequestBound = 22 * time.Minute
+ CLIBound = 23 * time.Minute
+ CancelEvery = 5 * time.Second
+ ListEvery = 30 * time.Second
+ ProbeEvery = 5 * time.Minute
+ WorkerExitGrace = 5 * time.Second
+ MaxJitter = 500 * time.Millisecond
+
+ DefaultMaxRestored int64 = 2 << 30 // CARLOS_DB_READ_MAX_BYTES
+ MaxOutputBytes = runs.MaxOutputBytes
+ ChunkBytes = runs.ChunkBytes
+ ChunkEvery = runs.ChunkEvery
+
+ ReadsPerBox = 2
+ MaxConnections = 3 // two reads and one probe (carlos-dbread.socket)
+ OpenPerPerson = 2
+ OpenPerConsole = 16
+
+ DiskFloor int64 = 5 << 30
+ DiskFreePercent = 10
+
+ // MaxSQLBytes keeps a request well inside the root envelope's payload
+ // bound (release.MaxRootEnvelopeBytes/2).
+ MaxSQLBytes = 256 << 10
+)
+```
+
+Run `gofmt -w internal/dbreads/limits.go` after writing it; the alignment above is not gofmt's.
+
+Create `internal/dbreads/ids.go`:
+
+```go
+package dbreads
+
+import (
+ "crypto/rand"
+ "encoding/base32"
+ "fmt"
+ "io"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+)
+
+var idEnc = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567").WithPadding(base32.NoPadding)
+
+var (
+ idShape = regexp.MustCompile(`^dbr_[a-z2-7]{26}$`)
+ regionShape = regexp.MustCompile(`^[a-z][a-z0-9-]{0,31}$`)
+ hourShape = regexp.MustCompile(`^[0-9]{10}$`)
+)
+
+// NewID mints an id from r: "dbr_" and 26 lowercase base32 characters, 128
+// random bits, the way runs.NewID mints run ids. Production mints with MintID.
+func NewID(r io.Reader) (string, error) {
+ var b [16]byte
+ if _, err := io.ReadFull(r, b[:]); err != nil {
+ return "", err
+ }
+ return "dbr_" + idEnc.EncodeToString(b[:]), nil
+}
+
+func MintID() (string, error) { return NewID(rand.Reader) }
+
+// ValidID reports a db read id: 30 bytes of [a-z2-7_], one clean path
+// element, and never "queue".
+func ValidID(id string) bool { return idShape.MatchString(id) }
+
+// ValidRegion reports a placement code that can be one key segment. A fleet
+// placement ("fleet/x") is not one: fleet instances never forward.
+func ValidRegion(r string) bool { return regionShape.MatchString(r) }
+
+func RequestKey(id string) string { return Prefix + id + "/request.json" }
+func PickedKey(id string) string { return Prefix + id + "/picked" }
+func OutPrefix(id string) string { return Prefix + id + "/out/" }
+func ResultKey(id string) string { return Prefix + id + "/result.json" }
+func CancelKey(id string) string { return Prefix + id + "/cancel" }
+
+// ChunkKey is out/<seq>, zero-padded to 8 digits so keys sort in order.
+func ChunkKey(id string, seq int64) string { return OutPrefix(id) + fmt.Sprintf("%08d", seq) }
+
+// ParseChunkKey reads a key ChunkKey made under outPrefix.
+func ParseChunkKey(key, outPrefix string) (int64, bool) {
+ name, ok := strings.CutPrefix(key, outPrefix)
+ if !ok || len(name) != 8 {
+ return 0, false
+ }
+ for i := 0; i < 8; i++ {
+ if name[i] < '0' || name[i] > '9' {
+ return 0, false
+ }
+ }
+ seq, err := strconv.ParseInt(name, 10, 64)
+ if err != nil || seq < 1 {
+ return 0, false
+ }
+ return seq, true
+}
+
+const hourLayout = "2006010215"
+
+func QueueRegionPrefix(region string) string { return Prefix + "queue/" + region + "/" }
+
+func QueueHourPrefix(region string, t time.Time) string {
+ return QueueRegionPrefix(region) + t.UTC().Format(hourLayout) + "/"
+}
+
+// QueueKey is the empty pointer the console writes after the request:
+// db-reads/queue/<region>/<YYYYMMDDHH>/<unix>-<id>. A box can skip an
+// out-of-window entry from its key alone (spec §5.4).
+func QueueKey(region string, issued time.Time, id string) string {
+ return QueueHourPrefix(region, issued) + strconv.FormatInt(issued.Unix(), 10) + "-" + id
+}
+
+// QueueEntry is a parsed queue key.
+type QueueEntry struct {
+ Region, Hour, ID string
+ Issued time.Time
+}
+
+func ParseQueueKey(key string) (QueueEntry, bool) {
+ rest, ok := strings.CutPrefix(key, Prefix+"queue/")
+ if !ok {
+ return QueueEntry{}, false
+ }
+ parts := strings.Split(rest, "/")
+ if len(parts) != 3 || !ValidRegion(parts[0]) || !hourShape.MatchString(parts[1]) {
+ return QueueEntry{}, false
+ }
+ unixRaw, id, ok := strings.Cut(parts[2], "-")
+ if !ok || !ValidID(id) {
+ return QueueEntry{}, false
+ }
+ unix, err := strconv.ParseInt(unixRaw, 10, 64)
+ if err != nil || unix <= 0 || strconv.FormatInt(unix, 10) != unixRaw {
+ return QueueEntry{}, false
+ }
+ return QueueEntry{Region: parts[0], Hour: parts[1], ID: id, Issued: time.Unix(unix, 0).UTC()}, true
+}
+
+// InWindow is the box's pickup window: no more than PickupWindow plus Skew
+// old, and no more than Skew in the future (spec §5.5, §5.7).
+func InWindow(issued, now time.Time) bool {
+ return !issued.Before(now.Add(-PickupWindow-Skew)) && !issued.After(now.Add(Skew))
+}
+```
+
+- [ ] **Step 4: Run the tests**
+
+Run: `gofmt -l internal/dbreads; go test ./internal/dbreads/ -count=1`
+Expected: no gofmt output; PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add internal/dbreads/limits.go internal/dbreads/ids.go internal/dbreads/ids_test.go
+git commit -m "dbreads: ids, db-reads/ keys and the spec's limits"
+```
+
+---
+
+### Task 4: The request envelope
+
+**Files:**
+- Create: `internal/dbreads/envelope.go`
+- Create: `internal/dbreads/envelope_test.go`
+
+**Interfaces:**
+- Consumes: `release.Signer`, `release.RootSigner`, `release.SignRootEnvelope`, `release.VerifyRootEnvelope`, `release.ValidAccount`, `release.ValidApp`, `provision.ValidHostShape`; `ParseReplyKey` (Task 5; write Task 5's `seal.go` reply-key functions first if you take Task 4 alone, or do Tasks 4 and 5 in one sitting).
+- Produces: `type Request struct` (fields below, JSON exactly spec §5.5); `Sign(sg release.Signer, req Request) ([]byte, error)`; `Verify(pub ed25519.PublicKey, wire []byte, id, region string, now time.Time) (Request, error)`; `QueueMatches(e QueueEntry, req Request) bool`; `(Request) AtTime() (time.Time, error)`; `(Request) Issued() (time.Time, error)`; `ErrEnvelope`.
+
+- [ ] **Step 1: Write the failing test**
+
+Create `internal/dbreads/envelope_test.go`:
+
+```go
+package dbreads
+
+import (
+ "crypto/ed25519"
+ "crypto/rand"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+var testNow = time.Date(2026, 10, 22, 9, 0, 0, 0, time.UTC)
+
+func testRequest(t *testing.T) Request {
+ t.Helper()
+ priv, err := NewReplyKey()
+ if err != nil {
+ t.Fatal(err)
+ }
+ return Request{
+ ID: "dbr_aaaaaaaaaaaaaaaaaaaaaaaaaa", Account: "bab", App: "shop", Host: "shop.example.com",
+ Incarnation: "inc", Region: "uk", Op: OpQuery, SQL: "SELECT count(*) FROM orders",
+ ReplyKey: EncodeReplyKey(priv.PublicKey()), By: "member@example.com",
+ IssuedAt: testNow.Format(time.RFC3339),
+ }
+}
+
+func rootKeys(t *testing.T) (ed25519.PublicKey, ed25519.PrivateKey) {
+ t.Helper()
+ pub, priv, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return pub, priv
+}
+
+func TestEnvelopeVerifiesWhatTheRootSigned(t *testing.T) {
+ t.Parallel()
+ pub, priv := rootKeys(t)
+ req := testRequest(t)
+ wire, err := Sign(release.RootSigner(priv), req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got, err := Verify(pub, wire, req.ID, "uk", testNow.Add(30*time.Second))
+ if err != nil {
+ t.Fatal(err)
+ }
+ req.Purpose, req.V = Purpose, 1
+ if got != req {
+ t.Fatalf("got %+v, want %+v", got, req)
+ }
+}
+
+// T4 (spec §11.4), the envelope half: every forgery and replay the envelope
+// alone must stop.
+func TestEnvelopeRefusesForgeryAndReplay(t *testing.T) {
+ t.Parallel()
+ pub, priv := rootKeys(t)
+ _, otherPriv := rootKeys(t)
+ req := testRequest(t)
+ good, err := Sign(release.RootSigner(priv), req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ unsigned, _ := json.Marshal(map[string]string{"payload": base64.StdEncoding.EncodeToString([]byte(`{}`)), "sig": ""})
+ wrongKey, err := Sign(release.RootSigner(otherPriv), req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ cases := []struct {
+ name string
+ wire []byte
+ id string
+ region string
+ now time.Time
+ }{
+ {"unsigned", unsigned, req.ID, "uk", testNow},
+ {"wrong key", wrongKey, req.ID, "uk", testNow},
+ {"moved to another id", good, "dbr_bbbbbbbbbbbbbbbbbbbbbbbbbb", "uk", testNow},
+ {"moved to another region", good, req.ID, "ie", testNow},
+ {"older than the window", good, req.ID, "uk", testNow.Add(61 * time.Second)},
+ {"from the future", good, req.ID, "uk", testNow.Add(-61 * time.Second)},
+ }
+ for _, c := range cases {
+ if _, err := Verify(pub, c.wire, c.id, c.region, c.now); !errors.Is(err, ErrEnvelope) && !errors.Is(err, release.ErrRootEnvelope) {
+ t.Errorf("%s: Verify = %v, want a refusal", c.name, err)
+ }
+ }
+}
+
+// A scoped release key cannot sign: the console that holds only one cannot
+// forward (spec §5.5).
+func TestEnvelopeScopedKeyCannotSign(t *testing.T) {
+ t.Parallel()
+ rootPub, rootPriv := rootKeys(t)
+ seed, grant, err := release.MintKeyGrant(rootPriv, "ci", []string{"canary"}, time.Hour)
+ if err != nil {
+ t.Fatal(err)
+ }
+ key, err := release.ParseSigningKey(seed, "scoped")
+ if err != nil {
+ t.Fatal(err)
+ }
+ sg, err := release.ScopedSigner(key, grant, rootPub)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := Sign(sg, testRequest(t)); !errors.Is(err, release.ErrRootEnvelope) {
+ t.Fatalf("a scoped key signed: %v", err)
+ }
+}
+
+func TestEnvelopeRefusesWhatItCannotCarry(t *testing.T) {
+ t.Parallel()
+ _, priv := rootKeys(t)
+ for name, mut := range map[string]func(*Request){
+ "bad id": func(r *Request) { r.ID = "run_aaaaaaaaaaaaaaaaaaaaaaaaaa" },
+ "fleet region": func(r *Request) { r.Region = "fleet/acme" },
+ "bad host": func(r *Request) { r.Host = "../../etc" },
+ "bad op": func(r *Request) { r.Op = "shell" },
+ "empty sql": func(r *Request) { r.SQL = "" },
+ "huge sql": func(r *Request) { r.SQL = strings.Repeat("x", MaxSQLBytes+1) },
+ "invalid utf-8": func(r *Request) { r.SQL = "SELECT '\xff'" },
+ "bad at": func(r *Request) { r.At = "yesterday" },
+ "bad reply key": func(r *Request) { r.ReplyKey = "AAAA" },
+ "bad issued_at": func(r *Request) { r.IssuedAt = "now" },
+ } {
+ req := testRequest(t)
+ mut(&req)
+ if _, err := Sign(release.RootSigner(priv), req); !errors.Is(err, ErrEnvelope) {
+ t.Errorf("%s: Sign = %v", name, err)
+ }
+ }
+}
+
+// Review Focus 1: the SQL a person typed is the SQL the box runs.
+func TestEnvelopeSQLRoundTripsByteForByte(t *testing.T) {
+ t.Parallel()
+ pub, priv := rootKeys(t)
+ req := testRequest(t)
+ req.SQL = "SELECT '<a href=\"x\">&amp;</a>' || char(10) || ' 
 
 é' WHERE 1 < 2 AND 3 > 2"
+ wire, err := Sign(release.RootSigner(priv), req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got, err := Verify(pub, wire, req.ID, "uk", testNow)
+ if err != nil || got.SQL != req.SQL {
+ t.Fatalf("Verify = %q, %v", got.SQL, err)
+ }
+}
+
+func TestQueueMatchesNeedsEveryPart(t *testing.T) {
+ t.Parallel()
+ req := testRequest(t)
+ issued, _ := req.Issued()
+ good, _ := ParseQueueKey(QueueKey("uk", issued, req.ID))
+ if !QueueMatches(good, req) {
+ t.Fatal("the console's own entry does not match")
+ }
+ for name, key := range map[string]string{
+ "other region": QueueKey("ie", issued, req.ID),
+ "other time": QueueKey("uk", issued.Add(time.Second), req.ID),
+ "other hour": QueueHourPrefix("uk", issued.Add(time.Hour)) + strconv.FormatInt(issued.Unix(), 10) + "-" + req.ID,
+ "other id": QueueKey("uk", issued, "dbr_bbbbbbbbbbbbbbbbbbbbbbbbbb"),
+ } {
+ e, ok := ParseQueueKey(key)
+ if ok && QueueMatches(e, req) {
+ t.Errorf("%s matched", name)
+ }
+ }
+}
+```
+
+- [ ] **Step 2: Run it to verify it fails**
+
+Run: `go test ./internal/dbreads/ -run 'TestEnvelope|TestQueueMatches' -count=1`
+Expected: FAIL, `Request`, `Sign` and `Verify` are undefined.
+
+- [ ] **Step 3: Write `envelope.go`**
+
+```go
+package dbreads
+
+import (
+ "crypto/ed25519"
+ "errors"
+ "time"
+ "unicode/utf8"
+
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// ErrEnvelope is a request this package will not sign or a box will not
+// accept: well signed, perhaps, but not for this id, region or moment.
+var ErrEnvelope = errors.New("dbreads: request refused")
+
+// Request is db-reads/<id>/request.json's payload (spec §5.5). The box
+// decodes it with release.VerifyRootEnvelope, which requires canonical JSON
+// for this exact type, so the field order here is the wire's.
+type Request struct {
+ Purpose string `json:"purpose"`
+ V int `json:"v"`
+ ID string `json:"id"`
+ Account string `json:"account"`
+ App string `json:"app"`
+ Host string `json:"host"`
+ Incarnation string `json:"incarnation"`
+ Region string `json:"region"`
+ Op string `json:"op"`
+ SQL string `json:"sql"`
+ At string `json:"at"`
+ ReplyKey string `json:"reply_key"`
+ By string `json:"by"`
+ IssuedAt string `json:"issued_at"`
+}
+
+// Issued is IssuedAt as a time.
+func (r Request) Issued() (time.Time, error) { return time.Parse(time.RFC3339, r.IssuedAt) }
+
+// AtTime is At as a time; zero for the newest copy.
+func (r Request) AtTime() (time.Time, error) {
+ if r.At == "" {
+ return time.Time{}, nil
+ }
+ return time.Parse(time.RFC3339, r.At)
+}
+
+// ops are the operations this build serves. Plan 3 adds OpPull.
+var ops = map[string]bool{OpQuery: true}
+
+func (r Request) valid() bool {
+ if r.Purpose != Purpose || r.V != 1 || !ValidID(r.ID) || !ValidRegion(r.Region) ||
+ !release.ValidAccount(r.Account) || !release.ValidApp(r.App) || !provision.ValidHostShape(r.Host) ||
+ r.Incarnation == "" || !ops[r.Op] || r.By == "" {
+ return false
+ }
+ if r.Op == OpQuery && (r.SQL == "" || len(r.SQL) > MaxSQLBytes || !utf8.ValidString(r.SQL)) {
+ return false
+ }
+ if _, err := r.AtTime(); err != nil {
+ return false
+ }
+ if _, err := r.Issued(); err != nil {
+ return false
+ }
+ _, err := ParseReplyKey(r.ReplyKey)
+ return err == nil
+}
+
+// Sign is the console's half: it fills Purpose and V, refuses a request it
+// could not carry intact (invalid UTF-8 would be rewritten by encoding/json,
+// so the box would run SQL nobody typed), and signs with the root.
+// release.SignRootEnvelope refuses a scoped key.
+func Sign(sg release.Signer, req Request) ([]byte, error) {
+ req.Purpose, req.V = Purpose, 1
+ if !req.valid() {
+ return nil, ErrEnvelope
+ }
+ return release.SignRootEnvelope(sg, req)
+}
+
+// Verify is the box's half (spec §5.5): the root signature over canonical
+// JSON, this package's purpose, the id the object was read under, this box's
+// own region, and an issue time no more than PickupWindow old and Skew in the
+// future. The age bound is PickupWindow, not PickupWindow+Skew: InWindow
+// already widened the listing by Skew, and the envelope is checked against
+// this box's own clock.
+func Verify(pub ed25519.PublicKey, wire []byte, id, region string, now time.Time) (Request, error) {
+ var req Request
+ if err := release.VerifyRootEnvelope(pub, wire, &req); err != nil {
+ return Request{}, err
+ }
+ if !req.valid() || req.ID != id || req.Region != region {
+ return Request{}, ErrEnvelope
+ }
+ issued, _ := req.Issued()
+ if issued.Before(now.Add(-PickupWindow)) || issued.After(now.Add(Skew)) {
+ return Request{}, ErrEnvelope
+ }
+ return req, nil
+}
+
+// QueueMatches is the queue entry check (spec §5.9): the entry's region,
+// hour, time and id are all the envelope's, so a box's forged entry for a
+// real request names nothing the request does not.
+func QueueMatches(e QueueEntry, req Request) bool {
+ issued, err := req.Issued()
+ return err == nil && e.Region == req.Region && e.ID == req.ID && e.Issued.Equal(issued) &&
+ e.Hour == issued.UTC().Format(hourLayout)
+}
+```
+
+Note the spec's window: "issued_at is no more than 60 s old and no more than 60 s in the future". `Verify` enforces exactly that; the test's "older than the window" case uses 61 s.
+
+- [ ] **Step 4: Run the tests**
+
+Run: `go test ./internal/dbreads/ -run 'TestEnvelope|TestQueueMatches' -count=1`
+Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add internal/dbreads/envelope.go internal/dbreads/envelope_test.go
+git commit -m "dbreads: the root-signed request envelope and the queue entry check"
+```
+
+---
+
+### Task 5: Sealing and the result
+
+**Files:**
+- Create: `internal/dbreads/seal.go`
+- Create: `internal/dbreads/result.go`
+- Create: `internal/dbreads/seal_test.go`
+
+**Interfaces:**
+- Produces: `NewReplyKey() (*ecdh.PrivateKey, error)`, `EncodeReplyKey(*ecdh.PublicKey) string`, `ParseReplyKey(string) (*ecdh.PublicKey, error)`; `SealChunk(pub *ecdh.PublicKey, id string, seq int64, stream string, last bool, plain []byte) ([]byte, error)`; `OpenChunk(priv *ecdh.PrivateKey, id string, seq int64, stream string, last bool, sealed []byte) ([]byte, error)`; `SealSummary(pub *ecdh.PublicKey, id string, s Summary) (string, error)`; `OpenSummary(priv *ecdh.PrivateKey, id, sealed string) (Summary, error)`; `SealOverhead`; `StreamRows`, `StreamLTX`; `ErrSeal`; `type Summary`, `type Result`, `type Picked`; the `State*` and `Code*` constants; `(Result) Valid() error`.
+
+- [ ] **Step 1: Write the failing test**
+
+Create `internal/dbreads/seal_test.go`:
+
+```go
+package dbreads
+
+import (
+ "bytes"
+ "errors"
+ "testing"
+ "time"
+)
+
+const sealID = "dbr_aaaaaaaaaaaaaaaaaaaaaaaaaa"
+
+func TestChunkSealRoundTripsAndBindsEverything(t *testing.T) {
+ t.Parallel()
+ priv, err := NewReplyKey()
+ if err != nil {
+ t.Fatal(err)
+ }
+ plain := bytes.Repeat([]byte("row\n"), 1000)
+ sealed, err := SealChunk(priv.PublicKey(), sealID, 3, StreamRows, false, plain)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(sealed)-len(plain) >= 100 || len(sealed)-len(plain) != SealOverhead {
+ t.Fatalf("overhead %d, spec §5.7 says under 100 bytes", len(sealed)-len(plain))
+ }
+ got, err := OpenChunk(priv, sealID, 3, StreamRows, false, sealed)
+ if err != nil || !bytes.Equal(got, plain) {
+ t.Fatalf("OpenChunk = %v", err)
+ }
+ other, _ := NewReplyKey()
+ for name, open := range map[string]func() ([]byte, error){
+ "moved to another read": func() ([]byte, error) {
+ return OpenChunk(priv, "dbr_bbbbbbbbbbbbbbbbbbbbbbbbbb", 3, StreamRows, false, sealed)
+ },
+ "reordered": func() ([]byte, error) { return OpenChunk(priv, sealID, 4, StreamRows, false, sealed) },
+ "relabelled as last": func() ([]byte, error) { return OpenChunk(priv, sealID, 3, StreamRows, true, sealed) },
+ "another stream": func() ([]byte, error) { return OpenChunk(priv, sealID, 3, StreamLTX, false, sealed) },
+ "another key": func() ([]byte, error) { return OpenChunk(other, sealID, 3, StreamRows, false, sealed) },
+ "truncated": func() ([]byte, error) { return OpenChunk(priv, sealID, 3, StreamRows, false, sealed[:len(sealed)-1]) },
+ } {
+ if _, err := open(); !errors.Is(err, ErrSeal) {
+ t.Errorf("%s: %v", name, err)
+ }
+ }
+}
+
+func TestSummarySealRoundTrips(t *testing.T) {
+ t.Parallel()
+ priv, _ := NewReplyKey()
+ want := Summary{TXID: 42, At: time.Date(2026, 10, 7, 14, 0, 0, 0, time.UTC), Bytes: 10, SHA256: "ab"}
+ sealed, err := SealSummary(priv.PublicKey(), sealID, want)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got, err := OpenSummary(priv, sealID, sealed)
+ if err != nil || got.TXID != want.TXID || !got.At.Equal(want.At) || got.SHA256 != want.SHA256 {
+ t.Fatalf("OpenSummary = %+v, %v", got, err)
+ }
+ if _, err := OpenSummary(priv, "dbr_bbbbbbbbbbbbbbbbbbbbbbbbbb", sealed); !errors.Is(err, ErrSeal) {
+ t.Fatalf("a summary moved to another read opened: %v", err)
+ }
+}
+
+func TestReplyKeyRefusesWhatIsNotAnUncompressedPoint(t *testing.T) {
+ t.Parallel()
+ for _, bad := range []string{"", "AAAA", "!!!!"} {
+ if _, err := ParseReplyKey(bad); err == nil {
+ t.Errorf("ParseReplyKey(%q) accepted", bad)
+ }
+ }
+}
+
+func TestResultValidKnowsTheVocabulary(t *testing.T) {
+ t.Parallel()
+ ok := []Result{
+ {State: StateDone, LastSeq: 1},
+ {State: StateDone, Code: CodeOutputCap, LastSeq: 3},
+ {State: StateFailed, Code: CodeSQLError},
+ {State: StateRefused, Code: CodeRecordChanged},
+ {State: StateCancelled},
+ }
+ for _, r := range ok {
+ if err := r.Valid(); err != nil {
+ t.Errorf("%+v: %v", r, err)
+ }
+ }
+ bad := []Result{
+ {State: StateDone, LastSeq: 0},
+ {State: "maybe"},
+ {State: StateFailed, Code: "nope"},
+ {State: StateDone, LastSeq: -1},
+ }
+ for _, r := range bad {
+ if err := r.Valid(); err == nil {
+ t.Errorf("%+v accepted", r)
+ }
+ }
+}
+```
+
+- [ ] **Step 2: Run it to verify it fails**
+
+Run: `go test ./internal/dbreads/ -run 'TestChunkSeal|TestSummarySeal|TestReplyKey|TestResultValid' -count=1`
+Expected: FAIL, undefined names.
+
+- [ ] **Step 3: Write `seal.go`**
+
+```go
+package dbreads
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/ecdh"
+ "crypto/rand"
+ "crypto/sha256"
+ "encoding/base64"
+ "encoding/binary"
+ "encoding/json"
+ "errors"
+ "io"
+ "strconv"
+
+ "golang.org/x/crypto/hkdf"
+)
+
+// Sealing is the appconfig v2 construction (spec §5.6): ECDH P-256 with a
+// fresh sender key per seal, HKDF-SHA256 with this package's own info string,
+// AES-256-GCM with bound associated data. The wire is raw bytes, never
+// base64, in the bucket: "DBR1" || sender key (65) || nonce (12) || ciphertext.
+const (
+ sealPrefix = "DBR1"
+ sealEphLen = 65
+ sealNonceLen = 12
+ // SealOverhead is what sealing adds: under 100 bytes (spec §5.7).
+ SealOverhead = len(sealPrefix) + sealEphLen + sealNonceLen + 16
+)
+
+// The streams a chunk can carry, bound into its associated data.
+const (
+ StreamRows = "rows" // a query's output
+ StreamLTX = "ltx" // a pull's planned files (plan 3)
+)
+
+// ErrSeal is any sealed object that does not open: wrong key, moved,
+// reordered, relabelled, truncated or corrupt.
+var ErrSeal = errors.New("dbreads: the sealed bytes do not open")
+
+var errBadReplyKey = errors.New("dbreads: reply_key is not an uncompressed P-256 point")
+
+// randReader is a var so a test can make sealing deterministic.
+var randReader io.Reader = rand.Reader
+
+// NewReplyKey is the CLI's ephemeral key pair for one read. The private half
+// never leaves the CLI's memory.
+func NewReplyKey() (*ecdh.PrivateKey, error) { return ecdh.P256().GenerateKey(randReader) }
+
+func EncodeReplyKey(pub *ecdh.PublicKey) string { return base64.StdEncoding.EncodeToString(pub.Bytes()) }
+
+func ParseReplyKey(s string) (*ecdh.PublicKey, error) {
+ raw, err := base64.StdEncoding.Strict().DecodeString(s)
+ if err != nil || len(raw) != sealEphLen {
+ return nil, errBadReplyKey
+ }
+ pub, err := ecdh.P256().NewPublicKey(raw)
+ if err != nil {
+ return nil, errBadReplyKey
+ }
+ return pub, nil
+}
+
+// lengthPrefixed encodes fields so no choice of values makes two lists
+// encode the same.
+func lengthPrefixed(fields ...string) []byte {
+ var out []byte
+ for _, f := range fields {
+ out = binary.BigEndian.AppendUint32(out, uint32(len(f)))
+ out = append(out, f...)
+ }
+ return out
+}
+
+func chunkAAD(id string, seq int64, stream string, last bool) []byte {
+ l := "0"
+ if last {
+ l = "1"
+ }
+ return lengthPrefixed(Purpose, "chunk", id, strconv.FormatInt(seq, 10), stream, l)
+}
+
+func summaryAAD(id string) []byte { return lengthPrefixed(Purpose, "summary", id) }
+
+func gcmFor(shared []byte) (cipher.AEAD, error) {
+ key := make([]byte, 32)
+ if _, err := io.ReadFull(hkdf.New(sha256.New, shared, nil, []byte(Purpose)), key); err != nil {
+ return nil, err
+ }
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, err
+ }
+ return cipher.NewGCM(block)
+}
+
+func seal(pub *ecdh.PublicKey, aad, plain []byte) ([]byte, error) {
+ eph, err := ecdh.P256().GenerateKey(randReader)
+ if err != nil {
+ return nil, err
+ }
+ nonce := make([]byte, sealNonceLen)
+ if _, err := io.ReadFull(randReader, nonce); err != nil {
+ return nil, err
+ }
+ shared, err := eph.ECDH(pub)
+ if err != nil {
+ return nil, err
+ }
+ gcm, err := gcmFor(shared)
+ if err != nil {
+ return nil, err
+ }
+ out := make([]byte, 0, SealOverhead+len(plain))
+ out = append(out, sealPrefix...)
+ out = append(out, eph.PublicKey().Bytes()...)
+ out = append(out, nonce...)
+ return gcm.Seal(out, nonce, plain, aad), nil
+}
+
+func open(priv *ecdh.PrivateKey, aad, sealed []byte) ([]byte, error) {
+ if len(sealed) < SealOverhead || string(sealed[:len(sealPrefix)]) != sealPrefix {
+ return nil, ErrSeal
+ }
+ body := sealed[len(sealPrefix):]
+ ephPub, err := ecdh.P256().NewPublicKey(body[:sealEphLen])
+ if err != nil {
+ return nil, ErrSeal
+ }
+ shared, err := priv.ECDH(ephPub)
+ if err != nil {
+ return nil, ErrSeal
+ }
+ gcm, err := gcmFor(shared)
+ if err != nil {
+ return nil, ErrSeal
+ }
+ plain, err := gcm.Open(nil, body[sealEphLen:sealEphLen+sealNonceLen], body[sealEphLen+sealNonceLen:], aad)
+ if err != nil {
+ return nil, ErrSeal
+ }
+ return plain, nil
+}
+
+// SealChunk seals one chunk of a read's output: bound to the read, its
+// sequence number, its stream and whether it is the last.
+func SealChunk(pub *ecdh.PublicKey, id string, seq int64, stream string, last bool, plain []byte) ([]byte, error) {
+ return seal(pub, chunkAAD(id, seq, stream, last), plain)
+}
+
+func OpenChunk(priv *ecdh.PrivateKey, id string, seq int64, stream string, last bool, sealed []byte) ([]byte, error) {
+ return open(priv, chunkAAD(id, seq, stream, last), sealed)
+}
+
+// SealSummary seals the result's summary (base64, it rides in result.json).
+func SealSummary(pub *ecdh.PublicKey, id string, s Summary) (string, error) {
+ raw, err := json.Marshal(s)
+ if err != nil {
+ return "", err
+ }
+ sealed, err := seal(pub, summaryAAD(id), raw)
+ if err != nil {
+ return "", err
+ }
+ return base64.StdEncoding.EncodeToString(sealed), nil
+}
+
+func OpenSummary(priv *ecdh.PrivateKey, id, sealed string) (Summary, error) {
+ raw, err := base64.StdEncoding.Strict().DecodeString(sealed)
+ if err != nil {
+ return Summary{}, ErrSeal
+ }
+ plain, err := open(priv, summaryAAD(id), raw)
+ if err != nil {
+ return Summary{}, err
+ }
+ var s Summary
+ if err := json.Unmarshal(plain, &s); err != nil {
+ return Summary{}, ErrSeal
+ }
+ return s, nil
+}
+```
+
+- [ ] **Step 4: Write `result.go`**
+
+```go
+package dbreads
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/carlosframework/platform/internal/dbdoor"
+)
+
+// How a read ended (result.json's state, spec §5.6).
+const (
+ StateDone = "done"
+ StateFailed = "failed"
+ StateRefused = "refused"
+ StateCancelled = "cancelled"
+)
+
+// The fixed code vocabulary the console may read.
+const (
+ CodeNothingReplicated = "nothing-replicated"
+ CodeHistoryGone = "history-gone"
+ CodeCheckFailed = "check-failed"
+ CodeChangedTwice = "changed-twice"
+ CodeTooLarge = "too-large"
+ CodeNoRoom = "no-room"
+ CodeSQLError = "sql-error"
+ CodeTimeout = "timeout"
+ CodeOutputCap = "output-cap"
+ CodeRecordChanged = "record-changed"
+ CodeMoving = "moving"
+ CodeInterrupted = "interrupted"
+)
+
+var codesByState = map[string]map[string]bool{
+ StateDone: {"": true, CodeOutputCap: true},
+ StateFailed: {CodeNothingReplicated: true, CodeHistoryGone: true, CodeCheckFailed: true, CodeChangedTwice: true, CodeTooLarge: true, CodeNoRoom: true, CodeSQLError: true, CodeTimeout: true, CodeInterrupted: true},
+ StateRefused: {CodeRecordChanged: true, CodeMoving: true},
+ StateCancelled: {"": true},
+}
+
+// Result is db-reads/<id>/result.json, written only after every chunk is
+// confirmed. LastSeq is the last chunk's sequence number; a done read has at
+// least one chunk, the last of which is sealed as last.
+type Result struct {
+ State string `json:"state"`
+ Code string `json:"code"`
+ Box string `json:"box"`
+ LastSeq int64 `json:"last_seq"`
+ Started time.Time `json:"started"`
+ Ended time.Time `json:"ended"`
+ Sealed string `json:"sealed"`
+}
+
+// Valid is the shape the console checks before it relays a result.
+func (r Result) Valid() error {
+ codes, ok := codesByState[r.State]
+ switch {
+ case !ok:
+ return fmt.Errorf("dbreads: unknown state %q", r.State)
+ case !codes[r.Code]:
+ return fmt.Errorf("dbreads: code %q is not one of state %s's", r.Code, r.State)
+ case r.LastSeq < 0, r.State == StateDone && r.LastSeq < 1:
+ return fmt.Errorf("dbreads: last_seq %d for state %s", r.LastSeq, r.State)
+ }
+ return nil
+}
+
+// Picked is db-reads/<id>/picked: the owner (J3).
+type Picked struct {
+ Box string `json:"box"`
+ Boot string `json:"boot"`
+ Nonce string `json:"nonce"`
+ Deadline time.Time `json:"deadline"`
+}
+
+// Summary is the result's sealed half, which only the CLI can open (§5.6).
+type Summary struct {
+ TXID uint64 `json:"txid"` // the copy's newest transaction
+ At time.Time `json:"at"` // that transaction's time, from its LTX header
+ Oldest time.Time `json:"oldest"` // history-gone: the earliest --at that works
+ Restored int64 `json:"restored"` // too-large: the restored size
+ Cap int64 `json:"cap"` // too-large: the box's cap
+ Error string `json:"error"` // sql-error: SQLite's text; check-failed: the detail
+ Bytes int64 `json:"bytes"` // the output's plaintext byte count
+ SHA256 string `json:"sha256"` // the output's plaintext SHA-256, hex
+ // Files is a pull's planned files in order (plan 3); empty for a query.
+ Files []dbdoor.LTXFile `json:"files,omitempty"`
+}
+```
+
+- [ ] **Step 5: Run the tests**
+
+Run: `go test ./internal/dbreads/ -count=1`
+Expected: PASS, including Task 4's tests.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add internal/dbreads/seal.go internal/dbreads/result.go internal/dbreads/seal_test.go
+git commit -m "dbreads: reply keys, bound chunk and summary sealing, the result"
+```
+
+---
+
+### Task 6: `dbdoor` planning over any replica reader, listed as the door lists
+
+**Files:**
+- Create: `internal/dbdoor/plan_from.go`
+- Create: `internal/dbdoor/plan_from_test.go`
+- Modify: `internal/dbdoor/plan.go` (shared tail; two new `RestorePlan` fields)
+- Modify: `internal/console/apidb.go:316-342` (`listReplicaLevel` delegates)
+
+**Interfaces:**
+- Consumes: `litestream.CalcRestorePlan`, `fileIterator`, `restoreOnce`, `removeCopy`, `checkDB`, `errReadOnly`, `EarliestAsOf`, `BeforeHistoryError` (all existing in `dbdoor`).
+- Produces: `dbdoor.ReplicaReader` (interface: `LTXFiles`, `OpenLTXFile`); `dbdoor.ListLevel(ctx context.Context, rd ReplicaReader, level int) (ListResponse, error)`; `dbdoor.PlanFrom(ctx context.Context, rd ReplicaReader, at time.Time) (RestorePlan, error)`; `dbdoor.RestoreLocal(ctx context.Context, c litestream.ReplicaClient, out string, target uint64) error`; `RestorePlan.Planned []LTXFile` and `RestorePlan.At time.Time`.
+
+- [ ] **Step 1: Write the failing test**
+
+Create `internal/dbdoor/plan_from_test.go`:
+
+```go
+package dbdoor
+
+import (
+ "context"
+ "errors"
+ "io"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/benbjohnson/litestream"
+ lsfile "github.com/benbjohnson/litestream/file"
+ "github.com/superfly/ltx"
+
+ "github.com/carlosframework/platform/internal/dbdoor/dbdoortest"
+)
+
+// metaSpy records every useMetadata the listing passes down: the box must
+// list LastModified as CreatedAt, exactly as the console's door does
+// (listReplicaLevel), never S3 metadata timestamps (spec §6, T6).
+type metaSpy struct {
+ ReplicaReader
+ sawMetadata bool
+}
+
+func (m *metaSpy) LTXFiles(ctx context.Context, level int, seek ltx.TXID, useMetadata bool) (ltx.FileIterator, error) {
+ if useMetadata {
+ m.sawMetadata = true
+ }
+ return m.ReplicaReader.LTXFiles(ctx, level, seek, useMetadata)
+}
+
+func TestListLevelIsTheDoorsListing(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ dbdoortest.Replicate(t, dir, 3)
+ spy := &metaSpy{ReplicaReader: lsfile.NewReplicaClient(dir)}
+ got, err := ListLevel(context.Background(), spy, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := collect(t, NewClient(newTestDoor(t, dir).do), 0, 0)
+ if len(got.Files) != len(want) || spy.sawMetadata {
+ t.Fatalf("ListLevel = %d files (metadata %v), the door lists %d", len(got.Files), spy.sawMetadata, len(want))
+ }
+ for i := range want {
+ f := got.Files[i]
+ if f.MinTXID != uint64(want[i].MinTXID) || f.MaxTXID != uint64(want[i].MaxTXID) || f.Size != want[i].Size || !f.CreatedAt.Equal(want[i].CreatedAt) {
+ t.Fatalf("file %d: %+v, the door lists %+v", i, f, want[i])
+ }
+ }
+ empty, err := ListLevel(context.Background(), spy, 5)
+ if err != nil || empty.Files == nil || len(empty.Files) != 0 {
+ t.Fatalf("an empty level = %+v, %v; want an empty list", empty, err)
+ }
+}
+
+// PlanFrom plans what RestoreAsOf through the door restores: the same
+// transaction, the same time, the same size, for the newest copy and as of
+// a time.
+func TestPlanFromMatchesTheDoorsRestore(t *testing.T) {
+ dir := t.TempDir()
+ src := dbdoortest.Replicate(t, dir, 2)
+ at := pause()
+ addBatch(t, src, dir, 2)
+ for _, when := range []time.Time{{}, at} {
+ plan, err := PlanFrom(context.Background(), lsfile.NewReplicaClient(dir), when)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got, err := RestoreAsOf(context.Background(), NewClient(newTestDoor(t, dir).do), t.TempDir(), when)
+ if err != nil {
+ t.Fatal(err)
+ }
+ st, err := os.Stat(got.Path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ os.RemoveAll(got.Dir)
+ if plan.TXID != got.TXID || !plan.At.Equal(got.At) || plan.RestoredBytes != st.Size() || len(plan.Planned) != plan.Files || plan.Files == 0 {
+ t.Fatalf("as of %v: plan %+v; the door restored txid %d at %v, %d bytes", when, plan, got.TXID, got.At, st.Size())
+ }
+ }
+}
+
+func TestPlanFromBeforeHistoryNamesTheOldestPoint(t *testing.T) {
+ t.Parallel()
+ before := time.Now().Add(-time.Hour)
+ dir := t.TempDir()
+ dbdoortest.Replicate(t, dir, 2)
+ _, err := PlanFrom(context.Background(), lsfile.NewReplicaClient(dir), before)
+ var bh *BeforeHistoryError
+ if !errors.As(err, &bh) {
+ t.Fatalf("PlanFrom before history = %v", err)
+ }
+ _, derr := RestoreAsOf(context.Background(), NewClient(newTestDoor(t, dir).do), t.TempDir(), before)
+ var dbh *BeforeHistoryError
+ if !errors.As(derr, &dbh) || !dbh.Oldest.Equal(bh.Oldest) {
+ t.Fatalf("the door says %v, the plan says %v", derr, bh.Oldest)
+ }
+}
+
+func TestPlanFromAnEmptyReplica(t *testing.T) {
+ t.Parallel()
+ _, err := PlanFrom(context.Background(), lsfile.NewReplicaClient(t.TempDir()), time.Time{})
+ if !errors.Is(err, ErrNothingReplicated) {
+ t.Fatalf("PlanFrom on nothing = %v", err)
+ }
+}
+
+// RestoreLocal restores exactly the planned files, copied into a fresh file
+// replica the way the worker writes them, to the plan's transaction.
+func TestRestoreLocalRestoresThePlan(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ dbdoortest.Replicate(t, dir, 4)
+ src := lsfile.NewReplicaClient(dir)
+ plan, err := PlanFrom(context.Background(), src, time.Time{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ root := t.TempDir()
+ for _, f := range plan.Planned {
+ rc, err := src.OpenLTXFile(context.Background(), f.Level, ltx.TXID(f.MinTXID), ltx.TXID(f.MaxTXID), 0, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ path := litestream.LTXFilePath(root, f.Level, ltx.TXID(f.MinTXID), ltx.TXID(f.MaxTXID))
+ if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
+ t.Fatal(err)
+ }
+ out, err := os.Create(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := io.Copy(out, rc); err != nil {
+ t.Fatal(err)
+ }
+ rc.Close()
+ out.Close()
+ }
+ db := filepath.Join(t.TempDir(), "copy.db")
+ if err := RestoreLocal(context.Background(), lsfile.NewReplicaClient(root), db, plan.TXID); err != nil {
+ t.Fatal(err)
+ }
+ if rows := countRows(t, db); rows != 4*dbdoortest.Rows {
+ t.Fatalf("restored %d rows", rows)
+ }
+ st, _ := os.Stat(db)
+ if st.Size() != plan.RestoredBytes {
+ t.Fatalf("restored %d bytes, planned %d", st.Size(), plan.RestoredBytes)
+ }
+}
+```
+
+- [ ] **Step 2: Run it to verify it fails**
+
+Run: `go test ./internal/dbdoor/ -run 'TestListLevel|TestPlanFrom|TestRestoreLocal' -count=1`
+Expected: FAIL, undefined `ListLevel`, `PlanFrom`, `RestoreLocal`, `ReplicaReader`.
+
+- [ ] **Step 3: Refactor `plan.go` around a shared tail**
+
+In `internal/dbdoor/plan.go`, add the two fields to `RestorePlan`:
+
+```go
+ // Planned is the plan's files in restore order (box side: what the
+ // worker is sent, spec §8.2).
+ Planned []LTXFile
+ // At is the last planned file's header time: the time of the copy's
+ // newest transaction, as RestoreAsOf's Restored.At reports it.
+ At time.Time
+```
+
+and replace everything in `PlanRestore` after the `switch` with a call to a new shared tail, so both planners read and check the last file's header the same way:
+
+```go
+ return finishPlan(ctx, c, infos, target)
+}
+
+// finishPlan sums the plan, records its files, and reads and bounds the
+// last planned file's header: the size check's one decode (spec §8.1).
+func finishPlan(ctx context.Context, c litestream.ReplicaClient, infos []*ltx.FileInfo, target uint64) (RestorePlan, error) {
+ p := RestorePlan{Target: target, Files: len(infos)}
+ for _, f := range infos {
+ p.LTXBytes += f.Size
+ p.Planned = append(p.Planned, LTXFile{Level: f.Level, MinTXID: uint64(f.MinTXID), MaxTXID: uint64(f.MaxTXID), Size: f.Size, CreatedAt: f.CreatedAt})
+ }
+ last := infos[len(infos)-1]
+ p.TXID = uint64(last.MaxTXID)
+ rc, err := c.OpenLTXFile(ctx, last.Level, last.MinTXID, last.MaxTXID, 0, ltx.HeaderSize)
+ if err != nil {
+ return RestorePlan{}, fmt.Errorf("dbdoor: read the last planned file's header: %w", err)
+ }
+ defer rc.Close()
+ buf := make([]byte, ltx.HeaderSize)
+ if _, err := io.ReadFull(rc, buf); err != nil {
+ return RestorePlan{}, fmt.Errorf("dbdoor: read the last planned file's header: %w", err)
+ }
+ var h ltx.Header
+ if err := h.UnmarshalBinary(buf); err != nil {
+ return RestorePlan{}, fmt.Errorf("dbdoor: the last planned file's header: %w", err)
+ }
+ if !ltx.IsValidPageSize(h.PageSize) || h.Commit == 0 || h.MinTXID != last.MinTXID || h.MaxTXID != last.MaxTXID {
+ return RestorePlan{}, fmt.Errorf("dbdoor: the last planned file's header does not describe it (level %d %s: page size %d, commit %d, txids %s)",
+ last.Level, Span(uint64(last.MinTXID), uint64(last.MaxTXID)), h.PageSize, h.Commit, Span(uint64(h.MinTXID), uint64(h.MaxTXID)))
+ }
+ p.RestoredBytes = int64(h.Commit) * int64(h.PageSize)
+ p.At = time.UnixMilli(h.Timestamp).UTC()
+ return p, nil
+}
+```
+
+Keep the comment block above the bounds check that explains why the header is untrusted. `PlanRestore`'s existing tests must still pass unchanged.
+
+- [ ] **Step 4: Write `plan_from.go`**
+
+```go
+package dbdoor
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "log/slog"
+ "sort"
+ "sync"
+ "time"
+
+ "github.com/benbjohnson/litestream"
+ "github.com/superfly/ltx"
+)
+
+// ReplicaReader is the read half of a litestream replica client: the two
+// methods a door or a planner may hold. A *lss3.ReplicaClient from
+// lease.ReadReplicaClient is one; so is a file replica client.
+type ReplicaReader interface {
+ LTXFiles(ctx context.Context, level int, seek ltx.TXID, useMetadata bool) (ltx.FileIterator, error)
+ OpenLTXFile(ctx context.Context, level int, minTXID, maxTXID ltx.TXID, offset, size int64) (io.ReadCloser, error)
+}
+
+// ListLevel lists one level of rd in the door's wire shape, ordered by
+// MinTXID, with an empty level as an empty list, and with S3's LastModified
+// as CreatedAt (useMetadata false). The console's door and the box's planner
+// both list through it, so a forwarded --at stops at the transaction an
+// in-region one does (spec §6, T6).
+func ListLevel(ctx context.Context, rd ReplicaReader, level int) (ListResponse, error) {
+ itr, err := rd.LTXFiles(ctx, level, 0, false)
+ if err != nil {
+ return ListResponse{}, err
+ }
+ out := ListResponse{Files: []LTXFile{}}
+ for itr.Next() {
+ f := itr.Item()
+ out.Files = append(out.Files, LTXFile{
+ Level: level, MinTXID: uint64(f.MinTXID), MaxTXID: uint64(f.MaxTXID),
+ Size: f.Size, CreatedAt: f.CreatedAt,
+ })
+ }
+ err = itr.Err()
+ if cerr := itr.Close(); err == nil {
+ err = cerr
+ }
+ if err != nil {
+ return ListResponse{}, err
+ }
+ sort.Slice(out.Files, func(i, j int) bool { return out.Files[i].MinTXID < out.Files[j].MinTXID })
+ return out, nil
+}
+
+// listing is a read-only litestream.ReplicaClient over rd that lists through
+// ListLevel. For an as-of plan it also does what the CLI's recorder does
+// over the door's listings: refuse a listing with no times (ErrNoTimes) and
+// remember the earliest file a restore could start from.
+type listing struct {
+ rd ReplicaReader
+ asOf bool
+ mu sync.Mutex
+ oldest time.Time
+}
+
+func (l *listing) Type() string { return "carlos-db-listing" }
+func (l *listing) Init(context.Context) error { return nil }
+func (l *listing) SetLogger(*slog.Logger) {}
+
+func (l *listing) LTXFiles(ctx context.Context, level int, seek ltx.TXID, _ bool) (ltx.FileIterator, error) {
+ out, err := ListLevel(ctx, l.rd, level)
+ if err != nil {
+ return nil, err
+ }
+ if l.asOf {
+ l.mu.Lock()
+ defer l.mu.Unlock()
+ for _, f := range out.Files {
+ if f.CreatedAt.IsZero() {
+ return nil, ErrNoTimes
+ }
+ if level == litestream.SnapshotLevel || f.MinTXID == 1 {
+ if l.oldest.IsZero() || f.CreatedAt.Before(l.oldest) {
+ l.oldest = f.CreatedAt
+ }
+ }
+ }
+ }
+ return fileIterator(out.Files, level, seek), nil
+}
+
+func (l *listing) OpenLTXFile(ctx context.Context, level int, minTXID, maxTXID ltx.TXID, offset, size int64) (io.ReadCloser, error) {
+ return l.rd.OpenLTXFile(ctx, level, minTXID, maxTXID, offset, size)
+}
+
+func (l *listing) WriteLTXFile(context.Context, int, ltx.TXID, ltx.TXID, io.Reader) (*ltx.FileInfo, error) {
+ return nil, errReadOnly
+}
+
+func (l *listing) DeleteLTXFiles(context.Context, []*ltx.FileInfo) error { return errReadOnly }
+
+func (l *listing) DeleteAll(context.Context) error { return errReadOnly }
+
+var _ litestream.ReplicaClient = (*listing)(nil)
+
+// PlanFrom plans the restore RestoreAsOf would make through the door, from
+// rd directly: the newest copy when at is zero, else the copy holding every
+// file listed before at rounded down to its second. Its errors are
+// RestoreAsOf's: ErrNothingReplicated, a *BeforeHistoryError naming the
+// oldest point kept, ErrNoTimes.
+func PlanFrom(ctx context.Context, rd ReplicaReader, at time.Time) (RestorePlan, error) {
+ l := &listing{rd: rd, asOf: !at.IsZero()}
+ var ts time.Time
+ if !at.IsZero() {
+ if at.Truncate(time.Second).Before(EarliestAsOf) {
+ at = EarliestAsOf
+ }
+ ts = at.Truncate(time.Second)
+ }
+ infos, err := litestream.CalcRestorePlan(ctx, l, 0, ts, slog.Default())
+ switch {
+ case errors.Is(err, ErrNoTimes):
+ return RestorePlan{}, ErrNoTimes
+ case errors.Is(err, litestream.ErrTxNotAvailable) || errors.Is(err, litestream.ErrNoSnapshots):
+ l.mu.Lock()
+ oldest := l.oldest
+ l.mu.Unlock()
+ if !ts.IsZero() && !oldest.IsZero() {
+ if !oldest.Before(ts) {
+ return RestorePlan{}, &BeforeHistoryError{Oldest: oldest.Truncate(time.Second).Add(time.Second)}
+ }
+ return RestorePlan{}, err
+ }
+ return RestorePlan{}, ErrNothingReplicated
+ case err != nil:
+ return RestorePlan{}, err
+ case len(infos) == 0:
+ return RestorePlan{}, ErrNothingReplicated
+ }
+ return finishPlan(ctx, l, infos, 0)
+}
+
+// RestoreLocal restores the replica c holds, a litestream file replica in a
+// private directory (the db-read worker's, or the CLI's for a forwarded
+// pull), exactly to target, at out, and proves it with lease.CheckDBContext.
+// On failure nothing is left at out.
+func RestoreLocal(ctx context.Context, c litestream.ReplicaClient, out string, target uint64) error {
+ if err := restoreOnce(ctx, c, out, target, time.Time{}); err != nil {
+ removeCopy(out)
+ if cerr := ctx.Err(); cerr != nil {
+ return cerr
+ }
+ return err
+ }
+ if err := checkDB(ctx, out); err != nil {
+ removeCopy(out)
+ if cerr := ctx.Err(); cerr != nil {
+ return cerr
+ }
+ return fmt.Errorf("%w: %v", ErrCheckFailed, err)
+ }
+ if err := ctx.Err(); err != nil {
+ removeCopy(out)
+ return err
+ }
+ return nil
+}
+```
+
+In `internal/console/apidb.go`, replace `listReplicaLevel`'s body with `return dbdoor.ListLevel(ctx, rd, level)` and keep its doc comment, adding one line: "The body moved to dbdoor.ListLevel so the box's planner lists through it too (db region forward spec §6)." Drop the now unused `sort` import if nothing else in the file uses it.
+
+- [ ] **Step 5: Run the tests**
+
+Run: `go test ./internal/dbdoor/ -count=1 && go test ./internal/console/ -run 'TestAPIDB|TestDBDoor|TestSeed' -count=1`
+Expected: PASS. The `dbdoor` package is small enough to run whole; for the console, only the db door's and the seed door's tests (list them with `grep -ln 'listReplicaLevel\|handleAPIDBList' internal/console/*_test.go` and `-run` their names if the pattern above matches none).
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add internal/dbdoor internal/console/apidb.go
+git commit -m "dbdoor: ListLevel, PlanFrom and RestoreLocal; the console door lists through ListLevel"
+```
+
+---
+
+### Task 7: The worker protocol and `ServeWorker`
+
+**Files:**
+- Create: `internal/dbreads/protocol.go`
+- Create: `internal/dbreads/worker.go`
+- Create: `internal/dbreads/worker_test.go`
+
+**Interfaces:**
+- Consumes: `dbdoor.LTXFile`, `dbdoor.RestoreLocal`, `dbdoor.ErrCheckFailed`, `dbquery.RunContext`, `dbquery.Options`, `litestream.LTXFilePath`, `lsfile.NewReplicaClient`.
+- Produces: `ProtocolVersion`; frame bytes `frameHello`, `frameHelloReply`, `frameHeader`, `frameFile`, `frameReady`, `frameRun`, `frameStop`, `frameOutput`, `frameEnd`; `writeFrame(w io.Writer, typ byte, p []byte) error`, `writeJSONFrame(w io.Writer, typ byte, v any) error`, `readFrame(r io.Reader) (byte, []byte, error)`; message types `hello`, `HelloReply`, `planHeader`, `readyMsg`, `runMsg`, `endMsg`; `type WorkerOptions struct { Scratch string; QueryTimeout, Grace time.Duration; Exit func(int); Self func() HelloReply; Restore func(ctx context.Context, c litestream.ReplicaClient, out string, target uint64) error }`; `ServeWorker(conn io.ReadWriter, opt WorkerOptions) error`; `SelfView() HelloReply`.
+
+- [ ] **Step 1: Write the failing test**
+
+Create `internal/dbreads/worker_test.go`. These tests drive the real worker code over `net.Pipe`: not acceptance (Task 15 is); they pin the protocol.
+
+```go
+package dbreads
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "io"
+ "net"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/benbjohnson/litestream"
+ lsfile "github.com/benbjohnson/litestream/file"
+ "github.com/superfly/ltx"
+
+ "github.com/carlosframework/platform/internal/dbdoor"
+ "github.com/carlosframework/platform/internal/dbdoor/dbdoortest"
+ "github.com/carlosframework/platform/internal/dbquery"
+)
+
+// workerPipe starts ServeWorker on one end of a pipe and returns the other.
+// Not acceptance: the units are Task 15's.
+func workerPipe(t *testing.T, opt WorkerOptions) (net.Conn, <-chan error) {
+ t.Helper()
+ agent, worker := net.Pipe()
+ if opt.Scratch == "" {
+ opt.Scratch = t.TempDir()
+ }
+ if opt.Exit == nil {
+ opt.Exit = func(int) { t.Error("the worker asked to exit the process") }
+ }
+ done := make(chan error, 1)
+ go func() {
+ err := ServeWorker(worker, opt)
+ worker.Close()
+ done <- err
+ }()
+ t.Cleanup(func() { agent.Close() })
+ return agent, done
+}
+
+// sendPlan sends a header and the planned files of dir's replica.
+func sendPlan(t *testing.T, conn net.Conn, dir string) dbdoor.RestorePlan {
+ t.Helper()
+ rd := lsfile.NewReplicaClient(dir)
+ plan, err := dbdoor.PlanFrom(context.Background(), rd, time.Time{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := writeJSONFrame(conn, frameHeader, planHeader{V: ProtocolVersion, Op: OpQuery, Files: plan.Planned, Target: plan.TXID, MaxRestored: DefaultMaxRestored}); err != nil {
+ t.Fatal(err)
+ }
+ for _, f := range plan.Planned {
+ rc, err := rd.OpenLTXFile(context.Background(), f.Level, ltx.TXID(f.MinTXID), ltx.TXID(f.MaxTXID), 0, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ body, _ := io.ReadAll(rc)
+ rc.Close()
+ if err := writeFrame(conn, frameFile, body); err != nil {
+ t.Fatal(err)
+ }
+ }
+ return plan
+}
+
+func readTyped(t *testing.T, conn net.Conn, want byte, v any) {
+ t.Helper()
+ typ, p, err := readFrame(conn)
+ if err != nil || typ != want {
+ t.Fatalf("frame %q (%s), want %q: %v", typ, p, want, err)
+ }
+ if v != nil {
+ if err := json.Unmarshal(p, v); err != nil {
+ t.Fatal(err)
+ }
+ }
+}
+
+func TestWorkerAnswersHelloWithItsSelfView(t *testing.T) {
+ t.Parallel()
+ conn, done := workerPipe(t, WorkerOptions{Self: func() HelloReply {
+ return HelloReply{V: ProtocolVersion, Interfaces: []string{"lo"}}
+ }})
+ if err := writeJSONFrame(conn, frameHello, hello{V: ProtocolVersion}); err != nil {
+ t.Fatal(err)
+ }
+ var h HelloReply
+ readTyped(t, conn, frameHelloReply, &h)
+ if h.V != ProtocolVersion || len(h.Interfaces) != 1 {
+ t.Fatalf("hello = %+v", h)
+ }
+ if err := <-done; err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestWorkerRestoresThenRunsOnlyWhenTold(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ dbdoortest.Replicate(t, dir, 3)
+ conn, done := workerPipe(t, WorkerOptions{})
+ plan := sendPlan(t, conn, dir)
+ var ready readyMsg
+ readTyped(t, conn, frameReady, &ready)
+ if ready.TXID != plan.TXID {
+ t.Fatalf("ready at %d, planned %d", ready.TXID, plan.TXID)
+ }
+ if err := writeJSONFrame(conn, frameRun, runMsg{SQL: "SELECT count(*) AS n FROM t"}); err != nil {
+ t.Fatal(err)
+ }
+ var out bytes.Buffer
+ for {
+ typ, p, err := readFrame(conn)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if typ == frameOutput {
+ out.Write(p)
+ continue
+ }
+ var end endMsg
+ if typ != frameEnd || json.Unmarshal(p, &end) != nil || end.Code != "" {
+ t.Fatalf("end %q %s", typ, p)
+ }
+ break
+ }
+ if out.String() != "n\n30\n" {
+ t.Fatalf("output %q", out.String())
+ }
+ if err := <-done; err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestWorkerStopRunsNothing(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ dbdoortest.Replicate(t, dir, 1)
+ conn, done := workerPipe(t, WorkerOptions{})
+ sendPlan(t, conn, dir)
+ readTyped(t, conn, frameReady, nil)
+ if err := writeFrame(conn, frameStop, nil); err != nil {
+ t.Fatal(err)
+ }
+ if err := <-done; err != nil {
+ t.Fatal(err)
+ }
+}
+
+// The worker refuses a file larger than its header said (spec §8.3).
+func TestWorkerRefusesAFileLargerThanItsHeader(t *testing.T) {
+ t.Parallel()
+ conn, done := workerPipe(t, WorkerOptions{})
+ files := []dbdoor.LTXFile{{Level: 0, MinTXID: 1, MaxTXID: 1, Size: 4}}
+ if err := writeJSONFrame(conn, frameHeader, planHeader{V: ProtocolVersion, Op: OpQuery, Files: files, Target: 1, MaxRestored: DefaultMaxRestored}); err != nil {
+ t.Fatal(err)
+ }
+ go writeFrame(conn, frameFile, []byte("12345"))
+ var end endMsg
+ readTyped(t, conn, frameEnd, &end)
+ if end.Code != CodeCheckFailed {
+ t.Fatalf("end = %+v", end)
+ }
+ <-done
+}
+
+func TestWorkerSQLErrorIsSQLitesText(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ dbdoortest.Replicate(t, dir, 1)
+ conn, done := workerPipe(t, WorkerOptions{})
+ sendPlan(t, conn, dir)
+ readTyped(t, conn, frameReady, nil)
+ writeJSONFrame(conn, frameRun, runMsg{SQL: "SELECT * FROM nope"})
+ var end endMsg
+ readTyped(t, conn, frameEnd, &end)
+ if end.Code != CodeSQLError || end.Error == "" {
+ t.Fatalf("end = %+v", end)
+ }
+ if err := dbquery.CheckSQL("SELECT * FROM nope"); err != nil {
+ t.Fatal(err)
+ }
+ <-done
+}
+
+func TestWorkerQueryTimeout(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ dbdoortest.Replicate(t, dir, 1)
+ conn, done := workerPipe(t, WorkerOptions{QueryTimeout: 50 * time.Millisecond})
+ sendPlan(t, conn, dir)
+ readTyped(t, conn, frameReady, nil)
+ writeJSONFrame(conn, frameRun, runMsg{SQL: "WITH RECURSIVE c(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM c) SELECT count(*) FROM c"})
+ for {
+ typ, p, err := readFrame(conn)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if typ == frameEnd {
+ var end endMsg
+ json.Unmarshal(p, &end)
+ if end.Code != CodeTimeout {
+ t.Fatalf("end = %+v", end)
+ }
+ break
+ }
+ }
+ <-done
+}
+
+// The worker stops when its agent goes (spec §8.3, codex 8): EOF cancels the
+// context, and a step that ignores the context is ended by the exit after
+// the grace.
+func TestWorkerExitsWhenTheAgentGoesMidRestore(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ dbdoortest.Replicate(t, dir, 1)
+ var exited atomic.Int32
+ hang := make(chan struct{})
+ t.Cleanup(func() { close(hang) })
+ conn, _ := workerPipe(t, WorkerOptions{
+ Grace: 50 * time.Millisecond,
+ Exit: func(code int) { exited.Store(int32(code)) },
+ Restore: func(context.Context, litestream.ReplicaClient, string, uint64) error {
+ <-hang // a compaction that ignores its context
+ return nil
+ },
+ })
+ sendPlan(t, conn, dir)
+ conn.Close()
+ deadline := time.Now().Add(5 * time.Second)
+ for exited.Load() == 0 && time.Now().Before(deadline) {
+ time.Sleep(10 * time.Millisecond)
+ }
+ if exited.Load() == 0 {
+ t.Fatal("the worker outlived its agent past the grace")
+ }
+}
+
+func TestWorkerCheckFailsAMissingFile(t *testing.T) {
+ t.Parallel()
+ conn, done := workerPipe(t, WorkerOptions{})
+ if err := writeJSONFrame(conn, frameHeader, planHeader{V: ProtocolVersion, Op: OpQuery, Files: nil, Target: 9, MaxRestored: DefaultMaxRestored}); err != nil {
+ t.Fatal(err)
+ }
+ var end endMsg
+ readTyped(t, conn, frameEnd, &end)
+ if end.Code != CodeCheckFailed {
+ t.Fatalf("an empty plan = %+v", end)
+ }
+ <-done
+}
+```
+
+- [ ] **Step 2: Run it to verify it fails**
+
+Run: `go test ./internal/dbreads/ -run 'TestWorker' -count=1`
+Expected: FAIL, undefined names.
+
+- [ ] **Step 3: Write `protocol.go`**
+
+```go
+package dbreads
+
+import (
+ "encoding/binary"
+ "encoding/json"
+ "errors"
+ "io"
+
+ "github.com/carlosframework/platform/internal/dbdoor"
+)
+
+// ProtocolVersion is the worker protocol's version, answered in hello. A
+// worker that speaks another one never advertises anything (spec §7.1).
+const ProtocolVersion = 1
+
+// Frames are one type byte, a four-byte big-endian length and the payload,
+// at most maxFrame bytes (spec §8.3: length-framed with a fixed maximum).
+const (
+ maxFrame = 1 << 20
+ fileFrameBytes = 256 << 10
+ outputFrameBytes = 64 << 10
+)
+
+const (
+ frameHello byte = 'H' // agent: the readiness probe
+ frameHelloReply byte = 'h' // worker: its version and its view of its sandbox
+ frameHeader byte = 'P' // agent: the plan
+ frameFile byte = 'F' // agent: file bytes, in the header's order
+ frameReady byte = 'R' // worker: restored and checked; runs nothing yet
+ frameRun byte = 'X' // agent: check B passed; the statement
+ frameStop byte = 'S' // agent: end without running
+ frameOutput byte = 'O' // worker: output bytes
+ frameEnd byte = 'E' // worker: how it ended
+)
+
+var (
+ errFrameTooLarge = errors.New("dbreads: frame over the protocol's maximum")
+ errProtocol = errors.New("dbreads: unexpected frame")
+)
+
+type hello struct {
+ V int `json:"v"`
+}
+
+// HelloReply is the worker's answer to the probe: its protocol version and
+// what it can see of the box (spec §7.1). A sandboxed worker sees only the
+// loopback interface, an empty /data and no /etc/carlos.
+type HelloReply struct {
+ V int `json:"v"`
+ Interfaces []string `json:"interfaces"`
+ DataEntries int `json:"data_entries"`
+ EtcReadable bool `json:"etc_readable"`
+}
+
+type planHeader struct {
+ V int `json:"v"`
+ Op string `json:"op"`
+ Files []dbdoor.LTXFile `json:"files"`
+ Target uint64 `json:"target"`
+ MaxRestored int64 `json:"max_restored"`
+}
+
+type readyMsg struct {
+ TXID uint64 `json:"txid"`
+}
+
+type runMsg struct {
+ SQL string `json:"sql"`
+}
+
+type endMsg struct {
+ Code string `json:"code"`
+ Error string `json:"error"`
+}
+
+// writeFrame writes one frame in a single Write.
+func writeFrame(w io.Writer, typ byte, p []byte) error {
+ if len(p) > maxFrame {
+ return errFrameTooLarge
+ }
+ buf := make([]byte, 5, 5+len(p))
+ buf[0] = typ
+ binary.BigEndian.PutUint32(buf[1:], uint32(len(p)))
+ _, err := w.Write(append(buf, p...))
+ return err
+}
+
+func writeJSONFrame(w io.Writer, typ byte, v any) error {
+ p, err := json.Marshal(v)
+ if err != nil {
+ return err
+ }
+ return writeFrame(w, typ, p)
+}
+
+func readFrame(r io.Reader) (byte, []byte, error) {
+ var hdr [5]byte
+ if _, err := io.ReadFull(r, hdr[:]); err != nil {
+ return 0, nil, err
+ }
+ n := binary.BigEndian.Uint32(hdr[1:])
+ if n > maxFrame {
+ return 0, nil, errFrameTooLarge
+ }
+ p := make([]byte, n)
+ if _, err := io.ReadFull(r, p); err != nil {
+ return 0, nil, err
+ }
+ return hdr[0], p, nil
+}
+```
+
+- [ ] **Step 4: Write `worker.go`**
+
+```go
+package dbreads
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/benbjohnson/litestream"
+ lsfile "github.com/benbjohnson/litestream/file"
+ "github.com/superfly/ltx"
+
+ "github.com/carlosframework/platform/internal/dbdoor"
+ "github.com/carlosframework/platform/internal/dbquery"
+)
+
+// WorkerOptions are ServeWorker's settings. Production (RunWorker) sets
+// Scratch and leaves the rest zero.
+type WorkerOptions struct {
+ // Scratch is where the read's private directory goes: /var/tmp, the
+ // unit's own PrivateTmp, which systemd removes at stop.
+ Scratch string
+ // QueryTimeout is QueryTimeout when zero; Grace is WorkerExitGrace.
+ QueryTimeout time.Duration
+ Grace time.Duration
+ // Exit ends the process (os.Exit when nil): the 5 s backstop for a step
+ // that ignores its context (spec §8.3, codex 8).
+ Exit func(code int)
+ // Self is the hello's view of the sandbox (SelfView when nil).
+ Self func() HelloReply
+ // Restore is dbdoor.RestoreLocal when nil. Tests replace it.
+ Restore func(ctx context.Context, c litestream.ReplicaClient, out string, target uint64) error
+}
+
+type frame struct {
+ typ byte
+ p []byte
+}
+
+var errAgentGone = errors.New("dbreads: the agent closed the connection")
+
+// ServeWorker is the worker's whole job on one connection (spec §8.3): a
+// hello, or a plan, its files, a restore, ready, then run or stop. One
+// goroutine reads the connection from the start; EOF or an error cancels
+// the one context everything runs under, and if the work has not returned
+// Grace later, the process exits.
+func ServeWorker(conn io.ReadWriter, opt WorkerOptions) error {
+ if opt.QueryTimeout == 0 {
+ opt.QueryTimeout = QueryTimeout
+ }
+ if opt.Grace == 0 {
+ opt.Grace = WorkerExitGrace
+ }
+ if opt.Exit == nil {
+ opt.Exit = os.Exit
+ }
+ if opt.Self == nil {
+ opt.Self = SelfView
+ }
+ if opt.Restore == nil {
+ opt.Restore = dbdoor.RestoreLocal
+ }
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ frames := make(chan frame)
+ done := make(chan struct{})
+ defer close(done)
+ go func() {
+ defer close(frames)
+ for {
+ typ, p, err := readFrame(conn)
+ if err != nil {
+ cancel()
+ select {
+ case <-done:
+ case <-time.After(opt.Grace):
+ opt.Exit(3)
+ }
+ return
+ }
+ select {
+ case frames <- frame{typ, p}:
+ case <-done:
+ return
+ }
+ }
+ }()
+ next := func() (frame, error) {
+ select {
+ case f, ok := <-frames:
+ if !ok {
+ return frame{}, errAgentGone
+ }
+ return f, nil
+ case <-ctx.Done():
+ return frame{}, errAgentGone
+ }
+ }
+ end := func(code string, err error) error {
+ msg := endMsg{Code: code}
+ if err != nil {
+ msg.Error = err.Error()
+ }
+ return writeJSONFrame(conn, frameEnd, msg)
+ }
+
+ f, err := next()
+ if err != nil {
+ return err
+ }
+ switch f.typ {
+ case frameHello:
+ var h hello
+ if json.Unmarshal(f.p, &h) != nil {
+ return errProtocol
+ }
+ return writeJSONFrame(conn, frameHelloReply, opt.Self())
+ case frameHeader:
+ default:
+ return errProtocol
+ }
+ var h planHeader
+ if err := json.Unmarshal(f.p, &h); err != nil || h.V != ProtocolVersion || h.Op != OpQuery {
+ return errProtocol
+ }
+ dir, err := os.MkdirTemp(opt.Scratch, "dbread-")
+ if err != nil {
+ return err
+ }
+ defer os.RemoveAll(dir)
+ root := filepath.Join(dir, "replica")
+ for _, lf := range h.Files {
+ path := litestream.LTXFilePath(root, lf.Level, ltx.TXID(lf.MinTXID), ltx.TXID(lf.MaxTXID))
+ if err := receiveFile(next, path, lf.Size); err != nil {
+ if errors.Is(err, errAgentGone) {
+ return err
+ }
+ return end(CodeCheckFailed, err)
+ }
+ }
+ db := filepath.Join(dir, "copy.db")
+ if err := opt.Restore(ctx, lsfile.NewReplicaClient(root), db, h.Target); err != nil {
+ if ctx.Err() != nil {
+ return errAgentGone
+ }
+ return end(CodeCheckFailed, err)
+ }
+ if st, err := os.Stat(db); err != nil || st.Size() > h.MaxRestored {
+ return end(CodeTooLarge, err)
+ }
+ if err := writeJSONFrame(conn, frameReady, readyMsg{TXID: h.Target}); err != nil {
+ return err
+ }
+ f, err = next()
+ if err != nil {
+ return err
+ }
+ switch f.typ {
+ case frameStop:
+ return nil
+ case frameRun:
+ default:
+ return errProtocol
+ }
+ var run runMsg
+ if err := json.Unmarshal(f.p, &run); err != nil {
+ return errProtocol
+ }
+ qctx, qcancel := context.WithTimeout(ctx, opt.QueryTimeout)
+ defer qcancel()
+ out := &frameWriter{w: conn}
+ qerr := dbquery.RunContext(qctx, out, db, run.SQL, dbquery.Options{Immutable: true})
+ if ferr := out.flush(); ferr != nil {
+ return ferr
+ }
+ switch {
+ case qerr == nil:
+ return end("", nil)
+ case errors.Is(qctx.Err(), context.DeadlineExceeded):
+ return end(CodeTimeout, nil)
+ case ctx.Err() != nil:
+ return errAgentGone
+ }
+ return end(CodeSQLError, qerr)
+}
+
+// receiveFile writes exactly size bytes of file frames to path, refusing a
+// file larger than its header said (spec §8.3).
+func receiveFile(next func() (frame, error), path string, size int64) error {
+ if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
+ return err
+ }
+ out, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
+ if err != nil {
+ return err
+ }
+ defer out.Close()
+ var got int64
+ for got < size {
+ f, err := next()
+ if err != nil {
+ return err
+ }
+ if f.typ != frameFile {
+ return errProtocol
+ }
+ if got+int64(len(f.p)) > size {
+ return fmt.Errorf("dbreads: %s is larger than its header said (%d bytes)", filepath.Base(path), size)
+ }
+ if _, err := out.Write(f.p); err != nil {
+ return err
+ }
+ got += int64(len(f.p))
+ }
+ return out.Close()
+}
+
+// frameWriter frames the query's output in outputFrameBytes pieces.
+type frameWriter struct {
+ w io.Writer
+ buf []byte
+}
+
+func (f *frameWriter) Write(p []byte) (int, error) {
+ f.buf = append(f.buf, p...)
+ for len(f.buf) >= outputFrameBytes {
+ if err := writeFrame(f.w, frameOutput, f.buf[:outputFrameBytes]); err != nil {
+ return 0, err
+ }
+ f.buf = f.buf[outputFrameBytes:]
+ }
+ return len(p), nil
+}
+
+func (f *frameWriter) flush() error {
+ if len(f.buf) == 0 {
+ return nil
+ }
+ err := writeFrame(f.w, frameOutput, f.buf)
+ f.buf = nil
+ return err
+}
+
+// SelfView is what the worker can see of the box: in the unit, the loopback
+// interface alone, an empty /data and no readable /etc/carlos.
+func SelfView() HelloReply {
+ h := HelloReply{V: ProtocolVersion}
+ if ifs, err := net.Interfaces(); err == nil {
+ for _, i := range ifs {
+ h.Interfaces = append(h.Interfaces, i.Name)
+ }
+ }
+ if ents, err := os.ReadDir("/data"); err == nil {
+ h.DataEntries = len(ents)
+ }
+ if f, err := os.Open("/etc/carlos/host.env"); err == nil {
+ f.Close()
+ h.EtcReadable = true
+ }
+ return h
+}
+```
+
+- [ ] **Step 5: Run the tests**
+
+Run (sandbox off: `net.Pipe` is fine inside, but the replicas need the Go cache): `go test ./internal/dbreads/ -run 'TestWorker' -count=1 -race`
+Expected: PASS.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add internal/dbreads/protocol.go internal/dbreads/worker.go internal/dbreads/worker_test.go
+git commit -m "dbreads: the worker protocol and ServeWorker, with the 5 s exit"
+```
+
+---
+
+### Task 8: `carlos system db-worker`
+
+**Files:**
+- Create: `internal/dbreads/worker_linux.go`
+- Create: `internal/dbreads/worker_other.go`
+- Create: `internal/dbreads/worker_linux_test.go`
+- Modify: `system.go:52` (verb list) and `system.go:59-110` (dispatch)
+
+**Interfaces:**
+- Consumes: `ServeWorker`, `WorkerOptions` (Task 7).
+- Produces: `RunWorker(args []string)`; `RunWorkerWith(opt WorkerOptions)`; `StdinConn(f *os.File, wantUID func() (uint32, error)) (net.Conn, error)`; `CarlosUID() (uint32, error)`.
+
+- [ ] **Step 1: Write the failing test**
+
+Create `internal/dbreads/worker_linux_test.go`:
+
+```go
+//go:build linux
+
+package dbreads
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "golang.org/x/sys/unix"
+)
+
+func me() (uint32, error) { return uint32(os.Getuid()), nil }
+
+func other() (uint32, error) { return uint32(os.Getuid()) + 1, nil }
+
+func socketpairFile(t *testing.T, typ int) (*os.File, *os.File) {
+ t.Helper()
+ fds, err := unix.Socketpair(unix.AF_UNIX, typ, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ a, b := os.NewFile(uintptr(fds[0]), "a"), os.NewFile(uintptr(fds[1]), "b")
+ t.Cleanup(func() { a.Close(); b.Close() })
+ return a, b
+}
+
+// The worker runs only on a connected unix stream socket whose peer is the
+// agent's user (spec §8.3): it cannot be started by hand or pointed at a
+// file.
+func TestStdinConnRefusesWhatIsNotTheAgentsSocket(t *testing.T) {
+ t.Parallel()
+ file, err := os.Create(filepath.Join(t.TempDir(), "f"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer file.Close()
+ r, w, err := os.Pipe()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer r.Close()
+ defer w.Close()
+ dgram, _ := socketpairFile(t, unix.SOCK_DGRAM)
+ stream, _ := socketpairFile(t, unix.SOCK_STREAM)
+ for name, f := range map[string]*os.File{"a file": file, "a pipe": r, "a datagram socket": dgram} {
+ if _, err := StdinConn(f, me); err == nil {
+ t.Errorf("%s accepted", name)
+ }
+ }
+ if _, err := StdinConn(stream, other); err == nil {
+ t.Fatal("a peer that is not the agent's user accepted")
+ }
+ c, err := StdinConn(stream, me)
+ if err != nil {
+ t.Fatal(err)
+ }
+ c.Close()
+}
+```
+
+- [ ] **Step 2: Run it to verify it fails**
+
+Run (sandbox off): `go test ./internal/dbreads/ -run 'TestStdinConn' -count=1`
+Expected: FAIL, `StdinConn` undefined.
+
+- [ ] **Step 3: Write the Linux and other files**
+
+Create `internal/dbreads/worker_linux.go`:
+
+```go
+//go:build linux
+
+package dbreads
+
+import (
+ "errors"
+ "fmt"
+ "log"
+ "net"
+ "os"
+ "os/user"
+ "strconv"
+
+ "golang.org/x/sys/unix"
+)
+
+var errNotSocket = errors.New("stdin is not a connected unix stream socket")
+
+// RunWorker is `carlos system db-worker`, which carlos-dbread@.service runs
+// once per connection to carlos-dbread.socket (spec §8.3).
+func RunWorker(args []string) {
+ if len(args) != 0 {
+ fmt.Fprintln(os.Stderr, "db-worker: takes no arguments")
+ os.Exit(2)
+ }
+ RunWorkerWith(WorkerOptions{Scratch: "/var/tmp"})
+}
+
+// RunWorkerWith is RunWorker with its options supplied: the seam the root
+// runner's acceptance tests use to make a restore slow (Task 15).
+func RunWorkerWith(opt WorkerOptions) {
+ conn, err := StdinConn(os.Stdin, CarlosUID)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "db-worker: %v\n", err)
+ os.Exit(2)
+ }
+ if err := ServeWorker(conn, opt); err != nil {
+ log.Printf("db-worker: %v", err)
+ os.Exit(1)
+ }
+}
+
+// CarlosUID is the agent's user, the only peer the worker serves.
+func CarlosUID() (uint32, error) {
+ u, err := user.Lookup("carlos")
+ if err != nil {
+ return 0, err
+ }
+ uid, err := strconv.ParseUint(u.Uid, 10, 32)
+ return uint32(uid), err
+}
+
+// StdinConn checks f is a connected unix stream socket (SO_TYPE, the socket
+// name's family, SO_PEERCRED) whose peer is wantUID, and returns it as a
+// connection.
+func StdinConn(f *os.File, wantUID func() (uint32, error)) (net.Conn, error) {
+ fd := int(f.Fd())
+ typ, err := unix.GetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_TYPE)
+ if err != nil || typ != unix.SOCK_STREAM {
+ return nil, errNotSocket
+ }
+ if sa, err := unix.Getsockname(fd); err != nil {
+ return nil, errNotSocket
+ } else if _, ok := sa.(*unix.SockaddrUnix); !ok {
+ return nil, errNotSocket
+ }
+ cred, err := unix.GetsockoptUcred(fd, unix.SOL_SOCKET, unix.SO_PEERCRED)
+ if err != nil || cred.Pid == 0 {
+ return nil, errNotSocket
+ }
+ want, err := wantUID()
+ if err != nil {
+ return nil, err
+ }
+ if cred.Uid != want {
+ return nil, fmt.Errorf("the peer is uid %d, not the agent's uid %d", cred.Uid, want)
+ }
+ c, err := net.FileConn(f)
+ if err != nil {
+ return nil, errNotSocket
+ }
+ return c, nil
+}
+```
+
+Create `internal/dbreads/worker_other.go`:
+
+```go
+//go:build !linux
+
+package dbreads
+
+import (
+ "errors"
+ "fmt"
+ "net"
+ "os"
+)
+
+var errUnsupported = errors.New("db-worker runs only on Linux")
+
+func RunWorker([]string) {
+ fmt.Fprintln(os.Stderr, "db-worker:", errUnsupported)
+ os.Exit(2)
+}
+
+func RunWorkerWith(WorkerOptions) { RunWorker(nil) }
+
+func CarlosUID() (uint32, error) { return 0, errUnsupported }
+
+func StdinConn(*os.File, func() (uint32, error)) (net.Conn, error) { return nil, errUnsupported }
+```
+
+In `system.go`, add `"db-worker"` to the `case` list in `systemVerb` (line 52), and to `runSystem`'s switch:
+
+```go
+ case "db-worker":
+ // The db-read worker (db region forward spec §8.3): systemd starts
+ // it per connection to carlos-dbread.socket. Never run by hand.
+ dbreads.RunWorker(rest)
+```
+
+with the import `"github.com/carlosframework/platform/internal/dbreads"`. Do not add it to `systemUsage`: nobody runs it.
+
+- [ ] **Step 4: Run the tests and the cross-compiles**
+
+Run (sandbox off):
+```bash
+go test ./internal/dbreads/ -run 'TestStdinConn' -count=1
+go build ./... && GOOS=darwin GOARCH=arm64 go build ./... && GOOS=windows GOARCH=amd64 go build ./...
+```
+Expected: PASS and three clean builds.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add internal/dbreads/worker_linux.go internal/dbreads/worker_other.go internal/dbreads/worker_linux_test.go system.go
+git commit -m "carlos system db-worker: the stdin socket and peer checks, Linux only"
+```
+
+---
+
+### Task 9: Admission: reservations, read slots and the orphan rule
+
+**Files:**
+- Create: `internal/dbreads/admission.go`
+- Create: `internal/dbreads/admission_test.go`
+
+**Interfaces:**
+- Produces: `newAdmission(units func(context.Context) (int, error), free func() (avail, total uint64, err error)) *admission`; methods `admitRead(ctx, id) error`, `grow(id string, need int64) error`, `finish(id string)`, `admitProbe(ctx) bool`, `probeDone()`, `opened() bool`, `closed()`; errors `errBusy`, `errOrphans`, `errNoRoom`.
+
+- [ ] **Step 1: Write the failing test**
+
+Create `internal/dbreads/admission_test.go`:
+
+```go
+package dbreads
+
+import (
+ "context"
+ "errors"
+ "testing"
+)
+
+type unitsSeam struct {
+ n int
+ err error
+}
+
+func (u *unitsSeam) count(context.Context) (int, error) { return u.n, u.err }
+
+func gib(n uint64) uint64 { return n << 30 }
+
+func TestAdmissionTwoReadsThenBusy(t *testing.T) {
+ t.Parallel()
+ u := &unitsSeam{}
+ a := newAdmission(u.count, func() (uint64, uint64, error) { return gib(100), gib(200), nil })
+ ctx := context.Background()
+ for _, id := range []string{"r1", "r2"} {
+ if err := a.admitRead(ctx, id); err != nil {
+ t.Fatalf("%s: %v", id, err)
+ }
+ }
+ if err := a.admitRead(ctx, "r3"); !errors.Is(err, errBusy) {
+ t.Fatalf("a third read: %v", err)
+ }
+ // The probe still has its connection (spec §7.1, codex 9).
+ if !a.opened() || !a.opened() || !a.admitProbe(ctx) || !a.opened() {
+ t.Fatal("two reads and a probe do not fit three connections")
+ }
+ if a.opened() {
+ t.Fatal("a fourth connection was allowed")
+ }
+}
+
+// T12, unit half: free space that fits one read admits one.
+func TestAdmissionDiskFitsOneOfTwo(t *testing.T) {
+ t.Parallel()
+ a := newAdmission((&unitsSeam{}).count, func() (uint64, uint64, error) { return gib(12), gib(20), nil })
+ ctx := context.Background()
+ if err := a.admitRead(ctx, "r1"); err != nil {
+ t.Fatal(err)
+ }
+ if err := a.admitRead(ctx, "r2"); !errors.Is(err, errNoRoom) {
+ t.Fatalf("a second read on a box with room for one: %v", err)
+ }
+ if err := a.grow("r1", int64(gib(8))); !errors.Is(err, errNoRoom) {
+ t.Fatalf("growing past the floor and 10%%: %v", err)
+ }
+ if err := a.grow("r1", int64(gib(6))); err != nil {
+ t.Fatalf("growing within room: %v", err)
+ }
+}
+
+// An orphan (a unit this agent is not running) blocks every admission until
+// it is gone, and no size is guessed for it (spec §5.7, codex 8).
+func TestAdmissionRefusesBesideAnOrphan(t *testing.T) {
+ t.Parallel()
+ u := &unitsSeam{n: 1}
+ a := newAdmission(u.count, func() (uint64, uint64, error) { return gib(100), gib(200), nil })
+ ctx := context.Background()
+ if err := a.admitRead(ctx, "r1"); !errors.Is(err, errOrphans) {
+ t.Fatalf("beside an orphan: %v", err)
+ }
+ if a.admitProbe(ctx) {
+ t.Fatal("a probe beside an orphan")
+ }
+ u.n = 0
+ if err := a.admitRead(ctx, "r1"); err != nil {
+ t.Fatalf("once the orphan is gone: %v", err)
+ }
+}
+
+// A finished read's reservation is held until no db-read unit is left, since
+// systemd removes the scratch only at unit stop.
+func TestAdmissionHoldsAReservationUntilItsUnitIsGone(t *testing.T) {
+ t.Parallel()
+ u := &unitsSeam{}
+ // 12 GiB free of 20: r1 grown to 6 leaves room for r2's floor only once
+ // r1's reservation is released.
+ a := newAdmission(u.count, func() (uint64, uint64, error) { return gib(12), gib(20), nil })
+ ctx := context.Background()
+ if err := a.admitRead(ctx, "r1"); err != nil {
+ t.Fatal(err)
+ }
+ a.opened()
+ if err := a.grow("r1", int64(gib(6))); err != nil {
+ t.Fatal(err)
+ }
+ u.n = 1
+ a.closed()
+ a.finish("r1")
+ // The unit is still deactivating: one unit, no connection. Refused.
+ if err := a.admitRead(ctx, "r2"); !errors.Is(err, errOrphans) {
+ t.Fatalf("while the unit deactivates: %v", err)
+ }
+ u.n = 0
+ if err := a.admitRead(ctx, "r2"); err != nil {
+ t.Fatalf("after the unit is gone the reservation is free: %v", err)
+ }
+}
+
+// Review Focus 5: a unit listing that fails admits nothing and skips the
+// probe rather than guessing.
+func TestAdmissionFailsClosedWhenUnitsCannotBeListed(t *testing.T) {
+ t.Parallel()
+ u := &unitsSeam{err: errors.New("systemctl: not found")}
+ a := newAdmission(u.count, func() (uint64, uint64, error) { return gib(100), gib(200), nil })
+ if err := a.admitRead(context.Background(), "r1"); err == nil {
+ t.Fatal("admitted with no unit listing")
+ }
+ if a.admitProbe(context.Background()) {
+ t.Fatal("probed with no unit listing")
+ }
+}
+
+func TestAdmissionFailsClosedWhenTheDiskCannotBeRead(t *testing.T) {
+ t.Parallel()
+ a := newAdmission((&unitsSeam{}).count, func() (uint64, uint64, error) { return 0, 0, errors.New("statfs") })
+ if err := a.admitRead(context.Background(), "r1"); !errors.Is(err, errNoRoom) {
+ t.Fatalf("admitted with no disk reading: %v", err)
+ }
+}
+```
+
+- [ ] **Step 2: Run it to verify it fails**
+
+Run: `go test ./internal/dbreads/ -run 'TestAdmission' -count=1`
+Expected: FAIL, `newAdmission` undefined.
+
+- [ ] **Step 3: Write `admission.go`**
+
+```go
+package dbreads
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "sync"
+)
+
+var (
+ errBusy = errors.New("dbreads: this box is running its two reads")
+ errOrphans = errors.New("dbreads: a carlos-dbread@ unit this agent is not running is still active")
+ errNoRoom = errors.New("dbreads: not enough disk for the read")
+)
+
+// admission is the agent's one table (spec §5.7, J5): how many worker
+// connections it holds, which reads it is running, and every disk
+// reservation. The agent is the only process that starts workers, so one
+// mutex covers it all.
+type admission struct {
+ units func(context.Context) (int, error)
+ free func() (avail, total uint64, err error)
+
+ mu sync.Mutex
+ conns int
+ running map[string]bool
+ held map[string]int64
+ probing bool
+}
+
+func newAdmission(units func(context.Context) (int, error), free func() (uint64, uint64, error)) *admission {
+ return &admission{units: units, free: free, running: map[string]bool{}, held: map[string]int64{}}
+}
+
+// reconcileLocked is the refusal-based reconciliation (spec §5.7): while any
+// carlos-dbread@ unit is active, activating or deactivating beyond the
+// connections this agent holds, nothing is admitted. A finished read's
+// reservation is released only when no unit is left at all: then every
+// stopped unit's scratch has been removed by systemd.
+func (a *admission) reconcileLocked(ctx context.Context) error {
+ n, err := a.units(ctx)
+ if err != nil {
+ return fmt.Errorf("dbreads: list carlos-dbread@ units: %w", err)
+ }
+ if n > a.conns {
+ return errOrphans
+ }
+ if n == 0 {
+ for id := range a.held {
+ if !a.running[id] {
+ delete(a.held, id)
+ }
+ }
+ }
+ return nil
+}
+
+// fitsLocked: after subtracting every other reservation and need, the
+// filesystem holding /var/tmp keeps DiskFloor and DiskFreePercent free.
+func (a *admission) fitsLocked(id string, need int64) bool {
+ avail, total, err := a.free()
+ if err != nil {
+ return false
+ }
+ left := int64(avail) - need
+ for k, v := range a.held {
+ if k != id {
+ left -= v
+ }
+ }
+ return left >= DiskFloor && left >= int64(total)*DiskFreePercent/100
+}
+
+// admitRead admits a read before it enters the election: a free read slot,
+// no orphan, and room for the floor reservation.
+func (a *admission) admitRead(ctx context.Context, id string) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+ if err := a.reconcileLocked(ctx); err != nil {
+ return err
+ }
+ if len(a.running) >= ReadsPerBox {
+ return errBusy
+ }
+ if !a.fitsLocked(id, DiskFloor) {
+ return errNoRoom
+ }
+ a.running[id] = true
+ a.held[id] = DiskFloor
+ return nil
+}
+
+// grow raises a running read's reservation to need (never below the floor)
+// once its plan is known.
+func (a *admission) grow(id string, need int64) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+ need = max(need, DiskFloor)
+ if !a.fitsLocked(id, need) {
+ return errNoRoom
+ }
+ a.held[id] = need
+ return nil
+}
+
+// finish ends a read's slot. Its reservation stays until reconcileLocked
+// sees no unit left.
+func (a *admission) finish(id string) {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+ delete(a.running, id)
+}
+
+// admitProbe admits the readiness probe: no orphan and no other probe. The
+// probe's connection is the third (opened checks the count).
+func (a *admission) admitProbe(ctx context.Context) bool {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+ if a.probing || a.reconcileLocked(ctx) != nil {
+ return false
+ }
+ a.probing = true
+ return true
+}
+
+func (a *admission) probeDone() {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+ a.probing = false
+}
+
+// opened takes a connection slot; the agent never opens a fourth.
+func (a *admission) opened() bool {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+ if a.conns >= MaxConnections {
+ return false
+ }
+ a.conns++
+ return true
+}
+
+func (a *admission) closed() {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+ a.conns--
+}
+```
+
+- [ ] **Step 4: Run the tests**
+
+Run: `go test ./internal/dbreads/ -run 'TestAdmission' -count=1 -race`
+Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add internal/dbreads/admission.go internal/dbreads/admission_test.go
+git commit -m "dbreads: admission, held until the worker unit is gone"
+```
+
+---
+
+### Task 10: The agent's identity check and the election
+
+**Files:**
+- Create: `internal/dbreads/class.go`
+- Create: `internal/dbreads/agent.go`
+- Create: `internal/dbreads/fixture_test.go`
+- Create: `internal/dbreads/agent_test.go`
+- Modify: `internal/console/apidb.go:236-250` (`dbClassRefusal` delegates)
+
+**Interfaces:**
+- Consumes: `provision.GetRecord`, `provision.ValidHostShape`, `hostclaim.Incarnation`, `registry.FleetPlacement`, `admission` (Task 9), `Verify`, `QueueMatches` (Task 4), `ServeWorker` (Task 7).
+- Produces: `ClassRefusal(rec provision.Record) string`; `type AgentConfig struct` (fields below); `NewAgent(cfg AgentConfig) *Agent`; `(*Agent).Capable() bool`; `(*Agent).Wait()`; unexported `check(ctx, req) (provision.Record, string, error)`, `elect(ctx, id) (Picked, bool)`, `consider(ctx, e QueueEntry)`, `dial(ctx) (net.Conn, error)`, `decide(id)`, `isDecided(id) bool`, `prune(now)`, `askProbe()`; `serve` is Task 11's and is called from `consider`.
+
+- [ ] **Step 1: Write the test fixture**
+
+Create `internal/dbreads/fixture_test.go`. It is shared by Tasks 10 to 12. Its in-process worker runs the real `ServeWorker` over `net.Pipe`: not acceptance.
+
+```go
+package dbreads
+
+import (
+ "bytes"
+ "context"
+ "crypto/ecdh"
+ "crypto/ed25519"
+ "crypto/rand"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "net"
+ "os"
+ "sort"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ lsfile "github.com/benbjohnson/litestream/file"
+
+ "github.com/carlosframework/platform/internal/dbdoor"
+ "github.com/carlosframework/platform/internal/dbdoor/dbdoortest"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+const fxAccount, fxApp, fxHost = "bab", "shop", "shop.example.com"
+
+// inProcWorkers runs the real ServeWorker in process over net.Pipe: not
+// acceptance (Task 15 drives the installed units). units counts the live
+// workers, the way the box counts carlos-dbread@ units.
+type inProcWorkers struct {
+ t *testing.T
+ opt WorkerOptions
+ live atomic.Int64
+ dials atomic.Int64
+}
+
+func (w *inProcWorkers) dial(context.Context) (net.Conn, error) {
+ agentEnd, workerEnd := net.Pipe()
+ w.dials.Add(1)
+ w.live.Add(1)
+ opt := w.opt
+ if opt.Scratch == "" {
+ opt.Scratch = w.t.TempDir()
+ }
+ if opt.Exit == nil {
+ opt.Exit = func(int) {}
+ }
+ if opt.Self == nil {
+ opt.Self = func() HelloReply { return HelloReply{V: ProtocolVersion, Interfaces: []string{"lo"}} }
+ }
+ go func() {
+ defer w.live.Add(-1)
+ ServeWorker(workerEnd, opt)
+ workerEnd.Close()
+ }()
+ return agentEnd, nil
+}
+
+func (w *inProcWorkers) units(context.Context) (int, error) { return int(w.live.Load()), nil }
+
+func plentyFree() (uint64, uint64, error) { return 100 << 30, 200 << 30, nil }
+
+type fixture struct {
+ t *testing.T
+ store release.Store
+ rootPub ed25519.PublicKey
+ rootPriv ed25519.PrivateKey
+ reply *ecdh.PrivateKey
+ replica string
+ now time.Time
+}
+
+// newFixture is a deployment store holding shop.example.com's record in uk,
+// and a file replica of its database with batches*10 rows.
+func newFixture(t *testing.T, batches int) *fixture {
+ t.Helper()
+ pub, priv, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+ reply, err := NewReplyKey()
+ if err != nil {
+ t.Fatal(err)
+ }
+ fx := &fixture{t: t, store: release.NewDirStore(t.TempDir()), rootPub: pub, rootPriv: priv, reply: reply,
+ replica: t.TempDir(), now: time.Now().UTC().Truncate(time.Second)}
+ if batches > 0 {
+ dbdoortest.Replicate(t, fx.replica, batches)
+ }
+ fx.putRecord(provision.Record{Host: fxHost, Region: "uk", CreatedAt: time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC)})
+ return fx
+}
+
+func (fx *fixture) putRecordUnder(acct string, rec provision.Record) {
+ fx.t.Helper()
+ rec.Account, rec.App = acct, fxApp
+ if rec.Host == "" {
+ rec.Host = fxHost
+ }
+ if rec.Hibernate == nil {
+ yes := true
+ rec.Hibernate = &yes
+ }
+ body, err := json.Marshal(rec)
+ if err != nil {
+ fx.t.Fatal(err)
+ }
+ key := provision.RecordKey(acct, fxApp, rec.Host)
+ if err := fx.store.Delete(context.Background(), key); err != nil {
+ fx.t.Fatal(err)
+ }
+ if _, err := fx.store.Put(context.Background(), key, body, ""); err != nil {
+ fx.t.Fatal(err)
+ }
+}
+
+func (fx *fixture) putRecord(rec provision.Record) { fx.putRecordUnder(fxAccount, rec) }
+
+func (fx *fixture) record() provision.Record {
+ fx.t.Helper()
+ rec, err := provision.GetRecord(context.Background(), fx.store, fxAccount, fxApp, fxHost)
+ if err != nil {
+ fx.t.Fatal(err)
+ }
+ return *rec
+}
+
+// request is the envelope the console would sign for the current record.
+func (fx *fixture) request(sqlText, at string) Request {
+ fx.t.Helper()
+ rec := fx.record()
+ inc, err := hostclaim.Incarnation(rec.Account, rec.App, rec.Host, rec.CreatedAt)
+ if err != nil {
+ fx.t.Fatal(err)
+ }
+ id, err := MintID()
+ if err != nil {
+ fx.t.Fatal(err)
+ }
+ return Request{ID: id, Account: fxAccount, App: fxApp, Host: fxHost, Incarnation: inc, Region: "uk",
+ Op: OpQuery, SQL: sqlText, At: at, ReplyKey: EncodeReplyKey(fx.reply.PublicKey()),
+ By: "member@example.com", IssuedAt: fx.now.Format(time.RFC3339)}
+}
+
+// submit is the console's two creates: the signed request, then the queue
+// entry.
+func (fx *fixture) submit(req Request) QueueEntry {
+ fx.t.Helper()
+ wire, err := Sign(release.RootSigner(fx.rootPriv), req)
+ if err != nil {
+ fx.t.Fatal(err)
+ }
+ if _, err := fx.store.Put(context.Background(), RequestKey(req.ID), wire, ""); err != nil {
+ fx.t.Fatal(err)
+ }
+ issued, _ := req.Issued()
+ key := QueueKey(req.Region, issued, req.ID)
+ if _, err := fx.store.Put(context.Background(), key, nil, ""); err != nil {
+ fx.t.Fatal(err)
+ }
+ e, _ := ParseQueueKey(key)
+ return e
+}
+
+// agent is a capable agent in uk over the fixture's store and replica.
+// Its probe is Task 12's: it starts capable.
+func (fx *fixture) agent(box string, mut ...func(*AgentConfig, *inProcWorkers)) (*Agent, *inProcWorkers) {
+ w := &inProcWorkers{t: fx.t}
+ cfg := AgentConfig{
+ Store: fx.store, Region: "uk", Box: box, Boot: "boot-" + box, Pub: fx.rootPub, HibernatePrefix: "instances",
+ OpenReplica: func(_ context.Context, prefix string) (dbdoor.ReplicaReader, error) {
+ if prefix != "instances/"+fxHost {
+ fx.t.Errorf("the agent asked for prefix %q", prefix)
+ }
+ return lsfile.NewReplicaClient(fx.replica), nil
+ },
+ Units: w.units, Free: plentyFree, Logf: fx.t.Logf,
+ Jitter: func() time.Duration { return 0 },
+ }
+ for _, m := range mut {
+ m(&cfg, w)
+ }
+ cfg.DialWorker = w.dial
+ a := NewAgent(cfg)
+ a.probeOK.Store(true)
+ return a, w
+}
+
+// chunkKeys lists a read's chunk objects.
+func (fx *fixture) chunkKeys(id string) []string {
+ keys, err := fx.store.List(context.Background(), OutPrefix(id))
+ if err != nil && !errors.Is(err, os.ErrNotExist) {
+ fx.t.Fatal(err)
+ }
+ sort.Strings(keys)
+ return keys
+}
+
+// answer reads a read's result and opens its chunks as the CLI would.
+func (fx *fixture) answer(id string) ([]byte, Summary, Result) {
+ fx.t.Helper()
+ raw, _, err := fx.store.Get(context.Background(), ResultKey(id))
+ if err != nil {
+ fx.t.Fatalf("no result for %s: %v", id, err)
+ }
+ var r Result
+ if err := json.Unmarshal(raw, &r); err != nil || r.Valid() != nil {
+ fx.t.Fatalf("result %s: %v", raw, err)
+ }
+ s, err := OpenSummary(fx.reply, id, r.Sealed)
+ if err != nil {
+ fx.t.Fatal(err)
+ }
+ var out bytes.Buffer
+ for seq := int64(1); seq <= r.LastSeq; seq++ {
+ sealed, _, err := fx.store.Get(context.Background(), ChunkKey(id, seq))
+ if err != nil {
+ fx.t.Fatalf("chunk %d: %v", seq, err)
+ }
+ plain, err := OpenChunk(fx.reply, id, seq, StreamRows, seq == r.LastSeq, sealed)
+ if err != nil {
+ fx.t.Fatalf("chunk %d: %v", seq, err)
+ }
+ out.Write(plain)
+ }
+ sum := sha256.Sum256(out.Bytes())
+ if r.State == StateDone && (int64(out.Len()) != s.Bytes || hex.EncodeToString(sum[:]) != s.SHA256) {
+ fx.t.Fatalf("the answer does not add up: %d bytes, summary %+v", out.Len(), s)
+ }
+ return out.Bytes(), s, r
+}
+```
+
+- [ ] **Step 2: Write the failing tests**
+
+Create `internal/dbreads/agent_test.go`:
+
+```go
+package dbreads
+
+import (
+ "context"
+ "errors"
+ "math/rand/v2"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// The identity check (spec §8.2): the record as the envelope named it, or a
+// code. T9's stages are Task 11's; this is the check itself.
+func TestCheckIsTheRecordAsTheEnvelopeNamedIt(t *testing.T) {
+ t.Parallel()
+ no := false
+ for name, c := range map[string]struct {
+ change func(fx *fixture)
+ want string
+ }{
+ "unchanged": {func(*fixture) {}, ""},
+ "moved to another region": {func(fx *fixture) { r := fx.record(); r.Region = "ie"; fx.putRecord(r) }, CodeRecordChanged},
+ "moving": {func(fx *fixture) { r := fx.record(); r.Move = &provision.MoveState{ID: "mv_x", To: "ie", State: "requested"}; fx.putRecord(r) }, CodeMoving},
+ "recreated in the account": {func(fx *fixture) { r := fx.record(); r.CreatedAt = r.CreatedAt.Add(time.Hour); fx.putRecord(r) }, CodeRecordChanged},
+ "recreated in another account": {func(fx *fixture) {
+ created := fx.record().CreatedAt
+ fx.store.Delete(context.Background(), provision.RecordKey(fxAccount, fxApp, fxHost))
+ fx.putRecordUnder("bac", provision.Record{Region: "uk", CreatedAt: created})
+ }, CodeRecordChanged},
+ "no longer sleeps": {func(fx *fixture) { r := fx.record(); r.Hibernate = &no; fx.putRecord(r) }, CodeRecordChanged},
+ "pooled": {func(fx *fixture) { r := fx.record(); r.Pool = "shared"; fx.putRecord(r) }, CodeRecordChanged},
+ "placed on a fleet": {func(fx *fixture) { r := fx.record(); r.Region = "fleet/acme"; fx.putRecord(r) }, CodeRecordChanged},
+ } {
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+ fx := newFixture(t, 0)
+ req := fx.request("SELECT 1", "")
+ c.change(fx)
+ a, _ := fx.agent("uk-1")
+ _, code, err := a.check(context.Background(), req)
+ if err != nil || code != c.want {
+ t.Fatalf("check = %q, %v; want %q", code, err, c.want)
+ }
+ })
+ }
+}
+
+// T5: two capable boxes in one region; exactly one owns each read, across
+// 1,000 reads with random jitter.
+func TestElectionOneOwnerAcrossTwoAgents(t *testing.T) {
+ t.Parallel()
+ fx := newFixture(t, 0)
+ a, _ := fx.agent("uk-1")
+ b, _ := fx.agent("uk-2")
+ var mu sync.Mutex
+ owners := map[string]int{}
+ var wg sync.WaitGroup
+ for i := 0; i < 1000; i++ {
+ id, err := MintID()
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, ag := range []*Agent{a, b} {
+ wg.Add(1)
+ go func(ag *Agent) {
+ defer wg.Done()
+ time.Sleep(rand.N(2 * time.Millisecond))
+ if _, ok := ag.elect(context.Background(), id); ok {
+ mu.Lock()
+ owners[id]++
+ mu.Unlock()
+ }
+ }(ag)
+ }
+ }
+ wg.Wait()
+ if len(owners) != 1000 {
+ t.Fatalf("%d reads have an owner, want 1000", len(owners))
+ }
+ for id, n := range owners {
+ if n != 1 {
+ t.Fatalf("%s has %d owners", id, n)
+ }
+ }
+}
+
+// lossyStore commits some Puts and then answers an error: a response lost
+// on the way back (T5, codex 16).
+type lossyStore struct {
+ release.Store
+ lose func(key string) bool
+ refuse func(key string) bool
+}
+
+func (s lossyStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
+ if s.refuse != nil && s.refuse(key) {
+ return "", errors.New("connection reset before the write")
+ }
+ etag, err := s.Store.Put(ctx, key, body, ifMatch)
+ if err == nil && s.lose != nil && s.lose(key) {
+ return "", errors.New("connection reset after the write")
+ }
+ return etag, err
+}
+
+// A box whose picked create commits but answers an error reads it back and
+// owns the read.
+func TestElectionReadsBackALostAnswer(t *testing.T) {
+ t.Parallel()
+ fx := newFixture(t, 0)
+ a, _ := fx.agent("uk-1", func(c *AgentConfig, _ *inProcWorkers) {
+ c.Store = lossyStore{Store: fx.store, lose: func(string) bool { return true }}
+ })
+ if _, ok := a.elect(context.Background(), "dbr_aaaaaaaaaaaaaaaaaaaaaaaaaa"); !ok {
+ t.Fatal("a lost answer to a create that landed was not owned")
+ }
+}
+
+// A box whose create answers an error while another box owns the read steps
+// away; so does one whose read-back fails.
+func TestElectionStepsAwayFromAnotherOwner(t *testing.T) {
+ t.Parallel()
+ fx := newFixture(t, 0)
+ a, _ := fx.agent("uk-1")
+ id := "dbr_aaaaaaaaaaaaaaaaaaaaaaaaaa"
+ if _, ok := a.elect(context.Background(), id); !ok {
+ t.Fatal("the first create did not own")
+ }
+ b, _ := fx.agent("uk-2", func(c *AgentConfig, _ *inProcWorkers) {
+ c.Store = lossyStore{Store: fx.store, refuse: func(string) bool { return true }}
+ })
+ if _, ok := b.elect(context.Background(), id); ok {
+ t.Fatal("a box owned a read another box had picked")
+ }
+ c, _ := fx.agent("uk-3", func(cfg *AgentConfig, _ *inProcWorkers) {
+ cfg.Store = failingGets{lossyStore{Store: fx.store, refuse: func(string) bool { return true }}}
+ })
+ if _, ok := c.elect(context.Background(), "dbr_bbbbbbbbbbbbbbbbbbbbbbbbbb"); ok {
+ t.Fatal("a box owned a read it could not read back")
+ }
+}
+
+type failingGets struct{ release.Store }
+
+func (failingGets) Get(context.Context, string) ([]byte, string, error) {
+ return nil, "", errors.New("timeout")
+}
+```
+
+- [ ] **Step 3: Run them to verify they fail**
+
+Run: `go test ./internal/dbreads/ -run 'TestCheck|TestElection' -count=1`
+Expected: FAIL, undefined `NewAgent`, `AgentConfig`.
+
+- [ ] **Step 4: Move the class rail**
+
+Create `internal/dbreads/class.go` with `console.dbClassRefusal`'s body verbatim, renamed:
+
+```go
+package dbreads
+
+import (
+ "fmt"
+
+ "github.com/carlosframework/platform/internal/provision"
+)
+
+// ClassRefusal is the class rail as a sentence, or "" for an
+// activator-managed exec instance: the only class whose database the
+// platform replicates. It moved here from the console's db door so the door
+// and the box's identity bracket refuse on one rule (spec §8.2); the
+// sentences are the door's, unchanged.
+func ClassRefusal(rec provision.Record) string {
+ if k := rec.Kind; k != "" && k != "instance" {
+ return fmt.Sprintf("%s is a %s route. The platform keeps a database copy only for instances.", rec.Host, k)
+ }
+ if rec.Addr != "" || len(rec.Addrs) > 0 {
+ return fmt.Sprintf("%s is served from its own address, so the platform does not manage its database.", rec.Host)
+ }
+ if !rec.HibernateOrDefault() {
+ return fmt.Sprintf("%s does not sleep, so the platform does not keep a copy of its database.", rec.Host)
+ }
+ return ""
+}
+```
+
+In `internal/console/apidb.go`, make `dbClassRefusal` delegate, keeping its doc comment and adding "The rule lives in dbreads.ClassRefusal (db region forward spec §8.2).":
+
+```go
+func dbClassRefusal(rec provision.Record) string { return dbreads.ClassRefusal(rec) }
+```
+
+- [ ] **Step 5: Write `agent.go`**
+
+```go
+package dbreads
+
+import (
+ "context"
+ "crypto/ed25519"
+ "crypto/rand"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "net"
+ "os"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ mrand "math/rand/v2"
+
+ "github.com/carlosframework/platform/internal/dbdoor"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// AgentConfig is what the box's db-reads loop needs (spec §7, §8).
+// internal/host fills it from the box's environment; tests fill it with
+// stand-ins.
+type AgentConfig struct {
+ Store release.Store // the deployment bucket
+ Region string // CARLOS_REGION
+ Box string // this box's label, as its edges file names it
+ Boot string // this agent process's id
+ Pub ed25519.PublicKey // CARLOS_RELEASE_PUBKEY
+ HibernatePrefix string // CARLOS_HIBERNATE_PREFIX
+ // OpenReplica opens the read side of this box's own region bucket at a
+ // prefix the agent derived (lease.ReadReplicaClient in production).
+ OpenReplica func(ctx context.Context, prefix string) (dbdoor.ReplicaReader, error)
+ // DialWorker connects to carlos-dbread.socket.
+ DialWorker func(ctx context.Context) (net.Conn, error)
+ // Units counts carlos-dbread@ units active, activating or deactivating.
+ Units func(ctx context.Context) (int, error)
+ // Free reads the filesystem holding /var/tmp.
+ Free func() (avail, total uint64, err error)
+ MaxRestored int64 // DefaultMaxRestored when zero
+ Now func() time.Time
+ Jitter func() time.Duration // 0 to MaxJitter
+ Logf func(format string, args ...any)
+
+ // Test seams; zero is the spec's value.
+ OwnerDeadline time.Duration
+ PlanBudget time.Duration
+ CancelEvery time.Duration
+ ChunkEvery time.Duration
+ MaxOutput int64
+}
+
+// Agent is the box's half of forwarded reads (spec §8.1): it lists the
+// region's queue, verifies requests, elects an owner, runs the identity
+// bracket around planning and download, streams bytes to a worker, and
+// seals and writes output. It never opens SQLite, decodes pages, runs SQL or
+// writes a file.
+type Agent struct {
+ cfg AgentConfig
+ adm *admission
+
+ mu sync.Mutex
+ decided map[string]time.Time
+
+ probeOK atomic.Bool
+ probeAfter chan struct{}
+ reads sync.WaitGroup
+}
+
+func NewAgent(cfg AgentConfig) *Agent {
+ if cfg.MaxRestored <= 0 {
+ cfg.MaxRestored = DefaultMaxRestored
+ }
+ if cfg.Now == nil {
+ cfg.Now = time.Now
+ }
+ if cfg.Jitter == nil {
+ cfg.Jitter = func() time.Duration { return mrand.N(MaxJitter) }
+ }
+ if cfg.Logf == nil {
+ cfg.Logf = func(string, ...any) {}
+ }
+ if cfg.OwnerDeadline == 0 {
+ cfg.OwnerDeadline = OwnerDeadline
+ }
+ if cfg.PlanBudget == 0 {
+ cfg.PlanBudget = PlanBudget
+ }
+ if cfg.CancelEvery == 0 {
+ cfg.CancelEvery = CancelEvery
+ }
+ if cfg.ChunkEvery == 0 {
+ cfg.ChunkEvery = ChunkEvery
+ }
+ if cfg.MaxOutput == 0 {
+ cfg.MaxOutput = MaxOutputBytes
+ }
+ return &Agent{cfg: cfg, adm: newAdmission(cfg.Units, cfg.Free), decided: map[string]time.Time{},
+ probeAfter: make(chan struct{}, 1)}
+}
+
+// Capable is whether this box may publish db-read-1: its last readiness
+// probe passed (spec §7.1).
+func (a *Agent) Capable() bool { return a.probeOK.Load() }
+
+// Wait blocks until every read this agent owns has written its result.
+func (a *Agent) Wait() { a.reads.Wait() }
+
+func (a *Agent) now() time.Time { return a.cfg.Now().UTC() }
+
+func (a *Agent) logf(format string, args ...any) { a.cfg.Logf(format, args...) }
+
+func (a *Agent) decide(id string) {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+ a.decided[id] = a.now()
+}
+
+func (a *Agent) isDecided(id string) bool {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+ _, ok := a.decided[id]
+ return ok
+}
+
+// prune forgets decisions older than any listing can show again.
+func (a *Agent) prune(now time.Time) {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+ for id, at := range a.decided {
+ if now.Sub(at) > 2*time.Hour {
+ delete(a.decided, id)
+ }
+ }
+}
+
+// askProbe asks the loop for a probe now: a read's worker failed to start
+// (spec §7.1).
+func (a *Agent) askProbe() {
+ select {
+ case a.probeAfter <- struct{}{}:
+ default:
+ }
+}
+
+// countedConn gives a worker connection's slot back exactly once.
+type countedConn struct {
+ net.Conn
+ once sync.Once
+ adm *admission
+}
+
+func (c *countedConn) Close() error {
+ err := c.Conn.Close()
+ c.once.Do(c.adm.closed)
+ return err
+}
+
+// dial opens a worker connection inside the three the socket allows.
+func (a *Agent) dial(ctx context.Context) (net.Conn, error) {
+ if !a.adm.opened() {
+ return nil, errBusy
+ }
+ c, err := a.cfg.DialWorker(ctx)
+ if err != nil {
+ a.adm.closed()
+ return nil, err
+ }
+ return &countedConn{Conn: c, adm: a.adm}, nil
+}
+
+// check is one half of the identity bracket (spec §8.2): the record read
+// fresh, and refused unless it is the instance the envelope named, still a
+// replicated instance, still in this box's region and not moving. A class,
+// pool or fleet change after the console admitted the read is a record
+// change.
+func (a *Agent) check(ctx context.Context, req Request) (provision.Record, string, error) {
+ rec, err := provision.GetRecord(ctx, a.cfg.Store, req.Account, req.App, req.Host)
+ if errors.Is(err, os.ErrNotExist) {
+ return provision.Record{}, CodeRecordChanged, nil
+ }
+ if err != nil {
+ return provision.Record{}, "", err
+ }
+ inc, ierr := hostclaim.Incarnation(rec.Account, rec.App, rec.Host, rec.CreatedAt)
+ _, fleet := registry.FleetPlacement(rec.Region)
+ switch {
+ case ierr != nil, inc != req.Incarnation, rec.Account != req.Account, rec.App != req.App,
+ rec.Host != req.Host, !provision.ValidHostShape(rec.Host), ClassRefusal(*rec) != "",
+ rec.Pool != "", fleet, rec.Region != a.cfg.Region:
+ return *rec, CodeRecordChanged, nil
+ case rec.Move != nil:
+ return *rec, CodeMoving, nil
+ }
+ return *rec, "", nil
+}
+
+func nonce() string {
+ var b [16]byte
+ rand.Read(b[:])
+ return hex.EncodeToString(b[:])
+}
+
+// elect is J3: the first create of picked owns the read. A create that
+// answers an error or a conflict is decided by reading picked back: this
+// agent's own nonce owns it, anything else, or a read that fails, does not.
+func (a *Agent) elect(ctx context.Context, id string) (Picked, bool) {
+ p := Picked{Box: a.cfg.Box, Boot: a.cfg.Boot, Nonce: nonce(), Deadline: a.now().Add(a.cfg.OwnerDeadline)}
+ body, err := json.Marshal(p)
+ if err != nil {
+ return Picked{}, false
+ }
+ if _, err := a.cfg.Store.Put(ctx, PickedKey(id), body, ""); err == nil {
+ return p, true
+ }
+ raw, _, err := a.cfg.Store.Get(ctx, PickedKey(id))
+ if err != nil {
+ return Picked{}, false
+ }
+ var got Picked
+ if json.Unmarshal(raw, &got) != nil || got.Nonce != p.Nonce {
+ return Picked{}, false
+ }
+ return got, true
+}
+
+// consider decides one in-window queue entry (spec §5.5 step 4): read and
+// verify the request against its entry, check admission, wait the jitter,
+// and enter the election. A refused or forged request is decided, and so
+// logged once. A read that fails admission is not decided: a later pass may
+// admit it while the window lasts.
+func (a *Agent) consider(ctx context.Context, e QueueEntry) {
+ raw, _, err := a.cfg.Store.Get(ctx, RequestKey(e.ID))
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ a.decide(e.ID)
+ a.logf("dbreads: %s: a queue entry with no request; ignored", e.ID)
+ }
+ return
+ }
+ req, err := Verify(a.cfg.Pub, raw, e.ID, a.cfg.Region, a.now())
+ if err == nil && !QueueMatches(e, req) {
+ err = ErrEnvelope
+ }
+ if err != nil {
+ a.decide(e.ID)
+ a.logf("dbreads: %s: refused the request: %v", e.ID, err)
+ return
+ }
+ if err := a.adm.admitRead(ctx, e.ID); err != nil {
+ return
+ }
+ if d := a.cfg.Jitter(); d > 0 {
+ select {
+ case <-ctx.Done():
+ a.adm.finish(e.ID)
+ return
+ case <-time.After(d):
+ }
+ }
+ p, owned := a.elect(ctx, e.ID)
+ a.decide(e.ID)
+ if !owned {
+ a.adm.finish(e.ID)
+ return
+ }
+ a.reads.Add(1)
+ go a.serve(ctx, req, p)
+}
+```
+
+`serve` is Task 11's. Until Task 11 lands, add a temporary stub at the end of `agent.go` so this task builds, and delete it in Task 11:
+
+```go
+// serve is Task 11's.
+func (a *Agent) serve(_ context.Context, req Request, _ Picked) {
+ defer a.reads.Done()
+ defer a.adm.finish(req.ID)
+}
+```
+
+- [ ] **Step 6: Run the tests**
+
+Run: `go test ./internal/dbreads/ -run 'TestCheck|TestElection' -count=1 -race && go test ./internal/console/ -run 'TestResolveReplica|TestDBClass|TestAPIDB' -count=1`
+Expected: PASS.
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add internal/dbreads/class.go internal/dbreads/agent.go internal/dbreads/fixture_test.go internal/dbreads/agent_test.go internal/console/apidb.go
+git commit -m "dbreads: the identity check, the create-only election with read-back, ClassRefusal moved"
+```
+
+---
+
+### Task 11: One owned read: the bracket, the worker, the sealed output, the result
+
+**Files:**
+- Create: `internal/dbreads/read.go`
+- Create: `internal/dbreads/chunks.go`
+- Create: `internal/dbreads/read_test.go`
+- Modify: `internal/dbreads/agent.go` (delete Task 10's `serve` stub)
+
+**Interfaces:**
+- Consumes: `check`, `elect`, `dial`, `askProbe`, `adm` (Task 10), `dbdoor.PlanFrom` and its errors (Task 6), the frames (Task 7), `SealChunk`, `SealSummary` (Task 5), `activator.HibernatePrefix`.
+- Produces: `(*Agent).serve(ctx, req Request, p Picked)`; `read(ctx, req) Result`; `attempt(ctx, req, pub, at) (Result, bool)`; `sendFile(ctx, w io.Writer, rd dbdoor.ReplicaReader, f dbdoor.LTXFile) (vanished bool, err error)`; `relay(conn net.Conn, cw *chunkWriter) (code, sqlErr string, err error)`; `newChunkWriter(ctx, store, id string, pub *ecdh.PublicKey, stream string, every time.Duration, max int64) *chunkWriter`; `(*chunkWriter).Write`, `.close() (lastSeq, bytes int64, sha string, err error)`, `.capped() bool`; `putCreateOnly(ctx, s release.Store, key string, body []byte) error`; `errCapped`.
+
+- [ ] **Step 1: Write the failing tests**
+
+Create `internal/dbreads/read_test.go`. The in-process worker makes these unit tests, not acceptance.
+
+```go
+package dbreads
+
+import (
+ "bytes"
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "io"
+ "os"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/benbjohnson/litestream"
+ lsfile "github.com/benbjohnson/litestream/file"
+ "github.com/superfly/ltx"
+
+ "github.com/carlosframework/platform/internal/dbdoor"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func serveOne(t *testing.T, a *Agent, fx *fixture, req Request) ([]byte, Summary, Result) {
+ t.Helper()
+ e := fx.submit(req)
+ a.consider(context.Background(), e)
+ a.Wait()
+ return fx.answer(req.ID)
+}
+
+func TestReadServesAQuery(t *testing.T) {
+ t.Parallel()
+ fx := newFixture(t, 3)
+ a, w := fx.agent("uk-1")
+ rows, s, r := serveOne(t, a, fx, fx.request("SELECT count(*) AS n FROM t", ""))
+ if r.State != StateDone || r.Code != "" || string(rows) != "n\n30\n" || r.Box != "uk-1" {
+ t.Fatalf("result %+v, rows %q", r, rows)
+ }
+ plan, err := dbdoor.PlanFrom(context.Background(), lsfile.NewReplicaClient(fx.replica), time.Time{})
+ if err != nil || s.TXID != plan.TXID || !s.At.Equal(plan.At) {
+ t.Fatalf("summary %+v, plan %+v %v", s, plan, err)
+ }
+ if w.dials.Load() != 1 {
+ t.Fatalf("%d worker connections for one read", w.dials.Load())
+ }
+}
+
+// vanishing makes full-file opens of the first planned file answer
+// os.ErrNotExist, `times` times: litestream's compaction removing an L0
+// file under the plan.
+type vanishing struct {
+ dbdoor.ReplicaReader
+ mu sync.Mutex
+ times int
+}
+
+func (v *vanishing) OpenLTXFile(ctx context.Context, level int, lo, hi ltx.TXID, off, size int64) (io.ReadCloser, error) {
+ v.mu.Lock()
+ gone := off == 0 && size == 0 && v.times > 0
+ if gone {
+ v.times--
+ }
+ v.mu.Unlock()
+ if gone {
+ return nil, os.ErrNotExist
+ }
+ return v.ReplicaReader.OpenLTXFile(ctx, level, lo, hi, off, size)
+}
+
+// Review Focus 3: one quiet re-plan, then changed-twice, and no chunk.
+func TestReadRePlansOnceThenSaysChangedTwice(t *testing.T) {
+ t.Parallel()
+ for _, c := range []struct {
+ times int
+ want string
+ }{{1, ""}, {2, CodeChangedTwice}} {
+ fx := newFixture(t, 2)
+ v := &vanishing{ReplicaReader: lsfile.NewReplicaClient(fx.replica), times: c.times}
+ a, _ := fx.agent("uk-1", func(cfg *AgentConfig, _ *inProcWorkers) {
+ cfg.OpenReplica = func(context.Context, string) (dbdoor.ReplicaReader, error) { return v, nil }
+ })
+ req := fx.request("SELECT count(*) FROM t", "")
+ _, _, r := serveOne(t, a, fx, req)
+ if r.Code != c.want {
+ t.Fatalf("vanished %d times: %+v", c.times, r)
+ }
+ if c.want != "" && len(fx.chunkKeys(req.ID)) != 0 {
+ t.Fatal("a chunk exists for a read that never ran")
+ }
+ }
+}
+
+// stageHook changes the record at one stage of the read: planning (the
+// first listing), download (the first full-file open), or after the last
+// byte has been read from the bucket (EOF of the last planned file).
+type stageHook struct {
+ dbdoor.ReplicaReader
+ stage, lastSpan string
+ once sync.Once
+ change func()
+}
+
+func (h *stageHook) LTXFiles(ctx context.Context, level int, seek ltx.TXID, m bool) (ltx.FileIterator, error) {
+ if h.stage == "planning" {
+ h.once.Do(h.change)
+ }
+ return h.ReplicaReader.LTXFiles(ctx, level, seek, m)
+}
+
+func (h *stageHook) OpenLTXFile(ctx context.Context, level int, lo, hi ltx.TXID, off, size int64) (io.ReadCloser, error) {
+ rc, err := h.ReplicaReader.OpenLTXFile(ctx, level, lo, hi, off, size)
+ if err != nil || off != 0 || size != 0 {
+ return rc, err
+ }
+ switch {
+ case h.stage == "download":
+ h.once.Do(h.change)
+ case h.stage == "after the last byte" && dbdoor.Span(uint64(lo), uint64(hi)) == h.lastSpan:
+ return &eofHook{ReadCloser: rc, at: func() { h.once.Do(h.change) }}, nil
+ }
+ return rc, nil
+}
+
+type eofHook struct {
+ io.ReadCloser
+ at func()
+}
+
+func (e *eofHook) Read(p []byte) (int, error) {
+ n, err := e.ReadCloser.Read(p)
+ if err == io.EOF {
+ e.at()
+ }
+ return n, err
+}
+
+// T9: a record change at every stage ends with its code, and no chunk is
+// ever written (codex 1).
+func TestRecordChangeAtEveryStageWritesNoChunk(t *testing.T) {
+ t.Parallel()
+ no := false
+ changes := map[string]struct {
+ change func(fx *fixture)
+ want string
+ }{
+ "moved": {func(fx *fixture) { r := fx.record(); r.Region = "ie"; fx.putRecord(r) }, CodeRecordChanged},
+ "moving": {func(fx *fixture) { r := fx.record(); r.Move = &provision.MoveState{ID: "mv_x", To: "ie", State: "requested"}; fx.putRecord(r) }, CodeMoving},
+ "recreated in the account": {func(fx *fixture) { r := fx.record(); r.CreatedAt = r.CreatedAt.Add(time.Hour); fx.putRecord(r) }, CodeRecordChanged},
+ "recreated in another account": {func(fx *fixture) {
+ created := fx.record().CreatedAt
+ fx.store.Delete(context.Background(), provision.RecordKey(fxAccount, fxApp, fxHost))
+ fx.putRecordUnder("bac", provision.Record{Region: "uk", CreatedAt: created})
+ }, CodeRecordChanged},
+ "class changed": {func(fx *fixture) { r := fx.record(); r.Hibernate = &no; fx.putRecord(r) }, CodeRecordChanged},
+ }
+ for _, stage := range []string{"before the pick", "planning", "download", "after the last byte"} {
+ for name, c := range changes {
+ t.Run(stage+"/"+name, func(t *testing.T) {
+ t.Parallel()
+ fx := newFixture(t, 2)
+ plan, err := dbdoor.PlanFrom(context.Background(), lsfile.NewReplicaClient(fx.replica), time.Time{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ last := plan.Planned[len(plan.Planned)-1]
+ hook := &stageHook{ReplicaReader: lsfile.NewReplicaClient(fx.replica), stage: stage,
+ lastSpan: dbdoor.Span(last.MinTXID, last.MaxTXID), change: func() { c.change(fx) }}
+ a, _ := fx.agent("uk-1", func(cfg *AgentConfig, _ *inProcWorkers) {
+ cfg.OpenReplica = func(context.Context, string) (dbdoor.ReplicaReader, error) { return hook, nil }
+ })
+ req := fx.request("SELECT count(*) FROM t", "")
+ if stage == "before the pick" {
+ c.change(fx)
+ }
+ _, _, r := serveOne(t, a, fx, req)
+ if r.State != StateRefused || r.Code != c.want {
+ t.Fatalf("result %+v, want refused %s", r, c.want)
+ }
+ if keys := fx.chunkKeys(req.ID); len(keys) != 0 {
+ t.Fatalf("chunks written: %v", keys)
+ }
+ })
+ }
+ }
+}
+
+func TestReadTooLargeAndNoRoom(t *testing.T) {
+ t.Parallel()
+ fx := newFixture(t, 2)
+ a, _ := fx.agent("uk-1", func(c *AgentConfig, _ *inProcWorkers) { c.MaxRestored = 1024 })
+ _, s, r := serveOne(t, a, fx, fx.request("SELECT 1", ""))
+ if r.Code != CodeTooLarge || s.Cap != 1024 || s.Restored <= 1024 {
+ t.Fatalf("too large: %+v %+v", r, s)
+ }
+ fx2 := newFixture(t, 2)
+ b, _ := fx2.agent("uk-1", func(c *AgentConfig, _ *inProcWorkers) {
+ calls := 0
+ c.Free = func() (uint64, uint64, error) {
+ calls++
+ if calls == 1 { // the floor fits; the plan's growth does not
+ return 11 << 30, 20 << 30, nil
+ }
+ return 5<<30 + 1, 20 << 30, nil
+ }
+ })
+ _, _, r2 := serveOne(t, b, fx2, fx2.request("SELECT 1", ""))
+ if r2.Code != CodeNoRoom {
+ t.Fatalf("no room: %+v", r2)
+ }
+}
+
+// Review Focus 4: the cap is exact, a row bigger than a chunk is split, and
+// the summary covers exactly the kept bytes.
+func TestChunkWriterCapIsExact(t *testing.T) {
+ t.Parallel()
+ for _, extra := range []int{0, 1} {
+ fx := newFixture(t, 0)
+ max := int64(3*ChunkBytes + 7)
+ cw := newChunkWriter(context.Background(), fx.store, "dbr_aaaaaaaaaaaaaaaaaaaaaaaaaa", fx.reply.PublicKey(), StreamRows, time.Hour, max)
+ big := bytes.Repeat([]byte("x"), 2*ChunkBytes+5)
+ rest := bytes.Repeat([]byte("y"), int(max)-len(big)+extra)
+ cw.Write(big)
+ _, err := cw.Write(rest)
+ if (extra == 1) != (err == errCapped) || cw.capped() != (extra == 1) {
+ t.Fatalf("extra %d: Write = %v, capped %v", extra, err, cw.capped())
+ }
+ last, n, sum, err := cw.close()
+ if err != nil || n != max {
+ t.Fatalf("extra %d: kept %d of %d: %v", extra, n, max, err)
+ }
+ want := sha256.Sum256(append(append([]byte{}, big...), rest[:int(max)-len(big)]...))
+ if sum != hex.EncodeToString(want[:]) {
+ t.Fatal("the summary hash is not the kept bytes'")
+ }
+ for seq := int64(1); seq <= last; seq++ {
+ sealed, _, err := fx.store.Get(context.Background(), ChunkKey("dbr_aaaaaaaaaaaaaaaaaaaaaaaaaa", seq))
+ if err != nil || len(sealed)-SealOverhead > ChunkBytes {
+ t.Fatalf("chunk %d: %d bytes, %v", seq, len(sealed), err)
+ }
+ }
+ }
+}
+
+// The output cap end to end: done, output-cap, and exactly the cap kept.
+func TestReadOutputCap(t *testing.T) {
+ t.Parallel()
+ fx := newFixture(t, 2)
+ a, _ := fx.agent("uk-1", func(c *AgentConfig, _ *inProcWorkers) { c.MaxOutput = 100 })
+ rows, s, r := serveOne(t, a, fx, fx.request("SELECT body FROM t", ""))
+ if r.State != StateDone || r.Code != CodeOutputCap || len(rows) != 100 || s.Bytes != 100 {
+ t.Fatalf("result %+v, %d bytes", r, len(rows))
+ }
+}
+
+func TestCancelStopsTheOwner(t *testing.T) {
+ t.Parallel()
+ fx := newFixture(t, 2)
+ hang := make(chan struct{})
+ t.Cleanup(func() { close(hang) })
+ a, _ := fx.agent("uk-1", func(c *AgentConfig, w *inProcWorkers) {
+ c.CancelEvery = 20 * time.Millisecond
+ w.opt.Restore = func(ctx context.Context, _ litestream.ReplicaClient, _ string, _ uint64) error {
+ select {
+ case <-ctx.Done():
+ case <-hang:
+ }
+ return ctx.Err()
+ }
+ })
+ req := fx.request("SELECT 1", "")
+ e := fx.submit(req)
+ a.consider(context.Background(), e)
+ if _, err := fx.store.Put(context.Background(), CancelKey(req.ID), nil, ""); err != nil {
+ t.Fatal(err)
+ }
+ done := make(chan struct{})
+ go func() { a.Wait(); close(done) }()
+ select {
+ case <-done:
+ case <-time.After(5 * time.Second):
+ t.Fatal("the owner did not stop within 5 s of cancel")
+ }
+ if _, _, r := fx.answer(req.ID); r.State != StateCancelled {
+ t.Fatalf("result %+v", r)
+ }
+}
+
+// strictStore fails the test on any protocol delete or replacing write
+// under db-reads/ (T16, the box's half).
+type strictStore struct {
+ release.Store
+ t *testing.T
+}
+
+func (s strictStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
+ if strings.HasPrefix(key, Prefix) && ifMatch != "" {
+ s.t.Errorf("a replacing write of %s", key)
+ }
+ return s.Store.Put(ctx, key, body, ifMatch)
+}
+
+func (s strictStore) Delete(ctx context.Context, key string) error {
+ if strings.HasPrefix(key, Prefix) {
+ s.t.Errorf("a delete of %s", key)
+ }
+ return s.Store.Delete(ctx, key)
+}
+
+func TestReadNeverDeletesOrReplaces(t *testing.T) {
+ t.Parallel()
+ fx := newFixture(t, 2)
+ a, _ := fx.agent("uk-1", func(c *AgentConfig, _ *inProcWorkers) { c.Store = strictStore{Store: fx.store, t: t} })
+ if _, _, r := serveOne(t, a, fx, fx.request("SELECT count(*) FROM t", "")); r.State != StateDone {
+ t.Fatalf("%+v", r)
+ }
+}
+
+func TestOwnerDeadlineEndsAHungRead(t *testing.T) {
+ t.Parallel()
+ fx := newFixture(t, 1)
+ var started atomic.Bool
+ a, _ := fx.agent("uk-1", func(c *AgentConfig, w *inProcWorkers) {
+ c.OwnerDeadline = 200 * time.Millisecond
+ w.opt.Restore = func(ctx context.Context, _ litestream.ReplicaClient, _ string, _ uint64) error {
+ started.Store(true)
+ <-ctx.Done()
+ return ctx.Err()
+ }
+ })
+ _, _, r := serveOne(t, a, fx, fx.request("SELECT 1", ""))
+ if !started.Load() || r.State != StateFailed || r.Code != CodeInterrupted {
+ t.Fatalf("result %+v", r)
+ }
+}
+```
+
+- [ ] **Step 2: Run them to verify they fail**
+
+Run: `go test ./internal/dbreads/ -run 'TestRead|TestRecordChange|TestChunkWriter|TestCancel|TestOwnerDeadline' -count=1`
+Expected: FAIL, `newChunkWriter` undefined and the stub `serve` writes no result.
+
+- [ ] **Step 3: Write `chunks.go`**
+
+```go
+package dbreads
+
+import (
+ "bytes"
+ "context"
+ "crypto/ecdh"
+ "crypto/sha256"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "hash"
+ "sync"
+ "time"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+var errCapped = errors.New("dbreads: the output cap was reached")
+
+// putCreateOnly creates key with body, retrying until it lands or ctx ends.
+// An error or a conflict is resolved by reading the key back: the same
+// bytes mean an earlier attempt landed (spec §5.4). The caller keeps body
+// until this returns, so a retry is the same bytes.
+func putCreateOnly(ctx context.Context, s release.Store, key string, body []byte) error {
+ wait := 250 * time.Millisecond
+ for {
+ _, err := s.Put(ctx, key, body, "")
+ if err == nil {
+ return nil
+ }
+ got, _, gerr := s.Get(ctx, key)
+ switch {
+ case gerr == nil && bytes.Equal(got, body):
+ return nil
+ case gerr == nil:
+ return fmt.Errorf("dbreads: %s holds other bytes: %w", key, release.ErrCASConflict)
+ }
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-time.After(wait):
+ }
+ wait = min(wait*2, 5*time.Second)
+ }
+}
+
+// chunkWriter seals a read's output into out/<seq> objects: one every
+// ChunkBytes, and whatever is pending every `every`; close seals what is
+// left, possibly nothing, as the last chunk. Past max nothing more is kept
+// and Write answers errCapped. Each put is create-only and confirmed before
+// the next is sealed, under the lock, so seqs are in order (the run
+// chunker's discipline).
+type chunkWriter struct {
+ ctx context.Context
+ store release.Store
+ id string
+ pub *ecdh.PublicKey
+ stream string
+ max int64
+
+ mu sync.Mutex
+ pending []byte
+ seq int64
+ total int64
+ sum hash.Hash
+ err error
+ isCapped bool
+
+ stop chan struct{}
+ done chan struct{}
+}
+
+func newChunkWriter(ctx context.Context, s release.Store, id string, pub *ecdh.PublicKey, stream string, every time.Duration, max int64) *chunkWriter {
+ c := &chunkWriter{ctx: ctx, store: s, id: id, pub: pub, stream: stream, max: max, sum: sha256.New(),
+ stop: make(chan struct{}), done: make(chan struct{})}
+ go c.tick(every)
+ return c
+}
+
+func (c *chunkWriter) tick(every time.Duration) {
+ defer close(c.done)
+ t := time.NewTicker(every)
+ defer t.Stop()
+ for {
+ select {
+ case <-c.stop:
+ return
+ case <-t.C:
+ c.mu.Lock()
+ if len(c.pending) > 0 && c.err == nil {
+ c.err = c.flushLocked(c.pending, false)
+ c.pending = nil
+ }
+ c.mu.Unlock()
+ }
+ }
+}
+
+func (c *chunkWriter) Write(p []byte) (int, error) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ if c.err != nil {
+ return 0, c.err
+ }
+ if c.isCapped {
+ return 0, errCapped
+ }
+ capped := false
+ if room := c.max - c.total; int64(len(p)) > room {
+ p, capped = p[:room], true
+ }
+ c.sum.Write(p)
+ c.total += int64(len(p))
+ c.pending = append(c.pending, p...)
+ for len(c.pending) >= ChunkBytes {
+ if err := c.flushLocked(c.pending[:ChunkBytes], false); err != nil {
+ c.err = err
+ return 0, err
+ }
+ c.pending = append([]byte(nil), c.pending[ChunkBytes:]...)
+ }
+ if capped {
+ c.isCapped = true
+ return len(p), errCapped
+ }
+ return len(p), nil
+}
+
+func (c *chunkWriter) flushLocked(b []byte, last bool) error {
+ c.seq++
+ sealed, err := SealChunk(c.pub, c.id, c.seq, c.stream, last, b)
+ if err != nil {
+ return err
+ }
+ return putCreateOnly(c.ctx, c.store, ChunkKey(c.id, c.seq), sealed)
+}
+
+func (c *chunkWriter) capped() bool {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ return c.isCapped
+}
+
+// close seals the pending bytes as the last chunk and reports the last seq,
+// the plaintext byte count and its SHA-256.
+func (c *chunkWriter) close() (int64, int64, string, error) {
+ close(c.stop)
+ <-c.done
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ if c.err == nil {
+ c.err = c.flushLocked(c.pending, true)
+ c.pending = nil
+ }
+ return c.seq, c.total, hex.EncodeToString(c.sum.Sum(nil)), c.err
+}
+```
+
+- [ ] **Step 4: Write `read.go`**
+
+```go
+package dbreads
+
+import (
+ "context"
+ "crypto/ecdh"
+ "encoding/json"
+ "errors"
+ "io"
+ "net"
+ "os"
+ "sync/atomic"
+ "time"
+
+ "github.com/superfly/ltx"
+
+ "github.com/carlosframework/platform/internal/activator"
+ "github.com/carlosframework/platform/internal/dbdoor"
+)
+
+// serve runs one read this agent owns, inside the owner's deadline, and
+// writes its result (spec §5.5 step 5). The read slot is given back here;
+// the reservation stays until the worker unit is gone (J5).
+func (a *Agent) serve(parent context.Context, req Request, p Picked) {
+ defer a.reads.Done()
+ defer a.adm.finish(req.ID)
+ ctx, cancel := context.WithTimeout(parent, a.cfg.OwnerDeadline)
+ defer cancel()
+ var cancelled atomic.Bool
+ go a.watchCancel(ctx, req.ID, cancel, &cancelled)
+ started := a.now()
+ res := a.read(ctx, req)
+ if cancelled.Load() {
+ res.State, res.Code = StateCancelled, ""
+ }
+ res.Box, res.Started, res.Ended = a.cfg.Box, started, a.now()
+ body, err := json.Marshal(res)
+ if err == nil {
+ wctx, wcancel := context.WithTimeout(context.WithoutCancel(parent), 30*time.Second)
+ err = putCreateOnly(wctx, a.cfg.Store, ResultKey(req.ID), body)
+ wcancel()
+ }
+ if err != nil {
+ a.logf("dbreads: %s: write the result: %v", req.ID, err)
+ }
+}
+
+// watchCancel reads cancel every CancelEvery and ends the read when it
+// appears: the console's best-effort word that nobody is waiting.
+func (a *Agent) watchCancel(ctx context.Context, id string, cancel context.CancelFunc, seen *atomic.Bool) {
+ t := time.NewTicker(a.cfg.CancelEvery)
+ defer t.Stop()
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-t.C:
+ if _, _, err := a.cfg.Store.Get(ctx, CancelKey(id)); err == nil {
+ seen.Store(true)
+ cancel()
+ return
+ }
+ }
+ }
+}
+
+// ended seals the summary into a result.
+func (a *Agent) ended(pub *ecdh.PublicKey, id, state, code string, s Summary, last int64) Result {
+ sealed, err := SealSummary(pub, id, s)
+ if err != nil {
+ a.logf("dbreads: %s: seal the summary: %v", id, err)
+ }
+ return Result{State: state, Code: code, LastSeq: last, Sealed: sealed}
+}
+
+// read is today's "changed while it was being read" retry, box side: one
+// fresh plan, with both checks again, then changed-twice.
+func (a *Agent) read(ctx context.Context, req Request) Result {
+ pub, err := ParseReplyKey(req.ReplyKey)
+ if err != nil {
+ return Result{State: StateFailed, Code: CodeInterrupted}
+ }
+ at, _ := req.AtTime()
+ for attempt := 0; attempt < 2; attempt++ {
+ if res, retry := a.attempt(ctx, req, pub, at); !retry {
+ return res
+ }
+ }
+ return a.ended(pub, req.ID, StateFailed, CodeChangedTwice, Summary{}, 0)
+}
+
+// attempt is the identity bracket (spec §8.2): check A, plan, stream every
+// planned file into a worker that restores and checks it, check B after the
+// last byte has left the bucket, and only then run. No output exists before
+// check B passes. retry is true when a planned file vanished or changed.
+func (a *Agent) attempt(ctx context.Context, req Request, pub *ecdh.PublicKey, at time.Time) (Result, bool) {
+ fail := func(code string, s Summary) (Result, bool) {
+ return a.ended(pub, req.ID, StateFailed, code, s, 0), false
+ }
+ refuse := func(code string) (Result, bool) {
+ return a.ended(pub, req.ID, StateRefused, code, Summary{}, 0), false
+ }
+ pctx, pcancel := context.WithTimeout(ctx, a.cfg.PlanBudget)
+ defer pcancel()
+
+ rec, code, err := a.check(pctx, req) // check A
+ if err != nil {
+ a.logf("dbreads: %s: check A: %v", req.ID, err)
+ return fail(CodeInterrupted, Summary{})
+ }
+ if code != "" {
+ return refuse(code)
+ }
+ rd, err := a.cfg.OpenReplica(pctx, activator.HibernatePrefix(a.cfg.HibernatePrefix, rec.Host))
+ if err != nil {
+ a.logf("dbreads: %s: open the replica: %v", req.ID, err)
+ return fail(CodeInterrupted, Summary{})
+ }
+ plan, err := dbdoor.PlanFrom(pctx, rd, at)
+ var before *dbdoor.BeforeHistoryError
+ switch {
+ case errors.Is(err, dbdoor.ErrNothingReplicated):
+ return fail(CodeNothingReplicated, Summary{})
+ case errors.As(err, &before):
+ return fail(CodeHistoryGone, Summary{Oldest: before.Oldest})
+ case errors.Is(err, dbdoor.ErrHistoryGone):
+ return fail(CodeHistoryGone, Summary{})
+ case errors.Is(err, os.ErrNotExist):
+ return Result{}, true
+ case err != nil:
+ a.logf("dbreads: %s: plan: %v", req.ID, err)
+ return fail(CodeInterrupted, Summary{})
+ }
+ if plan.RestoredBytes > a.cfg.MaxRestored {
+ return fail(CodeTooLarge, Summary{Restored: plan.RestoredBytes, Cap: a.cfg.MaxRestored})
+ }
+ if err := a.adm.grow(req.ID, plan.LTXBytes+plan.RestoredBytes); err != nil {
+ return fail(CodeNoRoom, Summary{})
+ }
+ conn, err := a.dial(pctx)
+ if err != nil {
+ a.logf("dbreads: %s: the worker did not start: %v", req.ID, err)
+ a.askProbe()
+ return fail(CodeInterrupted, Summary{})
+ }
+ defer conn.Close()
+ defer context.AfterFunc(ctx, func() { conn.Close() })()
+ stopPlanBound := context.AfterFunc(pctx, func() { conn.Close() })
+ defer stopPlanBound()
+
+ if err := writeJSONFrame(conn, frameHeader, planHeader{V: ProtocolVersion, Op: req.Op, Files: plan.Planned,
+ Target: plan.TXID, MaxRestored: a.cfg.MaxRestored}); err != nil {
+ a.askProbe()
+ return fail(CodeInterrupted, Summary{})
+ }
+ for _, f := range plan.Planned {
+ vanished, err := sendFile(pctx, conn, rd, f)
+ if vanished {
+ return Result{}, true
+ }
+ if err != nil {
+ return fail(CodeInterrupted, Summary{})
+ }
+ }
+ typ, p, err := readFrame(conn)
+ switch {
+ case err != nil:
+ return fail(CodeInterrupted, Summary{})
+ case typ == frameEnd:
+ var e endMsg
+ json.Unmarshal(p, &e)
+ if e.Code != CodeTooLarge {
+ e.Code = CodeCheckFailed
+ }
+ return fail(e.Code, Summary{Error: e.Error, Restored: plan.RestoredBytes, Cap: a.cfg.MaxRestored})
+ }
+ var ready readyMsg
+ if typ != frameReady || json.Unmarshal(p, &ready) != nil || ready.TXID != plan.TXID {
+ return fail(CodeCheckFailed, Summary{})
+ }
+ if _, code, err := a.check(pctx, req); err != nil || code != "" { // check B
+ writeFrame(conn, frameStop, nil)
+ if err != nil {
+ return fail(CodeInterrupted, Summary{})
+ }
+ return refuse(code)
+ }
+ stopPlanBound() // the query is bounded by the worker and the owner's deadline
+ if err := writeJSONFrame(conn, frameRun, runMsg{SQL: req.SQL}); err != nil {
+ return fail(CodeInterrupted, Summary{})
+ }
+ cw := newChunkWriter(ctx, a.cfg.Store, req.ID, pub, StreamRows, a.cfg.ChunkEvery, a.cfg.MaxOutput)
+ endCode, sqlErr, rerr := relay(conn, cw)
+ last, n, sum, werr := cw.close()
+ s := Summary{TXID: plan.TXID, At: plan.At, Bytes: n, SHA256: sum, Error: sqlErr}
+ switch {
+ case werr != nil:
+ a.logf("dbreads: %s: write output: %v", req.ID, werr)
+ return a.ended(pub, req.ID, StateFailed, CodeInterrupted, Summary{}, last), false
+ case cw.capped():
+ return a.ended(pub, req.ID, StateDone, CodeOutputCap, s, last), false
+ case rerr != nil:
+ return a.ended(pub, req.ID, StateFailed, CodeInterrupted, Summary{}, last), false
+ case endCode == "":
+ return a.ended(pub, req.ID, StateDone, "", s, last), false
+ case endCode == CodeSQLError, endCode == CodeTimeout:
+ return a.ended(pub, req.ID, StateFailed, endCode, s, last), false
+ }
+ return a.ended(pub, req.ID, StateFailed, CodeInterrupted, Summary{}, last), false
+}
+
+// sendFile streams one planned file, whole, from the bucket into the
+// worker. A file that is gone, or not the size the plan listed, vanished
+// under the plan: the caller re-plans.
+func sendFile(ctx context.Context, w io.Writer, rd dbdoor.ReplicaReader, f dbdoor.LTXFile) (bool, error) {
+ rc, err := rd.OpenLTXFile(ctx, f.Level, ltx.TXID(f.MinTXID), ltx.TXID(f.MaxTXID), 0, 0)
+ if errors.Is(err, os.ErrNotExist) {
+ return true, nil
+ }
+ if err != nil {
+ return false, err
+ }
+ defer rc.Close()
+ buf := make([]byte, fileFrameBytes)
+ var sent int64
+ for {
+ n, rerr := rc.Read(buf)
+ if n > 0 {
+ if sent+int64(n) > f.Size {
+ return true, nil
+ }
+ if err := writeFrame(w, frameFile, buf[:n]); err != nil {
+ return false, err
+ }
+ sent += int64(n)
+ }
+ if rerr == io.EOF {
+ break
+ }
+ if errors.Is(rerr, os.ErrNotExist) {
+ return true, nil
+ }
+ if rerr != nil {
+ return false, rerr
+ }
+ }
+ return sent != f.Size, nil
+}
+
+// relay moves the worker's output into the chunk writer until the worker's
+// end frame, or until the cap: then the agent stops reading and closes the
+// connection, which the worker sees as EOF.
+func relay(conn net.Conn, cw *chunkWriter) (string, string, error) {
+ for {
+ typ, p, err := readFrame(conn)
+ if err != nil {
+ return "", "", err
+ }
+ switch typ {
+ case frameOutput:
+ if _, err := cw.Write(p); errors.Is(err, errCapped) {
+ return "", "", nil
+ } else if err != nil {
+ return "", "", err
+ }
+ case frameEnd:
+ var e endMsg
+ if json.Unmarshal(p, &e) != nil {
+ return "", "", errProtocol
+ }
+ return e.Code, e.Error, nil
+ default:
+ return "", "", errProtocol
+ }
+ }
+}
+```
+
+Delete the `serve` stub from `agent.go`.
+
+- [ ] **Step 5: Run the tests**
+
+Run: `go test ./internal/dbreads/ -count=1 -race`
+Expected: PASS. The package is new and small; running it whole is the targeted run.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add internal/dbreads
+git commit -m "dbreads: the owned read, its identity bracket, sealed chunks and result"
+```
+
+---
+
+### Task 12: The loop: listing the queue, the probe, the capability
+
+**Files:**
+- Modify: `internal/dbreads/agent.go` (add `Run`, `Pass`, `Probe`, `probe`)
+- Create: `internal/dbreads/loop_test.go`
+
+**Interfaces:**
+- Consumes: `consider`, `decide`, `isDecided`, `prune`, `dial`, `adm.admitProbe` (Tasks 9, 10).
+- Produces: `(*Agent).Run(ctx context.Context, nudge <-chan struct{})`; `(*Agent).Pass(ctx context.Context)`; `(*Agent).Probe(ctx context.Context)`; `errSandbox`.
+
+- [ ] **Step 1: Write the failing tests**
+
+Create `internal/dbreads/loop_test.go`:
+
+```go
+package dbreads
+
+import (
+ "context"
+ "errors"
+ "net"
+ "strings"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/benbjohnson/litestream"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// countingStore counts reads of request objects and lists.
+type countingStore struct {
+ release.Store
+ requestGets, lists atomic.Int64
+}
+
+func (s *countingStore) Get(ctx context.Context, key string) ([]byte, string, error) {
+ if strings.HasSuffix(key, "/request.json") {
+ s.requestGets.Add(1)
+ }
+ return s.Store.Get(ctx, key)
+}
+
+func (s *countingStore) List(ctx context.Context, prefix string) ([]string, error) {
+ s.lists.Add(1)
+ return s.Store.List(ctx, prefix)
+}
+
+// Review Focus 2: a request issued at 10:59:50 is listed at 11:00:20 under
+// the previous hour's prefix, and picked.
+func TestPassFindsAnEntryFromThePreviousHour(t *testing.T) {
+ t.Parallel()
+ fx := newFixture(t, 1)
+ fx.now = time.Date(2026, 10, 22, 10, 59, 50, 0, time.UTC)
+ a, _ := fx.agent("uk-1", func(c *AgentConfig, _ *inProcWorkers) {
+ c.Now = func() time.Time { return time.Date(2026, 10, 22, 11, 0, 20, 0, time.UTC) }
+ })
+ req := fx.request("SELECT count(*) FROM t", "")
+ fx.submit(req)
+ a.Pass(context.Background())
+ a.Wait()
+ if _, _, r := fx.answer(req.ID); r.State != StateDone {
+ t.Fatalf("%+v", r)
+ }
+}
+
+func TestPassSkipsAnOutOfWindowEntryWithoutReadingIt(t *testing.T) {
+ t.Parallel()
+ fx := newFixture(t, 0)
+ cs := &countingStore{Store: fx.store}
+ fx.now = time.Now().UTC().Add(-3 * time.Minute).Truncate(time.Second)
+ fx.submit(fx.request("SELECT 1", ""))
+ a, _ := fx.agent("uk-1", func(c *AgentConfig, _ *inProcWorkers) { c.Store = cs })
+ a.Pass(context.Background())
+ if cs.requestGets.Load() != 0 {
+ t.Fatal("an out-of-window request was read")
+ }
+}
+
+// T4, the box's half: a forged queue entry and an unsigned request are
+// refused, decided, and logged once.
+func TestPassRefusesForgedEntriesAndUnsignedRequests(t *testing.T) {
+ t.Parallel()
+ fx := newFixture(t, 1)
+ var logged atomic.Int64
+ a, w := fx.agent("uk-1", func(c *AgentConfig, _ *inProcWorkers) {
+ c.Logf = func(format string, args ...any) {
+ if strings.Contains(format, "refused the request") {
+ logged.Add(1)
+ }
+ }
+ })
+ forged := fx.request("SELECT 1", "")
+ wire, _ := Sign(release.RootSigner(fx.rootPriv), forged)
+ fx.store.Put(context.Background(), RequestKey(forged.ID), wire, "")
+ issued, _ := forged.Issued()
+ fx.store.Put(context.Background(), QueueKey("uk", issued.Add(-time.Second), forged.ID), nil, "")
+ unsigned := fx.request("SELECT 1", "")
+ fx.store.Put(context.Background(), RequestKey(unsigned.ID), []byte(`{"payload":"e30=","sig":""}`), "")
+ fx.store.Put(context.Background(), QueueKey("uk", issued, unsigned.ID), nil, "")
+ a.Pass(context.Background())
+ a.Pass(context.Background())
+ a.Wait()
+ for _, id := range []string{forged.ID, unsigned.ID} {
+ if _, _, err := fx.store.Get(context.Background(), PickedKey(id)); err == nil {
+ t.Errorf("%s was picked", id)
+ }
+ }
+ if logged.Load() != 2 || w.dials.Load() != 0 {
+ t.Fatalf("logged %d refusals, dialed %d workers", logged.Load(), w.dials.Load())
+ }
+}
+
+// T4 and codex 5: the same envelope offered again, before and after the
+// agent restarts, is never run twice: picked holds.
+func TestReplayIsRefusedByPickedAcrossARestart(t *testing.T) {
+ t.Parallel()
+ fx := newFixture(t, 1)
+ a, _ := fx.agent("uk-1")
+ req := fx.request("SELECT count(*) FROM t", "")
+ fx.submit(req)
+ a.Pass(context.Background())
+ a.Wait()
+ a.Pass(context.Background()) // the same agent again
+ restarted, w := fx.agent("uk-1")
+ restarted.Pass(context.Background())
+ restarted.Wait()
+ if w.dials.Load() != 0 {
+ t.Fatal("a restarted agent ran a read it had already picked")
+ }
+}
+
+func TestTwoAgentsServeEachReadOnce(t *testing.T) {
+ t.Parallel()
+ fx := newFixture(t, 1)
+ a, wa := fx.agent("uk-1")
+ b, wb := fx.agent("uk-2")
+ var ids []string
+ for i := 0; i < 10; i++ {
+ req := fx.request("SELECT count(*) FROM t", "")
+ fx.submit(req)
+ ids = append(ids, req.ID)
+ }
+ for round := 0; round < 20; round++ {
+ done := make(chan struct{}, 2)
+ go func() { a.Pass(context.Background()); done <- struct{}{} }()
+ go func() { b.Pass(context.Background()); done <- struct{}{} }()
+ <-done
+ <-done
+ a.Wait()
+ b.Wait()
+ }
+ for _, id := range ids {
+ if _, _, r := fx.answer(id); r.State != StateDone {
+ t.Fatalf("%s: %+v", id, r)
+ }
+ }
+ if got := wa.dials.Load() + wb.dials.Load(); got != int64(len(ids)) {
+ t.Fatalf("%d worker connections for %d reads", got, len(ids))
+ }
+}
+
+func TestPassNeedsTheCapability(t *testing.T) {
+ t.Parallel()
+ fx := newFixture(t, 0)
+ cs := &countingStore{Store: fx.store}
+ a, _ := fx.agent("uk-1", func(c *AgentConfig, _ *inProcWorkers) { c.Store = cs })
+ a.probeOK.Store(false)
+ a.Pass(context.Background())
+ if cs.lists.Load() != 0 {
+ t.Fatal("an incapable box listed the queue")
+ }
+}
+
+func TestProbe(t *testing.T) {
+ t.Parallel()
+ for name, c := range map[string]struct {
+ self func() HelloReply
+ dial error
+ want bool
+ }{
+ "a sandboxed worker": {func() HelloReply { return HelloReply{V: ProtocolVersion, Interfaces: []string{"lo"}} }, nil, true},
+ "another version": {func() HelloReply { return HelloReply{V: 2, Interfaces: []string{"lo"}} }, nil, false},
+ "a network": {func() HelloReply { return HelloReply{V: 1, Interfaces: []string{"lo", "eth0"}} }, nil, false},
+ "a visible /data": {func() HelloReply { return HelloReply{V: 1, Interfaces: []string{"lo"}, DataEntries: 3} }, nil, false},
+ "a readable host.env": {func() HelloReply { return HelloReply{V: 1, Interfaces: []string{"lo"}, EtcReadable: true} }, nil, false},
+ "no service": {nil, errors.New("connection refused"), false},
+ } {
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+ fx := newFixture(t, 0)
+ a, _ := fx.agent("uk-1", func(cfg *AgentConfig, w *inProcWorkers) { w.opt.Self = c.self })
+ if c.dial != nil {
+ a.cfg.DialWorker = func(context.Context) (net.Conn, error) { return nil, c.dial }
+ }
+ a.probeOK.Store(!c.want)
+ a.Probe(context.Background())
+ if a.Capable() != c.want {
+ t.Fatalf("Capable = %v", a.Capable())
+ }
+ })
+ }
+}
+
+// Review Focus 5 and spec §7.1: a probe beside an orphan, or with no unit
+// listing, is skipped and the last result stands.
+func TestProbeIsSkippedWhileAdmissionIsBlocked(t *testing.T) {
+ t.Parallel()
+ for _, last := range []bool{true, false} {
+ fx := newFixture(t, 0)
+ a, _ := fx.agent("uk-1", func(c *AgentConfig, _ *inProcWorkers) {
+ c.Units = func(context.Context) (int, error) { return 1, nil } // an orphan
+ })
+ a.probeOK.Store(last)
+ a.Probe(context.Background())
+ if a.Capable() != last {
+ t.Fatalf("a skipped probe changed the capability from %v", last)
+ }
+ }
+}
+
+// T13, the unit half: a box running two reads keeps the capability; its
+// probe gets the third connection.
+func TestProbeOnTheThirdConnection(t *testing.T) {
+ t.Parallel()
+ fx := newFixture(t, 1)
+ hang := make(chan struct{})
+ a, w := fx.agent("uk-1", func(c *AgentConfig, w *inProcWorkers) {
+ w.opt.Restore = func(ctx context.Context, _ litestream.ReplicaClient, _ string, _ uint64) error {
+ <-hang
+ return errors.New("released")
+ }
+ })
+ for i := 0; i < 2; i++ {
+ fx.submit(fx.request("SELECT 1", ""))
+ }
+ a.Pass(context.Background())
+ deadline := time.Now().Add(5 * time.Second)
+ for w.live.Load() < 2 && time.Now().Before(deadline) {
+ time.Sleep(10 * time.Millisecond)
+ }
+ a.Probe(context.Background())
+ if !a.Capable() {
+ t.Fatal("a busy box lost the capability")
+ }
+ close(hang)
+ a.Wait()
+}
+
+func TestRunPassesOnANudge(t *testing.T) {
+ t.Parallel()
+ fx := newFixture(t, 1)
+ a, _ := fx.agent("uk-1")
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ nudge := make(chan struct{}, 1)
+ go a.Run(ctx, nudge)
+ req := fx.request("SELECT count(*) FROM t", "")
+ fx.submit(req)
+ nudge <- struct{}{}
+ deadline := time.Now().Add(10 * time.Second)
+ for time.Now().Before(deadline) {
+ if _, _, err := fx.store.Get(context.Background(), ResultKey(req.ID)); err == nil {
+ return
+ }
+ time.Sleep(20 * time.Millisecond)
+ }
+ t.Fatal("a nudge did not serve the read")
+}
+```
+
+- [ ] **Step 2: Run them to verify they fail**
+
+Run: `go test ./internal/dbreads/ -run 'TestPass|TestReplay|TestTwoAgents|TestProbe|TestRun' -count=1`
+Expected: FAIL, `Pass`, `Probe`, `Run` undefined.
+
+- [ ] **Step 3: Add the loop to `agent.go`**
+
+```go
+var errSandbox = errors.New("dbreads: the worker can see more than its sandbox allows")
+
+// Run is the db-reads loop: a probe at start, a pass on every nudge (a
+// serial move) and every ListEvery, a probe every ProbeEvery and whenever a
+// read's worker failed to start (spec §5.5 step 4, §7.1).
+func (a *Agent) Run(ctx context.Context, nudge <-chan struct{}) {
+ a.Probe(ctx)
+ list := time.NewTicker(ListEvery)
+ defer list.Stop()
+ probe := time.NewTicker(ProbeEvery)
+ defer probe.Stop()
+ for {
+ select {
+ case <-ctx.Done():
+ a.reads.Wait()
+ return
+ case <-nudge:
+ a.Pass(ctx)
+ case <-list.C:
+ a.Pass(ctx)
+ case <-probe.C:
+ a.Probe(ctx)
+ case <-a.probeAfter:
+ a.Probe(ctx)
+ }
+ }
+}
+
+// Pass lists this region's queue for this hour and the last, and considers
+// every in-window entry it has not decided. An out-of-window entry is
+// skipped on its key alone. A box whose capability is withdrawn takes no
+// new read; one it already holds runs on (spec §11.2).
+func (a *Agent) Pass(ctx context.Context) {
+ if !a.Capable() {
+ return
+ }
+ now := a.now()
+ a.prune(now)
+ for _, hour := range []time.Time{now.Add(-time.Hour), now} {
+ keys, err := a.cfg.Store.List(ctx, QueueHourPrefix(a.cfg.Region, hour))
+ if err != nil {
+ a.logf("dbreads: list the %s queue: %v", a.cfg.Region, err)
+ continue
+ }
+ for _, key := range keys {
+ e, ok := ParseQueueKey(key)
+ if !ok || e.Region != a.cfg.Region || !InWindow(e.Issued, now) || a.isDecided(e.ID) {
+ continue
+ }
+ a.consider(ctx, e)
+ }
+ }
+}
+
+// Probe is the readiness probe (spec §7.1): a hello on its own connection,
+// so a busy box is never mistaken for a broken one. While admission is
+// blocked (an orphan unit, or no unit listing) it is skipped and the last
+// result stands.
+func (a *Agent) Probe(ctx context.Context) {
+ if !a.adm.admitProbe(ctx) {
+ return
+ }
+ defer a.adm.probeDone()
+ err := a.probe(ctx)
+ if err != nil {
+ a.logf("dbreads: readiness probe: %v", err)
+ }
+ a.probeOK.Store(err == nil)
+}
+
+func (a *Agent) probe(ctx context.Context) error {
+ pctx, cancel := context.WithTimeout(ctx, 30*time.Second)
+ defer cancel()
+ conn, err := a.dial(pctx)
+ if err != nil {
+ return err
+ }
+ defer conn.Close()
+ defer context.AfterFunc(pctx, func() { conn.Close() })()
+ if err := writeJSONFrame(conn, frameHello, hello{V: ProtocolVersion}); err != nil {
+ return err
+ }
+ typ, p, err := readFrame(conn)
+ if err != nil {
+ return err
+ }
+ var h HelloReply
+ if typ != frameHelloReply || json.Unmarshal(p, &h) != nil {
+ return errProtocol
+ }
+ if h.V != ProtocolVersion {
+ return fmt.Errorf("dbreads: the worker speaks protocol %d, this agent %d", h.V, ProtocolVersion)
+ }
+ if len(h.Interfaces) != 1 || h.Interfaces[0] != "lo" || h.DataEntries != 0 || h.EtcReadable {
+ return fmt.Errorf("%w: %+v", errSandbox, h)
+ }
+ return nil
+}
+```
+
+Add `"fmt"` to `agent.go`'s imports.
+
+- [ ] **Step 4: Run the package**
+
+Run: `go test ./internal/dbreads/ -count=1 -race`
+Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add internal/dbreads
+git commit -m "dbreads: the loop, the readiness probe and the capability"
+```
+
+---
+
+### Task 13: Host wiring: the units, arming, the capability, the nudge
+
+**Files:**
+- Create: `internal/host/units/carlos-dbread.socket`
+- Create: `internal/host/units/carlos-dbread@.service`
+- Create: `internal/host/dbreads.go`
+- Create: `internal/host/dbreads_linux.go`
+- Create: `internal/host/dbreads_other.go`
+- Create: `internal/host/dbreads_test.go`
+- Modify: `internal/host/systemunits.go` (install and arm the two units)
+- Modify: `internal/host/systemunits_test.go:66-71` (an armed box now answers four probes)
+- Modify: `internal/host/edgesfile.go:890` (publish the capability)
+- Modify: `internal/host/agent.go:612` (arm the loop) and the serial pass (`d.fullPass`, after `runRunsPass`)
+
+**Interfaces:**
+- Consumes: `dbreads.NewAgent`, `AgentConfig`, `Capability`, `ValidRegion`, `DefaultMaxRestored` (Tasks 3 to 12); `lease.ReadReplicaClient`; `openDeployStore`, `BoxLabel`, `newBootID`, `hibernationConfigured`, `hostwrite.Enabled`, `release.ParsePublicKey` (existing in `internal/host`).
+- Produces: `dbReadsSettings(getenv func(string) string) (dbReadsEnv, string)`; `armDBReads(hib activator.Config)`; `nudgeDBReads()`; `dbReadsCapable() bool`; `withDBReadCapability(caps []string, capable bool) []string`; `countDBReadUnits(out string) int`; Linux-only `dialDBReadWorker(ctx) (net.Conn, error)`, `dbReadUnits(ctx) (int, error)`, `varTmpFree() (uint64, uint64, error)`, with `!linux` stubs; constants `dbReadSocketUnit`, `dbReadSocketPath`.
+
+- [ ] **Step 1: Write the failing tests**
+
+Create `internal/host/dbreads_test.go`:
+
+```go
+package host
+
+import (
+ "bufio"
+ "bytes"
+ "crypto/ed25519"
+ "crypto/rand"
+ "encoding/base64"
+ "os"
+ "path/filepath"
+ "slices"
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/dbreads"
+)
+
+func TestDBReadsSettingsAreSpec71(t *testing.T) {
+ t.Parallel()
+ pub, _, _ := ed25519.GenerateKey(rand.Reader)
+ good := map[string]string{
+ "CARLOS_HIBERNATE_BUCKET": "data-uk", "CARLOS_REGION": "uk",
+ "CARLOS_RELEASE_PUBKEY": base64.StdEncoding.EncodeToString(pub),
+ }
+ env := func(over map[string]string) func(string) string {
+ return func(k string) string {
+ if v, ok := over[k]; ok {
+ return v
+ }
+ return good[k]
+ }
+ }
+ got, why := dbReadsSettings(env(nil))
+ if why != "" || got.region != "uk" || got.maxRestored != dbreads.DefaultMaxRestored {
+ t.Fatalf("a capable box: %+v %q", got, why)
+ }
+ if got, why := dbReadsSettings(env(map[string]string{"CARLOS_DB_READ_MAX_BYTES": "1073741824"})); why != "" || got.maxRestored != 1<<30 {
+ t.Fatalf("the size cap: %+v %q", got, why)
+ }
+ for name, over := range map[string]map[string]string{
+ "no hibernation": {"CARLOS_HIBERNATE_BUCKET": ""},
+ "no region": {"CARLOS_REGION": ""},
+ "a fleet placement": {"CARLOS_REGION": "fleet/acme"},
+ "no release key": {"CARLOS_RELEASE_PUBKEY": ""},
+ "switched off": {"CARLOS_DB_READS": "0"},
+ "host claims": {"CARLOS_HOST_CLAIM": "1"},
+ "a cap that is not a number": {"CARLOS_DB_READ_MAX_BYTES": "2GB"},
+ } {
+ if _, why := dbReadsSettings(env(over)); why == "" {
+ t.Errorf("%s: armed", name)
+ }
+ }
+}
+
+func TestCountDBReadUnits(t *testing.T) {
+ t.Parallel()
+ out := strings.Join([]string{
+ "carlos-dbread@0-1234-999.service loaded active running carlos db-read worker",
+ "carlos-dbread@1-1235-999.service loaded activating start carlos db-read worker",
+ "carlos-dbread@2-1236-999.service loaded deactivating stop-sigterm carlos db-read worker",
+ "carlos-dbread@3-1237-999.service loaded failed failed carlos db-read worker",
+ "carlos-dbread@4-1238-999.service loaded inactive dead carlos db-read worker",
+ "",
+ }, "\n")
+ if n := countDBReadUnits(out); n != 3 {
+ t.Fatalf("counted %d units, want active, activating and deactivating: 3", n)
+ }
+}
+
+func TestWithDBReadCapability(t *testing.T) {
+ t.Parallel()
+ base := []string{"region-ha-1"}
+ if got := withDBReadCapability(base, false); !slices.Equal(got, base) {
+ t.Fatalf("incapable: %v", got)
+ }
+ if got := withDBReadCapability(base, true); !slices.Equal(got, []string{"region-ha-1", dbreads.Capability}) || len(base) != 1 {
+ t.Fatalf("capable: %v (base %v)", got, base)
+ }
+}
+
+func unitLines(t *testing.T, name string) []string {
+ t.Helper()
+ b, err := unitsFS.ReadFile("units/" + name)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var lines []string
+ sc := bufio.NewScanner(bytes.NewReader(b))
+ for sc.Scan() {
+ if l := strings.TrimSpace(sc.Text()); l != "" && !strings.HasPrefix(l, "#") {
+ lines = append(lines, l)
+ }
+ }
+ return lines
+}
+
+// The sandbox is the unit file (spec §8.3): every line the spec names is
+// there, and nothing loosens it.
+func TestDBReadUnitsCarryTheSandbox(t *testing.T) {
+ t.Parallel()
+ sock := unitLines(t, "carlos-dbread.socket")
+ for _, want := range []string{"ListenStream=/run/carlos-dbread/worker.sock", "SocketUser=carlos", "SocketMode=0600", "Accept=yes", "MaxConnections=3"} {
+ if !slices.Contains(sock, want) {
+ t.Errorf("the socket lacks %q", want)
+ }
+ }
+ svc := unitLines(t, "carlos-dbread@.service")
+ for _, want := range []string{
+ "ExecStart=/opt/carlos/carlos system db-worker",
+ "StandardInput=socket", "StandardOutput=socket", "StandardError=journal",
+ "DynamicUser=yes", "PrivateNetwork=yes", "RestrictAddressFamilies=AF_UNIX",
+ "TemporaryFileSystem=/data:ro /var/lib:ro /opt/carlos:ro /run/carlos:ro",
+ "BindReadOnlyPaths=/opt/carlos/carlos",
+ "InaccessiblePaths=/etc/carlos -/run/carlos-control",
+ "PrivateTmp=yes", "MemoryMax=512M", "CPUQuota=100%", "TasksMax=16",
+ "LimitFSIZE=2147483648", "RuntimeMaxSec=20min",
+ "SystemCallFilter=@system-service", "NoNewPrivileges=yes", "CapabilityBoundingSet=", "ProtectProc=invisible",
+ "ProtectSystem=strict",
+ } {
+ if !slices.Contains(svc, want) {
+ t.Errorf("the service lacks %q", want)
+ }
+ }
+ for _, l := range svc {
+ for _, bad := range []string{"User=", "StandardOutput=file:", "IPAddressAllow=", "ReadWritePaths=", "AmbientCapabilities=CAP"} {
+ if strings.HasPrefix(l, bad) {
+ t.Errorf("the service loosens its sandbox: %q", l)
+ }
+ }
+ }
+}
+
+func TestEnsureSystemUnitsInstallsTheDBReadUnits(t *testing.T) {
+ t.Parallel()
+ etc := t.TempDir()
+ os.MkdirAll(filepath.Join(etc, "systemd", "system"), 0o755)
+ var calls []string
+ systemctl := func(args ...string) (string, error) {
+ calls = append(calls, strings.Join(args, " "))
+ return "", nil
+ }
+ probe := func(args ...string) (string, error) {
+ calls = append(calls, strings.Join(args, " "))
+ if args[0] == "is-active" {
+ return "inactive\n", systemctlExit(3)
+ }
+ return "disabled\n", systemctlExit(1)
+ }
+ ensureSystemUnits(etc, filepath.Join(t.TempDir(), "lock"), systemctl, probe, t.Logf)
+ for _, u := range []string{"carlos-dbread.socket", "carlos-dbread@.service"} {
+ got, err := os.ReadFile(filepath.Join(etc, "systemd", "system", u))
+ want, _ := unitsFS.ReadFile("units/" + u)
+ if err != nil || !bytes.Equal(got, want) {
+ t.Errorf("%s not installed as embedded: %v", u, err)
+ }
+ }
+ joined := strings.Join(calls, "\n")
+ for _, want := range []string{"is-active carlos-dbread.socket", "carlos-dbread.socket"} {
+ if !strings.Contains(joined, want) {
+ t.Errorf("calls lack %q:\n%s", want, joined)
+ }
+ }
+ // systemctlPlan starts every armed unit (sockets, paths, timers, mounts)
+ // in the order passed (bootstrap.go armedUnits), and the service
+ // template never.
+ if !strings.Contains(joined, "start carlos-system-update.path carlos-instance-control.socket carlos-dbread.socket") {
+ t.Errorf("the socket was not armed:\n%s", joined)
+ }
+ if strings.Contains(joined, "start carlos-dbread@") {
+ t.Fatal("a worker service was started")
+ }
+}
+```
+
+In `internal/host/systemunits_test.go`, `TestEnsureSystemUnitsInstallsAndArmsWhatIsMissing`'s armed-box case answers one more `is-active` now. Change `if len(calls) != 3` to `if len(calls) != 4` and its comment to "(is-active x3: the self-update path unit, the control socket and the db-read socket, plus is-enabled)".
+
+- [ ] **Step 2: Run them to verify they fail**
+
+Run (sandbox off): `go test ./internal/host/ -run 'TestDBReads|TestCountDBReadUnits|TestWithDBReadCapability|TestDBReadUnits|TestEnsureSystemUnits' -count=1`
+Expected: FAIL, undefined names and missing unit files.
+
+- [ ] **Step 3: Write the units**
+
+Create `internal/host/units/carlos-dbread.socket`:
+
+```ini
+# The db-read worker's socket (db region forward spec §8.3). Bound by root
+# systemd and owned by carlos, so the agent connects with no privilege of
+# its own. Each connection starts one carlos-dbread@ instance with the
+# connection as its stdin and stdout. Three connections at most: two reads
+# and the readiness probe (spec §7.1).
+[Unit]
+Description=carlos db-read worker socket
+
+[Socket]
+ListenStream=/run/carlos-dbread/worker.sock
+SocketUser=carlos
+SocketGroup=carlos
+SocketMode=0600
+Accept=yes
+MaxConnections=3
+
+[Install]
+WantedBy=sockets.target
+```
+
+Create `internal/host/units/carlos-dbread@.service`:
+
+```ini
+# The db-read worker (db region forward spec §8.3): one instance per
+# connection to carlos-dbread.socket. It writes one forwarded read's planned
+# LTX files into its own private /var/tmp, restores them, checks the copy,
+# and runs the member's one read-only statement. Everything that decodes
+# tenant bytes runs here, as a DynamicUser with no network; the agent only
+# moves bytes. systemd removes /tmp and /var/tmp when the unit stops,
+# however it stops.
+#
+# StandardInput/StandardOutput=socket: with Accept=yes systemd passes the
+# connection as a descriptor and puts it on stdin only when told to. The
+# claim-bound template's StandardOutput=file: line is deliberately absent.
+#
+# LimitFSIZE is the default restored-size cap (CARLOS_DB_READ_MAX_BYTES,
+# 2 GiB). A box that raises the cap needs a drop-in raising this too.
+[Unit]
+Description=carlos db-read worker
+
+[Service]
+ExecStart=/opt/carlos/carlos system db-worker
+StandardInput=socket
+StandardOutput=socket
+StandardError=journal
+DynamicUser=yes
+PrivateNetwork=yes
+RestrictAddressFamilies=AF_UNIX
+TemporaryFileSystem=/data:ro /var/lib:ro /opt/carlos:ro /run/carlos:ro
+BindReadOnlyPaths=/opt/carlos/carlos
+InaccessiblePaths=/etc/carlos -/run/carlos-control
+PrivateTmp=yes
+ProtectSystem=strict
+ProtectHome=yes
+PrivateDevices=yes
+ProtectProc=invisible
+ProcSubset=pid
+ProtectKernelTunables=yes
+ProtectKernelModules=yes
+ProtectKernelLogs=yes
+ProtectClock=yes
+ProtectHostname=yes
+ProtectControlGroups=yes
+RemoveIPC=yes
+LockPersonality=yes
+RestrictRealtime=yes
+RestrictNamespaces=yes
+RestrictSUIDSGID=yes
+NoNewPrivileges=yes
+CapabilityBoundingSet=
+SystemCallFilter=@system-service
+SystemCallArchitectures=native
+MemoryDenyWriteExecute=yes
+MemoryMax=512M
+CPUQuota=100%
+TasksMax=16
+LimitFSIZE=2147483648
+RuntimeMaxSec=20min
+```
+
+`/run/carlos-control` carries `-` because a box without the isolated-exec control socket has no such directory, and a missing path would fail every worker start (and so withdraw the capability on a box that is otherwise fine). `/etc/carlos` exists on every box and stays strict.
+
+- [ ] **Step 4: Install and arm them in `ensureSystemUnits`**
+
+In `internal/host/systemunits.go`:
+- add `"carlos-dbread.socket", "carlos-dbread@.service"` to `systemUnitFiles`;
+- after the `ctlActiveOut` probe add `dbrActiveOut, dbrActiveErr := probe("is-active", dbReadSocketUnit)` and `dbrActive := parseIsActive(dbrActiveOut)`, include `dbrActive == nil` in the "systemctl said nothing" refusal (with `dbrActiveErr` in its log line), and `*dbrActive` in the all-armed early return;
+- arm it: `systemctlPlan([]string{systemUpdatePathUnit, systemRecoverUnit, instanceControlSocket, dbReadSocketUnit})`.
+
+The service template is never started: `armedUnits` starts sockets, timers, paths and mounts only.
+
+- [ ] **Step 5: Write `dbreads.go` and the platform files**
+
+Create `internal/host/dbreads.go`:
+
+```go
+package host
+
+import (
+ "context"
+ "crypto/ed25519"
+ "log"
+ "os"
+ "slices"
+ "strconv"
+ "strings"
+ "sync/atomic"
+
+ "github.com/carlosframework/platform/internal/activator"
+ "github.com/carlosframework/platform/internal/dbdoor"
+ "github.com/carlosframework/platform/internal/dbreads"
+ "github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/lease"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+const (
+ dbReadSocketUnit = "carlos-dbread.socket"
+ dbReadSocketPath = "/run/carlos-dbread/worker.sock"
+ dbReadMaxBytesEnv = "CARLOS_DB_READ_MAX_BYTES"
+ dbReadsSwitchEnv = "CARLOS_DB_READS"
+)
+
+type dbReadsEnv struct {
+ region string
+ pub ed25519.PublicKey
+ maxRestored int64
+}
+
+// dbReadsSettings is the environment half of spec §7.1's conditions:
+// hibernation configured, CARLOS_REGION a region, CARLOS_RELEASE_PUBKEY
+// set, CARLOS_DB_READS not 0, CARLOS_HOST_CLAIM not set. The other half,
+// a running loop whose last probe passed, is the agent's. why is "" when
+// the loop may be armed.
+func dbReadsSettings(getenv func(string) string) (dbReadsEnv, string) {
+ switch {
+ case !hibernationConfigured(getenv):
+ return dbReadsEnv{}, "hibernation is not configured (CARLOS_HIBERNATE_BUCKET)"
+ case hostwrite.Enabled(getenv):
+ return dbReadsEnv{}, "CARLOS_HOST_CLAIM is set"
+ case strings.TrimSpace(getenv(dbReadsSwitchEnv)) == "0":
+ return dbReadsEnv{}, dbReadsSwitchEnv + " is 0"
+ }
+ region := strings.TrimSpace(getenv("CARLOS_REGION"))
+ if !dbreads.ValidRegion(region) {
+ return dbReadsEnv{}, "CARLOS_REGION is not a region"
+ }
+ pub, err := release.ParsePublicKey(getenv("CARLOS_RELEASE_PUBKEY"), "CARLOS_RELEASE_PUBKEY")
+ if err != nil {
+ return dbReadsEnv{}, err.Error()
+ }
+ env := dbReadsEnv{region: region, pub: pub, maxRestored: dbreads.DefaultMaxRestored}
+ if raw := strings.TrimSpace(getenv(dbReadMaxBytesEnv)); raw != "" {
+ n, err := strconv.ParseInt(raw, 10, 64)
+ if err != nil || n <= 0 {
+ return dbReadsEnv{}, dbReadMaxBytesEnv + " is not a whole number of bytes"
+ }
+ env.maxRestored = n
+ }
+ return env, ""
+}
+
+// armedDBReads is the armed loop; nil on a box that cannot serve.
+var armedDBReads atomic.Pointer[dbreads.Agent]
+
+// dbReadsNudge is the serial pass's word to the loop: list the queue now.
+var dbReadsNudge = make(chan struct{}, 1)
+
+func nudgeDBReads() {
+ select {
+ case dbReadsNudge <- struct{}{}:
+ default:
+ }
+}
+
+// dbReadsCapable is whether this box publishes db-read-1: the loop is armed
+// and its last readiness probe passed (spec §7.1).
+func dbReadsCapable() bool {
+ a := armedDBReads.Load()
+ return a != nil && a.Capable()
+}
+
+// withDBReadCapability is caps with db-read-1 when capable; caps is not
+// modified.
+func withDBReadCapability(caps []string, capable bool) []string {
+ if !capable {
+ return caps
+ }
+ return append(slices.Clone(caps), dbreads.Capability)
+}
+
+// countDBReadUnits reads `systemctl list-units --plain --no-legend` output:
+// carlos-dbread@ units that are active, activating or deactivating. A
+// stopped unit's scratch is gone; any other state may still hold it.
+func countDBReadUnits(out string) int {
+ n := 0
+ for _, line := range strings.Split(out, "\n") {
+ f := strings.Fields(line)
+ if len(f) < 3 || !strings.HasPrefix(f[0], "carlos-dbread@") {
+ continue
+ }
+ switch f[2] {
+ case "active", "activating", "deactivating", "reloading", "refreshing":
+ n++
+ }
+ }
+ return n
+}
+
+// armDBReads arms the db-reads loop on a box that can serve (spec §7.1).
+// Thin, like armRunsPass: the behaviour is dbreads.Agent's, tested there.
+func armDBReads(hib activator.Config) {
+ env, why := dbReadsSettings(os.Getenv)
+ if why != "" {
+ log.Printf("agent: db reads not armed: %s", why)
+ return
+ }
+ ctx := context.Background()
+ store, err := openDeployStore(ctx)
+ if err != nil {
+ log.Printf("agent: db reads not armed: %v", err)
+ return
+ }
+ a := dbreads.NewAgent(dbreads.AgentConfig{
+ Store: store, Region: env.region, Box: BoxLabel(), Boot: newBootID(), Pub: env.pub,
+ HibernatePrefix: hib.Prefix,
+ OpenReplica: func(ctx context.Context, prefix string) (dbdoor.ReplicaReader, error) {
+ return lease.ReadReplicaClient(ctx, hib.Bucket, prefix, hib.Region)
+ },
+ DialWorker: dialDBReadWorker, Units: dbReadUnits, Free: varTmpFree,
+ MaxRestored: env.maxRestored, Logf: log.Printf,
+ })
+ armedDBReads.Store(a)
+ go a.Run(ctx, dbReadsNudge)
+ log.Printf("agent: db reads armed (region %s)", env.region)
+}
+```
+
+Create `internal/host/dbreads_linux.go`:
+
+```go
+//go:build linux
+
+package host
+
+import (
+ "context"
+ "net"
+ "os/exec"
+
+ "golang.org/x/sys/unix"
+)
+
+func dialDBReadWorker(ctx context.Context) (net.Conn, error) {
+ var d net.Dialer
+ return d.DialContext(ctx, "unix", dbReadSocketPath)
+}
+
+// dbReadUnits lists carlos-dbread@ units, readable without privilege.
+func dbReadUnits(ctx context.Context) (int, error) {
+ out, err := exec.CommandContext(ctx, "systemctl", "list-units", "--all", "--plain", "--no-legend", "--no-pager", "carlos-dbread@*.service").Output()
+ if err != nil {
+ return 0, err
+ }
+ return countDBReadUnits(string(out)), nil
+}
+
+// varTmpFree is the filesystem holding /var/tmp, where every worker's
+// private /var/tmp lives.
+func varTmpFree() (uint64, uint64, error) {
+ var st unix.Statfs_t
+ if err := unix.Statfs("/var/tmp", &st); err != nil {
+ return 0, 0, err
+ }
+ return st.Bavail * uint64(st.Bsize), st.Blocks * uint64(st.Bsize), nil
+}
+```
+
+Create `internal/host/dbreads_other.go`:
+
+```go
+//go:build !linux
+
+package host
+
+import (
+ "context"
+ "errors"
+ "net"
+)
+
+var errDBReadsUnsupported = errors.New("db reads run only on Linux")
+
+func dialDBReadWorker(context.Context) (net.Conn, error) { return nil, errDBReadsUnsupported }
+
+func dbReadUnits(context.Context) (int, error) { return 0, errDBReadsUnsupported }
+
+func varTmpFree() (uint64, uint64, error) { return 0, 0, errDBReadsUnsupported }
+```
+
+- [ ] **Step 6: Publish, arm and nudge**
+
+In `internal/host/edgesfile.go`, line 890:
+
+```go
+ ident.Capabilities = withDBReadCapability(withOfflineCapability(baseCaps, armedOfflineCapable()), dbReadsCapable())
+```
+
+and extend the comment above `baseCaps` to say db-read-1 joins per pass too.
+
+In `internal/host/agent.go`, right after `armRunsPass(svc, reg, dd, rolloutSink)` (line 612):
+
+```go
+ // The db-reads loop (db region forward spec §7.1): armed
+ // only on a box that can serve; it takes nothing until a
+ // console asks.
+ armDBReads(*cfg)
+```
+
+and in `d.fullPass`, after `runRunsPass(ctx)` and its `mark("runs")`:
+
+```go
+ // A serial move may be a forwarded db read: list the queue now.
+ nudgeDBReads()
+```
+
+- [ ] **Step 7: Run the tests and the cross-compiles**
+
+Run (sandbox off):
+```bash
+go test ./internal/host/ -run 'TestDBReads|TestCountDBReadUnits|TestWithDBReadCapability|TestDBReadUnits|TestEnsureSystemUnits' -count=1
+go vet ./internal/host/ ./internal/dbreads/
+GOOS=darwin GOARCH=arm64 go build ./... && GOOS=windows GOARCH=amd64 go build ./...
+```
+Expected: PASS, no vet findings, clean builds.
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add internal/host
+git commit -m "host: carlos-dbread units, the db-reads loop armed dark, db-read-1 published while the probe passes"
+```
+
+---
+
+### Task 14: The parity half of T6, and `dbreadstest`
+
+**Files:**
+- Create: `internal/dbreads/dbreadstest/dbreadstest.go`
+- Create: `internal/console/dbreads_parity_test.go`
+
+**Interfaces:**
+- Consumes: `dbreads.NewAgent`, `Agent.Probe`, `Agent.Pass`, `Agent.Wait`, `ServeWorker`, `Sign`, `OpenSummary`, `OpenChunk`; the console's `replicaDoer` (`internal/console/seeddoer.go:28`); `dbdoor.RestoreAsOf`; `dbquery.RunContext`.
+- Produces (test support, imported only by `_test.go` files): `dbreadstest.Options`, `dbreadstest.Start(t testing.TB, o Options) *Box`, `(*Box).Serve(t testing.TB, store release.Store, id string, timeout time.Duration)`, `(*Box).Dials`, `dbreadstest.Submit(t testing.TB, store release.Store, root ed25519.PrivateKey, req dbreads.Request)`, `dbreadstest.Answer(t testing.TB, store release.Store, id string, priv *ecdh.PrivateKey, stream string) ([]byte, dbreads.Summary, dbreads.Result)`.
+
+- [ ] **Step 1: Write `dbreadstest`**
+
+```go
+// Package dbreadstest runs the box's half of a forwarded read inside a test
+// process: the real dbreads.Agent and the real dbreads.ServeWorker, joined by
+// net.Pipe instead of the installed socket. Test support only. It is not
+// acceptance for the units (internal/host's root-runner tests are), and
+// nothing outside a _test.go file may import it.
+package dbreadstest
+
+import (
+ "bytes"
+ "context"
+ "crypto/ecdh"
+ "crypto/ed25519"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "net"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/dbdoor"
+ "github.com/carlosframework/platform/internal/dbreads"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+type Options struct {
+ Store release.Store
+ Region string
+ Box string
+ Pub ed25519.PublicKey
+ HibernatePrefix string
+ OpenReplica func(ctx context.Context, prefix string) (dbdoor.ReplicaReader, error)
+ MaxRestored int64
+ // Worker's Scratch, Exit and Self are filled in when empty.
+ Worker dbreads.WorkerOptions
+}
+
+type Box struct {
+ Agent *dbreads.Agent
+ Dials atomic.Int64
+ live atomic.Int64
+ worker dbreads.WorkerOptions
+}
+
+// Start builds a capable in-process box: its probe passes against an
+// in-process worker reporting a sandboxed view.
+func Start(t testing.TB, o Options) *Box {
+ t.Helper()
+ b := &Box{worker: o.Worker}
+ if b.worker.Scratch == "" {
+ b.worker.Scratch = t.TempDir()
+ }
+ if b.worker.Exit == nil {
+ b.worker.Exit = func(int) {}
+ }
+ if b.worker.Self == nil {
+ b.worker.Self = func() dbreads.HelloReply {
+ return dbreads.HelloReply{V: dbreads.ProtocolVersion, Interfaces: []string{"lo"}}
+ }
+ }
+ b.Agent = dbreads.NewAgent(dbreads.AgentConfig{
+ Store: o.Store, Region: o.Region, Box: o.Box, Boot: "boot-" + o.Box, Pub: o.Pub,
+ HibernatePrefix: o.HibernatePrefix, OpenReplica: o.OpenReplica,
+ DialWorker: b.dial, Units: b.units,
+ Free: func() (uint64, uint64, error) { return 100 << 30, 200 << 30, nil },
+ MaxRestored: o.MaxRestored, Jitter: func() time.Duration { return 0 }, Logf: t.Logf,
+ })
+ b.Agent.Probe(context.Background())
+ if !b.Agent.Capable() {
+ t.Fatal("dbreadstest: the in-process box failed its probe")
+ }
+ return b
+}
+
+func (b *Box) dial(context.Context) (net.Conn, error) {
+ agentEnd, workerEnd := net.Pipe()
+ b.Dials.Add(1)
+ b.live.Add(1)
+ go func() {
+ defer b.live.Add(-1)
+ dbreads.ServeWorker(workerEnd, b.worker)
+ workerEnd.Close()
+ }()
+ return agentEnd, nil
+}
+
+func (b *Box) units(context.Context) (int, error) { return int(b.live.Load()), nil }
+
+// Serve passes until the read's result exists.
+func (b *Box) Serve(t testing.TB, store release.Store, id string, timeout time.Duration) {
+ t.Helper()
+ deadline := time.Now().Add(timeout)
+ for time.Now().Before(deadline) {
+ b.Agent.Pass(context.Background())
+ b.Agent.Wait()
+ if _, _, err := store.Get(context.Background(), dbreads.ResultKey(id)); err == nil {
+ return
+ }
+ time.Sleep(20 * time.Millisecond)
+ }
+ t.Fatalf("dbreadstest: no result for %s within %s", id, timeout)
+}
+
+// Submit is the console's two creates for req, signed with root: for tests
+// that have no console.
+func Submit(t testing.TB, store release.Store, root ed25519.PrivateKey, req dbreads.Request) {
+ t.Helper()
+ wire, err := dbreads.Sign(release.RootSigner(root), req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := store.Put(context.Background(), dbreads.RequestKey(req.ID), wire, ""); err != nil {
+ t.Fatal(err)
+ }
+ issued, err := req.Issued()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := store.Put(context.Background(), dbreads.QueueKey(req.Region, issued, req.ID), nil, ""); err != nil {
+ t.Fatal(err)
+ }
+}
+
+// Answer reads a read's result and opens its chunks with the CLI's key,
+// checking the count and hash a done read's summary carries.
+func Answer(t testing.TB, store release.Store, id string, priv *ecdh.PrivateKey, stream string) ([]byte, dbreads.Summary, dbreads.Result) {
+ t.Helper()
+ raw, _, err := store.Get(context.Background(), dbreads.ResultKey(id))
+ if err != nil {
+ t.Fatalf("dbreadstest: no result for %s: %v", id, err)
+ }
+ var r dbreads.Result
+ if err := json.Unmarshal(raw, &r); err != nil || r.Valid() != nil {
+ t.Fatalf("dbreadstest: result %s: %v", raw, err)
+ }
+ s, err := dbreads.OpenSummary(priv, id, r.Sealed)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var out bytes.Buffer
+ for seq := int64(1); seq <= r.LastSeq; seq++ {
+ sealed, _, err := store.Get(context.Background(), dbreads.ChunkKey(id, seq))
+ if err != nil {
+ t.Fatalf("dbreadstest: chunk %d: %v", seq, err)
+ }
+ plain, err := dbreads.OpenChunk(priv, id, seq, stream, seq == r.LastSeq, sealed)
+ if err != nil {
+ t.Fatalf("dbreadstest: chunk %d: %v", seq, err)
+ }
+ out.Write(plain)
+ }
+ if r.State == dbreads.StateDone {
+ sum := sha256.Sum256(out.Bytes())
+ if int64(out.Len()) != s.Bytes || hex.EncodeToString(sum[:]) != s.SHA256 {
+ t.Fatalf("dbreadstest: the answer does not add up: %d bytes, summary %+v", out.Len(), s)
+ }
+ }
+ return out.Bytes(), s, r
+}
+```
+
+- [ ] **Step 2: Write the parity test**
+
+Create `internal/console/dbreads_parity_test.go`:
+
+```go
+package console
+
+import (
+ "bytes"
+ "context"
+ "crypto/ed25519"
+ "crypto/rand"
+ "database/sql"
+ "encoding/json"
+ "path/filepath"
+ "testing"
+ "time"
+
+ lsfile "github.com/benbjohnson/litestream/file"
+
+ "github.com/carlosframework/platform/internal/dbdoor"
+ "github.com/carlosframework/platform/internal/dbdoor/dbdoortest"
+ "github.com/carlosframework/platform/internal/dbquery"
+ "github.com/carlosframework/platform/internal/dbreads"
+ "github.com/carlosframework/platform/internal/dbreads/dbreadstest"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// The parity half of spec T6 that needs no console server (plan 1): the box
+// path (dbreads.Agent through the real worker code, joined in process: not
+// acceptance for the units) and the CLI's in-region path (dbdoor.RestoreAsOf
+// over this package's own door code, replicaDoer and listReplicaLevel, then
+// dbquery.RunContext) print the same bytes, as of the same transaction: the
+// newest copy, a time between writes, and a time in the same second as a
+// write. Plan 2's T6 runs the same comparison through the console server and
+// the CLI binary.
+func TestDBReadsParityWithTheInRegionDoor(t *testing.T) {
+ const host = "shop.example.com"
+ dir := t.TempDir()
+ src := filepath.Join(t.TempDir(), "shop.db")
+ db, err := sql.Open("sqlite", src)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer db.Close()
+ exec := func(q string, args ...any) {
+ t.Helper()
+ if _, err := db.Exec(q, args...); err != nil {
+ t.Fatal(err)
+ }
+ }
+ nextSecond := func() time.Time {
+ time.Sleep(time.Until(time.Now().Truncate(time.Second).Add(time.Second + 10*time.Millisecond)))
+ return time.Now()
+ }
+ exec(`PRAGMA journal_mode=WAL; CREATE TABLE orders (id INTEGER PRIMARY KEY, note TEXT, data BLOB, at DATETIME)`)
+ exec(`INSERT INTO orders (note, data, at) VALUES (?, ?, ?), (NULL, NULL, NULL), (?, ?, ?)`,
+ "a\ttab", []byte{0, 0xff}, time.Date(2026, 10, 7, 14, 0, 0, 0, time.UTC),
+ "two\nlines", []byte("x"), time.Date(2026, 10, 7, 14, 0, 1, 500, time.UTC))
+ dbdoortest.Sync(t, src, dir)
+ between := nextSecond()
+ nextSecond()
+ exec(`INSERT INTO orders (note) VALUES ('later')`)
+ dbdoortest.Sync(t, src, dir)
+ sameSecond := time.Now() // in the second the last file was written
+ nextSecond()
+
+ store := release.NewDirStore(t.TempDir())
+ pub, priv, _ := ed25519.GenerateKey(rand.Reader)
+ yes := true
+ rec := provision.Record{Host: host, App: "shop", Account: "bab", Region: "uk", Hibernate: &yes, CreatedAt: time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC)}
+ body, _ := json.Marshal(rec)
+ if _, err := store.Put(context.Background(), provision.RecordKey("bab", "shop", host), body, ""); err != nil {
+ t.Fatal(err)
+ }
+ inc, _ := hostclaim.Incarnation("bab", "shop", host, rec.CreatedAt)
+ box := dbreadstest.Start(t, dbreadstest.Options{Store: store, Region: "uk", Box: "uk-1", Pub: pub, HibernatePrefix: "instances",
+ OpenReplica: func(context.Context, string) (dbdoor.ReplicaReader, error) { return lsfile.NewReplicaClient(dir), nil }})
+ const query = "SELECT id, note, data, at FROM orders ORDER BY id"
+ for name, at := range map[string]time.Time{"newest": {}, "between writes": between, "same second as a write": sameSecond} {
+ got, err := dbdoor.RestoreAsOf(context.Background(), dbdoor.NewClient(replicaDoer(lsfile.NewReplicaClient(dir))), t.TempDir(), at)
+ if err != nil {
+ t.Fatalf("%s: in-region: %v", name, err)
+ }
+ var want bytes.Buffer
+ if err := dbquery.RunContext(context.Background(), &want, got.Path, query, dbquery.Options{}); err != nil {
+ t.Fatal(err)
+ }
+ reply, _ := dbreads.NewReplyKey()
+ id, _ := dbreads.MintID()
+ atRaw := ""
+ if !at.IsZero() {
+ atRaw = at.UTC().Format(time.RFC3339Nano)
+ }
+ req := dbreads.Request{ID: id, Account: "bab", App: "shop", Host: host, Incarnation: inc, Region: "uk",
+ Op: dbreads.OpQuery, SQL: query, At: atRaw, ReplyKey: dbreads.EncodeReplyKey(reply.PublicKey()),
+ By: "member@example.com", IssuedAt: time.Now().UTC().Format(time.RFC3339)}
+ dbreadstest.Submit(t, store, priv, req)
+ box.Serve(t, store, id, 30*time.Second)
+ rows, s, r := dbreadstest.Answer(t, store, id, reply, dbreads.StreamRows)
+ if r.State != dbreads.StateDone || !bytes.Equal(rows, want.Bytes()) || s.TXID != got.TXID || !s.At.Equal(got.At) {
+ t.Fatalf("%s: box %+v txid %d at %v rows %q; in-region txid %d at %v rows %q",
+ name, r, s.TXID, s.At, rows, got.TXID, got.At, want.Bytes())
+ }
+ }
+}
+```
+
+The `at` parameter of a request is RFC 3339; the CLI sends it with sub-second precision when the person typed it, and both paths truncate to the second. `Request.AtTime` parses with `time.RFC3339`, which accepts fractional seconds.
+
+- [ ] **Step 3: Run it**
+
+Run (sandbox off): `go test ./internal/console/ -run 'TestDBReadsParityWithTheInRegionDoor' -count=1 -v`
+Expected: PASS, three cases. If it fails only on the "same second" case, the two paths disagree on rounding: that is the bug T6 exists to catch; fix `dbdoor.PlanFrom`, not the test.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add internal/dbreads/dbreadstest internal/console/dbreads_parity_test.go
+git commit -m "dbreads: dbreadstest, and T6's parity half against the in-region door"
+```
+
+---
+
+### Task 15: Root-runner acceptance: T2, T3, T12, T13
+
+These are the plan's acceptance gate. They run only on the throwaway root runner (`root-runner.py`, see the root-runner contract), never in CI and never on DOCS-T1.
+
+**Files:**
+- Create: `internal/host/dbreads_worker_dispatch_linux_test.go` (`//go:build linux`)
+- Create: `internal/host/dbreads_rootrunner_linux_test.go` (`//go:build linux && rootrunner`)
+- Modify: `internal/host/plan14_generation_registry_root_linux_test.go:18` (`TestMain`: two dispatches at the top)
+
+**Interfaces:**
+- Consumes: `ensureSystemUnits`, `systemctlProbe`, `dialDBReadWorker`, `dbReadUnits`, `varTmpFree`, `withDBReadCapability` (Task 13); `dbreads.RunWorkerWith`, `StdinConn`, `CarlosUID`, `SelfView`, `NewAgent`, `Sign`, `OpenSummary`, `OpenChunk` (Tasks 5 to 12); `dbdoortest.Replicate`.
+- Produces: test functions `TestRootDBReadServesThroughTheInstalledUnits` (T2), `TestRootDBReadWorkerSandbox` (T3), `TestRootDBReadAdmissionWithRealUnits` (T12), `TestRootDBReadReadiness` (T13), and the payload `TestRootDBReadPayload` (run only as a child, never by name).
+
+- [ ] **Step 1: Write the worker dispatch**
+
+Create `internal/host/dbreads_worker_dispatch_linux_test.go`:
+
+```go
+//go:build linux
+
+package host
+
+import (
+ "context"
+ "database/sql"
+ "encoding/json"
+ "net"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/benbjohnson/litestream"
+
+ "github.com/carlosframework/platform/internal/dbreads"
+)
+
+// dbreadTestMarkers holds marker files the root-runner tests drop to change
+// one thing about the worker. The unit can read /run outside /run/carlos.
+const dbreadTestMarkers = "/run/carlos-dbread-test"
+
+func dbreadMarker(name string) bool {
+ _, err := os.Stat(filepath.Join(dbreadTestMarkers, name))
+ return err == nil
+}
+
+// dbreadsTestWorker is this test binary acting as /opt/carlos/carlos system
+// db-worker inside the installed carlos-dbread@ unit. Without a marker it is
+// the real worker (dbreads.RunWorkerWith with RunWorker's options).
+func dbreadsTestWorker() {
+ opt := dbreads.WorkerOptions{Scratch: "/var/tmp"}
+ switch {
+ case dbreadMarker("exit-1"):
+ os.Exit(1)
+ case dbreadMarker("witness"):
+ dbreadsWitness()
+ return
+ }
+ if dbreadMarker("version-2") {
+ opt.Self = func() dbreads.HelloReply { h := dbreads.SelfView(); h.V = 2; return h }
+ }
+ if dbreadMarker("hang-restore") {
+ // A restore that ignores its context, as litestream's compactor
+ // does (spec §8.3): only the 5 s exit ends it.
+ opt.Restore = func(context.Context, litestream.ReplicaClient, string, uint64) error {
+ time.Sleep(time.Hour)
+ return nil
+ }
+ }
+ dbreads.RunWorkerWith(opt)
+}
+
+// dbreadsWitness is T3 from inside the unit: everything a member's SQL could
+// reach for if SQLite let it, tried directly. It reports each attempt's
+// error ("" for success) as one JSON object on the connection.
+func dbreadsWitness() {
+ conn, err := dbreads.StdinConn(os.Stdin, dbreads.CarlosUID)
+ if err != nil {
+ os.Exit(2)
+ }
+ defer conn.Close()
+ report := map[string]string{}
+ try := func(name string, f func() error) {
+ if err := f(); err != nil {
+ report[name] = err.Error()
+ } else {
+ report[name] = ""
+ }
+ }
+ open := func(p string) func() error {
+ return func() error {
+ f, err := os.Open(p)
+ if err == nil {
+ f.Close()
+ }
+ return err
+ }
+ }
+ dial := func(addr string) func() error {
+ return func() error {
+ c, err := net.DialTimeout("tcp", addr, 2*time.Second)
+ if err == nil {
+ c.Close()
+ }
+ return err
+ }
+ }
+ write := func(p string) func() error { return func() error { return os.WriteFile(p, []byte("x"), 0o600) } }
+ try("open /data/registry.db", open("/data/registry.db"))
+ try("open /etc/carlos/host.env", open("/etc/carlos/host.env"))
+ try("open another instance", open("/data/instances/other.example.com/other.db"))
+ try("connect the metadata service", dial("169.254.169.254:80"))
+ try("connect a TCP address", dial("1.1.1.1:443"))
+ try("write /opt/carlos", write("/opt/carlos/witness"))
+ try("write /var/lib", write("/var/lib/witness"))
+ try("write /etc", write("/etc/witness"))
+ try("write /var/tmp", write("/var/tmp/witness")) // the positive control
+ // SQLite with the attach limit removed: the mutation. Only the sandbox
+ // stands between these statements and the box.
+ sqlTry := func(name, q string) {
+ try(name, func() error {
+ db, err := sql.Open("sqlite", "file:/var/tmp/witness.db")
+ if err != nil {
+ return err
+ }
+ defer db.Close()
+ _, err = db.Exec(q)
+ return err
+ })
+ }
+ sqlTry("attach /data/registry.db", "ATTACH DATABASE '/data/registry.db' AS r; SELECT count(*) FROM r.sqlite_master")
+ sqlTry("attach /etc/carlos/host.env", "ATTACH DATABASE '/etc/carlos/host.env' AS e; SELECT count(*) FROM e.sqlite_master")
+ sqlTry("vacuum into /opt/carlos", "VACUUM INTO '/opt/carlos/copy.db'")
+ sqlTry("load_extension", "SELECT load_extension('/lib/x86_64-linux-gnu/libc.so.6')")
+ json.NewEncoder(conn).Encode(report)
+}
+```
+
+Import `_ "modernc.org/sqlite"` in this file if no other file of the host test binary registers the driver (check with `grep -rln 'modernc.org/sqlite' internal/host/*.go`; the registry package registers it for production code, so it is likely already linked).
+
+In `TestMain` (`internal/host/plan14_generation_registry_root_linux_test.go:18`), add at the very top:
+
+```go
+ // The installed carlos-dbread@ unit runs this binary as
+ // /opt/carlos/carlos system db-worker (dbreads_rootrunner_linux_test.go).
+ if len(os.Args) == 3 && os.Args[1] == "system" && os.Args[2] == "db-worker" {
+ dbreadsTestWorker()
+ return
+ }
+ // The agent half of the db-read root-runner tests, run as carlos.
+ if os.Getenv("DBREAD_RUNNER_PAYLOAD") != "" {
+ flag.Set("test.run", "^TestRootDBReadPayload$")
+ flag.Set("test.v", "false")
+ flag.Set("test.timeout", "5m")
+ os.Exit(m.Run())
+ }
+```
+
+- [ ] **Step 2: Write the root-runner tests**
+
+Create `internal/host/dbreads_rootrunner_linux_test.go`:
+
+```go
+//go:build linux && rootrunner
+
+package host
+
+import (
+ "bytes"
+ "context"
+ "crypto/ecdh"
+ "crypto/ed25519"
+ "crypto/rand"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "os/user"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "syscall"
+ "testing"
+ "time"
+
+ lsfile "github.com/benbjohnson/litestream/file"
+
+ "github.com/carlosframework/platform/internal/dbdoor"
+ "github.com/carlosframework/platform/internal/dbdoor/dbdoortest"
+ "github.com/carlosframework/platform/internal/dbreads"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+const dbreadRunnerHost = "shop.example.com"
+
+// dbreadRunner is the production wiring on a throwaway root runner: the real
+// units installed by the real ensureSystemUnits into /etc, as the agent's
+// adopt pass installs them (agent.go), this test binary at /opt/carlos/carlos
+// where the unit's ExecStart finds it, and the agent half run as carlos in a
+// child process, because the worker serves only the carlos user's
+// connections. /var/tmp is a tmpfs of tmpSize: the runner's small root
+// volume would refuse every read's 5 GiB floor, and tmpfs only spends memory
+// on what is written.
+type dbreadRunner struct {
+ t *testing.T
+ dir string // owned by carlos: the store and the replica
+ uid, gid int
+ rootPub ed25519.PublicKey
+ rootPriv ed25519.PrivateKey
+}
+
+func runSystemctl(args ...string) (string, error) {
+ out, err := exec.Command("systemctl", args...).CombinedOutput()
+ return string(out), err
+}
+
+func newDBReadRunner(t *testing.T, tmpSize string) *dbreadRunner {
+ t.Helper()
+ if os.Geteuid() != 0 {
+ t.Fatal("run on the root runner, as root")
+ }
+ r := &dbreadRunner{t: t}
+ if out, err := exec.Command("mount", "-t", "tmpfs", "-o", "size="+tmpSize+",mode=1777", "tmpfs", "/var/tmp").CombinedOutput(); err != nil {
+ t.Fatalf("mount /var/tmp: %v: %s", err, out)
+ }
+ t.Cleanup(func() { exec.Command("umount", "/var/tmp").Run() })
+ u, err := user.Lookup("carlos")
+ if err != nil {
+ if out, err := exec.Command("useradd", "--system", "--user-group", "--no-create-home", "--shell", "/usr/sbin/nologin", "carlos").CombinedOutput(); err != nil {
+ t.Fatalf("useradd: %v: %s", err, out)
+ }
+ t.Cleanup(func() { exec.Command("userdel", "carlos").Run() })
+ if u, err = user.Lookup("carlos"); err != nil {
+ t.Fatal(err)
+ }
+ }
+ r.uid, _ = strconv.Atoi(u.Uid)
+ r.gid, _ = strconv.Atoi(u.Gid)
+ if _, err := os.Stat("/opt/carlos/carlos"); err == nil {
+ t.Fatal("/opt/carlos/carlos exists: this test runs only on a fresh runner")
+ }
+ self, err := os.ReadFile(os.Args[0])
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, d := range []string{"/opt/carlos", "/etc/carlos", "/data/instances/other.example.com", "/run/carlos", dbreadTestMarkers} {
+ if err := os.MkdirAll(d, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ }
+ // Things the witness must not reach, present on the host.
+ os.WriteFile("/data/registry.db", []byte("registry"), 0o644)
+ os.WriteFile("/etc/carlos/host.env", []byte("CARLOS_REGION=uk\n"), 0o640)
+ os.WriteFile("/data/instances/other.example.com/other.db", []byte("other"), 0o644)
+ if err := os.WriteFile("/opt/carlos/carlos", self, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ for _, p := range []string{"/opt/carlos/carlos", "/data/registry.db", "/etc/carlos/host.env"} {
+ os.Remove(p)
+ }
+ os.RemoveAll(dbreadTestMarkers)
+ })
+ ensureSystemUnits("/etc", filepath.Join(t.TempDir(), "lock"), runSystemctl, systemctlProbe, t.Logf)
+ t.Cleanup(func() {
+ runSystemctl("stop", "carlos-dbread.socket", "carlos-instance-control.socket", "carlos-system-update.path")
+ runSystemctl("disable", "carlos-dbread.socket", "carlos-instance-control.socket", "carlos-system-update.path", "carlos-system-recover.service")
+ for _, u := range systemUnitFiles {
+ os.Remove(filepath.Join("/etc/systemd/system", u))
+ }
+ runSystemctl("daemon-reload")
+ })
+ if out, _ := systemctlProbe("is-active", dbReadSocketUnit); strings.TrimSpace(out) != "active" {
+ t.Fatalf("carlos-dbread.socket is %q after ensureSystemUnits", out)
+ }
+ r.dir, err = os.MkdirTemp("/var/lib", "carlos-dbread-runner-")
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { os.RemoveAll(r.dir) })
+ r.rootPub, r.rootPriv, _ = ed25519.GenerateKey(rand.Reader)
+ replica := filepath.Join(r.dir, "replica", "instances", dbreadRunnerHost)
+ dbdoortest.Replicate(t, replica, 3)
+ yes := true
+ rec := provision.Record{Host: dbreadRunnerHost, App: "shop", Account: "bab", Region: "uk", Hibernate: &yes,
+ CreatedAt: time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC)}
+ body, _ := json.Marshal(rec)
+ if _, err := r.store().Put(context.Background(), provision.RecordKey("bab", "shop", dbreadRunnerHost), body, ""); err != nil {
+ t.Fatal(err)
+ }
+ r.chown()
+ return r
+}
+
+func (r *dbreadRunner) store() release.Store { return release.NewDirStore(filepath.Join(r.dir, "store")) }
+
+func (r *dbreadRunner) chown() {
+ filepath.Walk(r.dir, func(p string, _ os.FileInfo, _ error) error { return os.Lchown(p, r.uid, r.gid) })
+}
+
+func (r *dbreadRunner) marker(name string, on bool) {
+ p := filepath.Join(dbreadTestMarkers, name)
+ if on {
+ os.WriteFile(p, nil, 0o644)
+ } else {
+ os.Remove(p)
+ }
+}
+
+// submit is the console's two creates; it returns the reply key.
+func (r *dbreadRunner) submit(sqlText string) (string, *ecdh.PrivateKey) {
+ r.t.Helper()
+ reply, _ := dbreads.NewReplyKey()
+ id, _ := dbreads.MintID()
+ inc, _ := hostclaim.Incarnation("bab", "shop", dbreadRunnerHost, time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC))
+ req := dbreads.Request{ID: id, Account: "bab", App: "shop", Host: dbreadRunnerHost, Incarnation: inc, Region: "uk",
+ Op: dbreads.OpQuery, SQL: sqlText, ReplyKey: dbreads.EncodeReplyKey(reply.PublicKey()),
+ By: "member@example.com", IssuedAt: time.Now().UTC().Format(time.RFC3339)}
+ wire, err := dbreads.Sign(release.RootSigner(r.rootPriv), req)
+ if err != nil {
+ r.t.Fatal(err)
+ }
+ s := r.store()
+ s.Put(context.Background(), dbreads.RequestKey(id), wire, "")
+ issued, _ := req.Issued()
+ s.Put(context.Background(), dbreads.QueueKey("uk", issued, id), nil, "")
+ r.chown()
+ return id, reply
+}
+
+// payload runs the agent half as carlos and returns its JSON report. id, when
+// set, is the read the scenario waits on or reports about.
+func (r *dbreadRunner) payload(scenario string, wait bool, id string) (map[string]any, *exec.Cmd) {
+ r.t.Helper()
+ cmd := exec.Command(os.Args[0])
+ cmd.Env = append(os.Environ(), "DBREAD_RUNNER_PAYLOAD="+scenario, "DBREAD_RUNNER_DIR="+r.dir,
+ "DBREAD_RUNNER_ID="+id, "DBREAD_RUNNER_PUB="+base64.StdEncoding.EncodeToString(r.rootPub))
+ cmd.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: uint32(r.uid), Gid: uint32(r.gid)}}
+ var out bytes.Buffer
+ cmd.Stdout, cmd.Stderr = &out, os.Stderr
+ if !wait {
+ if err := cmd.Start(); err != nil {
+ r.t.Fatal(err)
+ }
+ return nil, cmd
+ }
+ if err := cmd.Run(); err != nil {
+ r.t.Fatalf("payload %s: %v: %s", scenario, err, out.String())
+ }
+ var rep map[string]any
+ if err := json.Unmarshal(lastLine(out.Bytes()), &rep); err != nil {
+ r.t.Fatalf("payload %s said %q", scenario, out.String())
+ }
+ return rep, cmd
+}
+
+func lastLine(b []byte) []byte {
+ lines := bytes.Split(bytes.TrimSpace(b), []byte("\n"))
+ return lines[len(lines)-1]
+}
+
+func (r *dbreadRunner) answer(id string, reply *ecdh.PrivateKey) ([]byte, dbreads.Result) {
+ r.t.Helper()
+ s := r.store()
+ raw, _, err := s.Get(context.Background(), dbreads.ResultKey(id))
+ if err != nil {
+ r.t.Fatalf("no result: %v", err)
+ }
+ var res dbreads.Result
+ json.Unmarshal(raw, &res)
+ var out bytes.Buffer
+ for seq := int64(1); seq <= res.LastSeq; seq++ {
+ sealed, _, _ := s.Get(context.Background(), dbreads.ChunkKey(id, seq))
+ plain, err := dbreads.OpenChunk(reply, id, seq, dbreads.StreamRows, seq == res.LastSeq, sealed)
+ if err != nil {
+ r.t.Fatal(err)
+ }
+ out.Write(plain)
+ }
+ return out.Bytes(), res
+}
+
+// privateTmpGone waits up to limit for every worker's private /var/tmp to
+// be removed.
+func privateTmpGone(t *testing.T, limit time.Duration) {
+ t.Helper()
+ deadline := time.Now().Add(limit)
+ for {
+ left, _ := filepath.Glob("/var/tmp/systemd-private-*-carlos-dbread@*")
+ if len(left) == 0 {
+ return
+ }
+ if time.Now().After(deadline) {
+ t.Fatalf("a worker's private tmp outlived it: %v", left)
+ }
+ time.Sleep(200 * time.Millisecond)
+ }
+}
+
+func unitsActive(t *testing.T) int {
+ n, err := dbReadUnits(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ return n
+}
+
+// T2: a forwarded query through the real socket and the real service,
+// installed by the real ensureSystemUnits; the socket is the worker's stdin
+// and stdout (codex 2).
+func TestRootDBReadServesThroughTheInstalledUnits(t *testing.T) {
+ r := newDBReadRunner(t, "40g")
+ id, reply := r.submit("SELECT count(*) AS n FROM t")
+ rep, _ := r.payload("serve", true, id)
+ if rep["capable"] != true {
+ t.Fatalf("the installed worker failed its probe: %v", rep)
+ }
+ rows, res := r.answer(id, reply)
+ if res.State != dbreads.StateDone || string(rows) != "n\n30\n" {
+ t.Fatalf("result %+v rows %q", res, rows)
+ }
+ privateTmpGone(t, 10*time.Second)
+}
+
+// T3: from inside the worker, nothing outside its scratch is reachable; the
+// private tmp is gone after a normal end, a cancel and kill -9 of the agent.
+func TestRootDBReadWorkerSandbox(t *testing.T) {
+ r := newDBReadRunner(t, "40g")
+ r.marker("witness", true)
+ rep, _ := r.payload("witness", true, "")
+ r.marker("witness", false)
+ report, _ := rep["report"].(map[string]any)
+ if len(report) == 0 {
+ t.Fatalf("no witness report: %v", rep)
+ }
+ for name, v := range report {
+ failed := v.(string) != ""
+ if name == "write /var/tmp" {
+ if failed {
+ t.Errorf("the positive control failed: %s", v)
+ }
+ continue
+ }
+ if !failed {
+ t.Errorf("%s succeeded inside the worker", name)
+ }
+ }
+ // A cancel, then kill -9 of the agent, mid-read.
+ r.marker("hang-restore", true)
+ defer r.marker("hang-restore", false)
+ id, _ := r.submit("SELECT 1")
+ _, cmd := r.payload("serve-and-hang", false, "")
+ waitFor(t, 30*time.Second, func() bool { return unitsActive(t) >= 1 })
+ r.store().Put(context.Background(), dbreads.CancelKey(id), nil, "")
+ waitFor(t, 20*time.Second, func() bool { return unitsActive(t) == 0 })
+ privateTmpGone(t, 10*time.Second)
+ cmd.Process.Kill()
+ cmd.Wait()
+ r.submit("SELECT 1")
+ _, cmd = r.payload("serve-and-hang", false, "")
+ waitFor(t, 30*time.Second, func() bool { return unitsActive(t) >= 1 })
+ cmd.Process.Signal(syscall.SIGKILL)
+ cmd.Wait()
+ waitFor(t, 20*time.Second, func() bool { return unitsActive(t) == 0 })
+ privateTmpGone(t, 10*time.Second)
+}
+
+func waitFor(t *testing.T, limit time.Duration, ok func() bool) {
+ t.Helper()
+ deadline := time.Now().Add(limit)
+ for !ok() {
+ if time.Now().After(deadline) {
+ t.Fatal("timed out")
+ }
+ time.Sleep(200 * time.Millisecond)
+ }
+}
+
+// T12 with real units. The disk floor: /var/tmp on a 12 GiB tmpfs fits one
+// read's floor reservation (5 GiB and 10%) and not two, so the second read
+// waits unpicked until the first worker unit and its scratch are gone. A
+// fixture database small enough for the runner cannot make the plan's
+// growth bind, so the no-room result itself is pinned by
+// TestReadTooLargeAndNoRoom. Then an agent killed mid-read: the restarted
+// agent takes no read and no probe while the old unit is active, its worker
+// ends within 5 s of losing the connection though its restore ignores the
+// context, and the restarted agent admits only once the unit is gone.
+func TestRootDBReadAdmissionWithRealUnits(t *testing.T) {
+ // 12 GiB: one read's 5 GiB floor leaves 7 GiB (at least 5 GiB and 10%),
+ // a second leaves 2 GiB, which is refused.
+ r := newDBReadRunner(t, "12g")
+ r.marker("hang-restore", true)
+ r.submit("SELECT 1")
+ second, reply := r.submit("SELECT count(*) AS n FROM t")
+ _, hung := r.payload("serve-and-hang", false, "")
+ waitFor(t, 30*time.Second, func() bool { return unitsActive(t) == 1 })
+ time.Sleep(3 * time.Second)
+ if _, _, err := r.store().Get(context.Background(), dbreads.PickedKey(second)); err == nil {
+ t.Fatal("a second read was picked with room for one")
+ }
+ hung.Process.Signal(syscall.SIGKILL)
+ hung.Wait()
+ killedAt := time.Now()
+ rep, _ := r.payload("restarted", true, second)
+ if rep["units_at_start"].(float64) > 0 && (rep["capable"] == true || rep["picked"] == true) {
+ t.Fatalf("a restarted agent admitted beside the old unit: %v", rep)
+ }
+ waitFor(t, 20*time.Second, func() bool { return unitsActive(t) == 0 })
+ if gone := time.Since(killedAt); gone > 15*time.Second {
+ t.Fatalf("the old worker took %s to stop; its 5 s exit did not hold", gone)
+ }
+ privateTmpGone(t, 10*time.Second)
+ r.marker("hang-restore", false)
+ rep, _ = r.payload("serve", true, second)
+ if rep["capable"] != true {
+ t.Fatalf("the agent did not admit once the unit was gone: %v", rep)
+ }
+ if rows, res := r.answer(second, reply); res.State != dbreads.StateDone || string(rows) != "n\n30\n" {
+ t.Fatalf("the held-back read: %+v %q", res, rows)
+ }
+}
+
+// T13: a socket whose service is missing, masked, failing to start or
+// speaking another protocol version withdraws db-read-1 within one probe and
+// edges pass; a box running two reads keeps it (codex 9).
+func TestRootDBReadReadiness(t *testing.T) {
+ r := newDBReadRunner(t, "40g")
+ capable := func() bool {
+ rep, _ := r.payload("probe", true, "")
+ return rep["capable"] == true && rep["published"] == true
+ }
+ if !capable() {
+ t.Fatal("baseline: not capable")
+ }
+ svc := "/run/systemd/system/carlos-dbread@.service"
+ body, _ := os.ReadFile(svc)
+ os.Remove(svc)
+ runSystemctl("daemon-reload")
+ if capable() {
+ t.Error("capable with the service missing")
+ }
+ os.WriteFile(svc, body, 0o644)
+ runSystemctl("daemon-reload")
+ runSystemctl("mask", "--runtime", "carlos-dbread@.service")
+ if capable() {
+ t.Error("capable with the service masked")
+ }
+ runSystemctl("unmask", "--runtime", "carlos-dbread@.service")
+ for _, m := range []string{"exit-1", "version-2"} {
+ r.marker(m, true)
+ if capable() {
+ t.Errorf("capable with %s", m)
+ }
+ r.marker(m, false)
+ }
+ if !capable() {
+ t.Fatal("not capable again once restored")
+ }
+ r.marker("hang-restore", true)
+ defer r.marker("hang-restore", false)
+ r.submit("SELECT 1")
+ r.submit("SELECT 1")
+ rep, _ := r.payload("busy-probe", true, "")
+ if rep["capable"] != true {
+ t.Fatalf("a box running two reads lost the capability: %v", rep)
+ }
+}
+
+// TestRootDBReadPayload is the agent half, run as carlos by payload();
+// never run it by name.
+func TestRootDBReadPayload(t *testing.T) {
+ scenario := os.Getenv("DBREAD_RUNNER_PAYLOAD")
+ if scenario == "" {
+ t.Skip("run by the db-read root-runner tests as their carlos child")
+ }
+ dir := os.Getenv("DBREAD_RUNNER_DIR")
+ pubRaw, _ := base64.StdEncoding.DecodeString(os.Getenv("DBREAD_RUNNER_PUB"))
+ store := release.NewDirStore(filepath.Join(dir, "store"))
+ a := dbreads.NewAgent(dbreads.AgentConfig{
+ Store: store, Region: "uk", Box: "runner", Boot: newBootID(), Pub: ed25519.PublicKey(pubRaw),
+ HibernatePrefix: "instances",
+ OpenReplica: func(_ context.Context, prefix string) (dbdoor.ReplicaReader, error) {
+ return lsfile.NewReplicaClient(filepath.Join(dir, "replica", prefix)), nil
+ },
+ DialWorker: dialDBReadWorker, Units: dbReadUnits, Free: varTmpFree, Logf: t.Logf,
+ })
+ ctx := context.Background()
+ report := map[string]any{}
+ say := func() {
+ b, _ := json.Marshal(report)
+ fmt.Println(string(b))
+ }
+ switch scenario {
+ case "probe":
+ a.Probe(ctx)
+ report["capable"] = a.Capable()
+ report["published"] = len(withDBReadCapability(nil, a.Capable())) == 1
+ case "serve":
+ a.Probe(ctx)
+ report["capable"] = a.Capable()
+ id := os.Getenv("DBREAD_RUNNER_ID")
+ deadline := time.Now().Add(2 * time.Minute)
+ for time.Now().Before(deadline) {
+ a.Pass(ctx)
+ a.Wait()
+ if _, _, err := store.Get(ctx, dbreads.ResultKey(id)); err == nil {
+ break
+ }
+ time.Sleep(time.Second)
+ }
+ case "serve-and-hang":
+ a.Probe(ctx)
+ a.Pass(ctx)
+ select {} // the parent kills this process
+ case "restarted":
+ units, _ := dbReadUnits(ctx)
+ report["units_at_start"] = units
+ a.Probe(ctx)
+ a.Pass(ctx)
+ report["capable"] = a.Capable()
+ _, _, err := store.Get(ctx, dbreads.PickedKey(os.Getenv("DBREAD_RUNNER_ID")))
+ report["picked"] = err == nil
+ case "busy-probe":
+ a.Probe(ctx)
+ a.Pass(ctx)
+ deadline := time.Now().Add(30 * time.Second)
+ for n, _ := dbReadUnits(ctx); n < 2 && time.Now().Before(deadline); n, _ = dbReadUnits(ctx) {
+ time.Sleep(200 * time.Millisecond)
+ }
+ a.Probe(ctx)
+ report["capable"] = a.Capable()
+ case "witness":
+ c, err := dialDBReadWorker(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var w map[string]string
+ json.NewDecoder(io.LimitReader(c, 1<<20)).Decode(&w)
+ c.Close()
+ report["report"] = w
+ }
+ say()
+}
+```
+
+- [ ] **Step 3: Cross-compile the root-runner binary locally**
+
+Run (sandbox off): `GOOS=linux GOARCH=arm64 go test -c -tags rootrunner -o "$TMPDIR/dbreads-rootrunner.test" ./internal/host && rm "$TMPDIR/dbreads-rootrunner.test"`
+Expected: it builds. Do not run it on DOCS-T1.
+
+- [ ] **Step 4: Run them on the root runner**
+
+These need the sandbox off (assumed-role credentials). Before the run, check for leftover instances tagged `purpose=plan14-root-runner`. Then, one test per run, each with a bounded timeout:
+
+```bash
+~/.cache/tmp/claude-1001/own/after236/root-runner.py --worktree "$PWD" --pkg ./internal/host --run '^TestRootDBReadServesThroughTheInstalledUnits$'
+~/.cache/tmp/claude-1001/own/after236/root-runner.py --worktree "$PWD" --pkg ./internal/host --run '^TestRootDBReadWorkerSandbox$'
+~/.cache/tmp/claude-1001/own/after236/root-runner.py --worktree "$PWD" --pkg ./internal/host --run '^TestRootDBReadAdmissionWithRealUnits$'
+~/.cache/tmp/claude-1001/own/after236/root-runner.py --worktree "$PWD" --pkg ./internal/host --run '^TestRootDBReadReadiness$'
+```
+
+Expected: four PASS lines, each with its run id. Record the run ids in the branch description. If the runner script is unavailable to the executing session, list the four tests under "Root-runner acceptance: pending" in the report, with the commit they apply to, and stop: the plan is not accepted until they pass.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add internal/host/dbreads_worker_dispatch_linux_test.go internal/host/dbreads_rootrunner_linux_test.go internal/host/plan14_generation_registry_root_linux_test.go
+git commit -m "host: T2, T3, T12 and T13 on the root runner, through the installed units"
+```
+
+---
+
+### Task 16: DBF-31 in the CLI page
+
+Waits for Task 1 Step 5 (DBF-31 approved).
+
+**Files:**
+- Modify: `docs/site/cli.md` (the `carlos system` section, after "Host lifecycle guard")
+- Modify: `approved_db_forward_copy_test.go` (`dbForwardCopyBuilt`)
+
+**Interfaces:**
+- Consumes: the approved DBF-31 text from `docs/superpowers/plans/2026-10-22-db-region-forward-copy.md`.
+
+- [ ] **Step 1: Add the id to the built list**
+
+In `approved_db_forward_copy_test.go`: `var dbForwardCopyBuilt = []string{"DBF-31"}`.
+
+- [ ] **Step 2: Run the gate to verify it fails**
+
+Run: `go test . -run 'TestDBForwardBuiltCopy' -count=1`
+Expected: FAIL in `TestDBForwardBuiltCopyIsApplied`: "docs/site/cli.md does not contain the approved text".
+
+- [ ] **Step 3: Write the docs**
+
+In `docs/site/cli.md`, after the "Host lifecycle guard" subsection of `### carlos system`, add (the paragraph is the approved row's text, on one line):
+
+```markdown
+#### carlos system db-worker
+
+carlos system db-worker runs on platform boxes only. systemd starts it for each forwarded database query. you never run it yourself.
+```
+
+If Paul changed the text in review, write his text, not this.
+
+- [ ] **Step 4: Run the gate and the docs check**
+
+Run: `go test . -run 'TestDBForward' -count=1 && go test ./internal/docsite/ -count=1`
+Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add docs/site/cli.md approved_db_forward_copy_test.go
+git commit -m "docs: carlos system db-worker (DBF-31)"
+```
+
+---
+
+### Task 17: Census, test weights, merge main, CI, and the dev LIST-rate check
+
+**Files:**
+- Create: `internal/provision/testdata/plan14-root-reads-db-region-forward-1.tsv`
+- Modify: `internal/provision/plan14_root_reads_test.go` (load the overlay last; the seal)
+- Create: `internal/provision/db_region_forward_census_test.go` (9B8 entries)
+- Modify: `internal/provision/plan9_b8_census_test.go:369` (the inventory function `TestPlan9B8RecordWriteCensus` calls)
+- Create: `docs/superpowers/notes/census-proposal-db-region-forward-1.md`
+- Modify: `.amadan/test-weights.tsv` (only if a new console or root test takes a second or more)
+
+**Interfaces:**
+- Consumes: `rootReadProduction`, `censusProduction`, `rootReadLegacyFingerprint`, `rootReadLegacySeal`, `loadRootReadInventory`, `checkRootReadLegacy` (`internal/provision` tests).
+
+- [ ] **Step 1: Merge main**
+
+```bash
+git fetch origin && git merge origin/main
+go build ./...
+```
+Resolve conflicts, then re-read `TestPlan14RootReadCensus` to find the overlay main now loads last and the seal it sets.
+
+- [ ] **Step 2: Dump the new and changed census units**
+
+Create a temporary file `internal/provision/zz_dbforward_dump_test.go` (never committed):
+
+```go
+package provision
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestZZDBForwardCensusDump(t *testing.T) {
+ root := censusRoot(t)
+ got, err := rootReadProduction(root)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, e := range got.Entries {
+ for _, p := range []string{"db.go", "internal/dbquery/", "internal/dbreads/", "internal/dbdoor/", "internal/host/dbreads", "internal/host/units/carlos-dbread", "internal/host/systemunits.go", "internal/host/edgesfile.go", "internal/host/agent.go", "internal/console/apidb.go", "system.go"} {
+ if strings.HasPrefix(e.File, p) {
+ t.Logf("READ\t%s\t%s\t%s\t%s", e.File, e.Unit, e.Kinds, e.Digest)
+ }
+ }
+ }
+ for dir, hash := range got.Coverage {
+ t.Logf("@coverage\t%s\t%s", dir, hash)
+ }
+ writes, err := censusProduction(root)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for id, w := range writes {
+ if strings.HasPrefix(id, "internal/dbreads/") {
+ t.Logf("WRITE\t%s\t%s\t%s\t%s", w.File, w.Unit, w.Kinds, w.Digest)
+ }
+ }
+}
+```
+
+Run (sandbox off): `go test ./internal/provision/ -run 'TestZZDBForwardCensusDump|TestPlan14RootReadCensus$|TestPlan9B8RecordWriteCensus$' -count=1 -v 2>&1 | tee "$TMPDIR/dbf-census.log"`. The two census tests fail and name every unclassified, changed or stale unit and envelope; the dump gives each unit's kinds and digest.
+
+- [ ] **Step 3: Write the overlay**
+
+Create `internal/provision/testdata/plan14-root-reads-db-region-forward-1.tsv`. Header lines (`#`) say what the branch is, that it is loaded LAST after the overlay main loads last, that every row is `rev3=PROPOSED-db-region-forward-1` awaiting the census owner on #280, and that the seal moves. Then one row per unit the census reported, in the formats below (tab-separated; `<kinds>` and `<digest>` are the dump's):
+
+- **The retired CLI runner** (its reads moved to `dbquery.RunContext`):
+ ```
+ @remove db.go runQueryContext
+ @removed db.go runQueryContext H3 census-ruling-db-region-forward-1 (PROPOSED, awaiting the census owner on #280): still declared, and reads nothing now: it calls dbquery.RunContext, which holds its former open and query unchanged but for the immutable=1 option, an accepted-legacy row in this overlay. It runs in the carlos CLI as the person.
+ ```
+- **A new or changed legacy unit** is two lines, the frozen declaration and the entry:
+ ```
+ @legacy <fingerprint> <file> <unit> legacy accepted-legacy <basis> <kinds> <digest>
+ <file> <unit> legacy accepted-legacy <basis> <kinds> <digest>
+ ```
+ with `<basis>` by identity:
+ - `internal/dbquery` `RunContext`: `accepted-legacy;rev3=PROPOSED-db-region-forward-1;basis=root-input (J4, the footing main gave db.go runQueryContext); the carlos CLI on a person's machine, as the person, and the db-read worker (carlos system db-worker, carlos-dbread@.service, a DynamicUser with no network): db.go's runQueryContext body moved unchanged but for the immutable=1 open option the worker passes; opens one restored copy read-only (mode=ro, query_only, temp_store(2), no attached databases) and runs one guarded statement; neither caller is root and neither decides anything for root`
+ - agent units in `internal/dbreads` (`Agent.*`, `chunkWriter.*`, `putCreateOnly`, `sendFile`, `relay`, `readFrame` when it is the agent's): `accepted-legacy;rev3=PROPOSED-db-region-forward-1;basis=route-and-input (J1,J4, as the offline move's agent units); the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): new, <one clause: what this unit reads, for example "reads the region's db-reads/ queue listing and one request object, verified against the root key">; makes no root decision`
+ - worker units in `internal/dbreads` (`ServeWorker`, `receiveFile`, `SelfView`, `StdinConn`, `CarlosUID`, `RunWorker*`): `accepted-legacy;rev3=PROPOSED-db-region-forward-1;basis=root-input (J4); the db-read worker (carlos system db-worker, carlos-dbread@.service, a DynamicUser with no network, PrivateTmp, /data and /etc/carlos hidden): new, <one clause>; reads only the files the agent sent and its own scratch`
+ - `internal/dbdoor` `ListLevel`, `PlanFrom`, `finishPlan`, `listing.*`, `RestoreLocal`: `accepted-legacy;rev3=PROPOSED-db-region-forward-1;basis=root-input (J4, as main's dbdoor units); the console's db door, the carlos CLI as the person, the agent and the db-read worker: <one clause>`
+ - `internal/host/dbreads*.go` (`dbReadUnits`, `varTmpFree`, `armDBReads`): `accepted-legacy;rev3=PROPOSED-db-region-forward-1;basis=root-input (J4); the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): new, <for example "one Statfs of /var/tmp for the disk reservation" or "systemctl list-units of carlos-dbread@ units, readable without privilege">`
+ - the two unit files, as `internal/host/units/carlos-instance-cmd@.service` took: `<file> external legacy accepted-legacy accepted-legacy;rev3=PROPOSED-db-region-forward-1;basis=the db-read worker's units (written by root's ensureSystemUnits; carlos-dbread.socket bound by PID 1 and owned by carlos; carlos-dbread@.service runs as a DynamicUser with no network, its own PrivateTmp, /data, /var/lib, /opt/carlos and /run/carlos empty, /etc/carlos inaccessible): new file:external@0 <digest>`
+ - changed existing legacy units (for example `edgesFileLoop`, `ensureSystemUnits`, `runAgent`, `listReplicaLevel`, `dbClassRefusal` if the census lists them): re-frozen at main's own class and basis text with this branch's change appended in one clause, the form console-switcher-filter used.
+- **The probe** (`Agent.Probe` / `Agent.probe`, whichever the census names), one row, not legacy:
+ ```
+ internal/dbreads/agent.go Agent.probe diagnostic J4 rev3=PROPOSED-db-region-forward-1;identity=the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos);basis=the readiness probe: a hello on its own worker connection; decides only whether this box publishes db-read-1, never any root action <kinds> <digest>
+ ```
+- **Envelopes:** one `@coverage <dir> <hash>` line for every directory the census reports changed or new.
+
+Compute each `@legacy` fingerprint with `rootReadLegacyFingerprint` over exactly the row's seven fields: add a `t.Logf` to the dump test that loads the overlay file with `loadRootReadInventory` into a fresh inventory and prints `rootReadLegacyFingerprint(e)` for every legacy entry, then paste the values.
+
+- [ ] **Step 4: Load the overlay last, with the seal**
+
+In `TestPlan14RootReadCensus`, after the last overlay main loads and before the final `errs := append(compareRootReads(got, inv), ...)` line, add:
+
+```go
+ // Census overlay db-region-forward-1 (db region forward plan 1: the box
+ // side and the wire, dark), loaded last, after <main's last overlay>.
+ // PROPOSED-db-region-forward-1, asked of the census owner on #280
+ // (docs/superpowers/notes/census-proposal-db-region-forward-1.md).
+ // db.go runQueryContext is retired by an H3 removal; its body is
+ // internal/dbquery RunContext, accepted legacy at the same J4 footing
+ // with both callers named; the agent's db-reads units are accepted
+ // legacy at J1 and J4, the worker's at J4, the probe is diagnostic, and
+ // the two carlos-dbread units join as external units. Frozen
+ // declarations are added, so the seal moves; <main's last overlay>'s is
+ // checked first.
+ if errs := checkRootReadLegacy(inv, seal); len(errs) != 0 {
+ t.Fatal(errs)
+ }
+ if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-db-region-forward-1.tsv"), &inv, true); err != nil {
+ t.Fatal(err)
+ }
+ seal = "<the new seal>"
+```
+
+To get the new seal, add a temporary `t.Logf("seal %s", rootReadLegacySeal(inv))` right after the `loadRootReadInventory` call, run `go test ./internal/provision/ -run 'TestPlan14RootReadCensus$' -count=1 -v`, paste the value, and delete the `t.Logf`.
+
+- [ ] **Step 5: The 9B8 entries**
+
+Create `internal/provision/db_region_forward_census_test.go`, layered on whichever inventory function `TestPlan9B8RecordWriteCensus` calls on the merged main (at `5f341f33` it is `plans2WriteCensusInventory`):
+
+```go
+package provision
+
+// Record-write census entries for db region forward plan 1, layered on
+// plans2WriteCensusInventory. Reviewed by the branch author; the census owner
+// rules on #280 (PROPOSED-db-region-forward-1). Every new Put is a
+// conditional create under db-reads/ in the deployment bucket (picked, the
+// sealed chunks, result.json), never an instance or sidecar record:
+// nonrecord. There is no Delete site.
+func dbRegionForward1WriteCensusInventory() []censusEntry {
+ return append(append([]censusEntry(nil), plans2WriteCensusInventory()...),
+ censusEntry{"internal/dbreads/agent.go", "Agent.elect", "nonrecord", "<kinds>", "<digest>"},
+ censusEntry{"internal/dbreads/chunks.go", "putCreateOnly", "nonrecord", "<kinds>", "<digest>"},
+ )
+}
+```
+
+with one entry per unit the census lists as an unlisted site under `internal/dbreads/` (the `<kinds>` and `<digest>` are the dump's `WRITE` lines), and point `TestPlan9B8RecordWriteCensus` at `dbRegionForward1WriteCensusInventory()`.
+
+- [ ] **Step 6: Run the census and remove the dump**
+
+```bash
+rm internal/provision/zz_dbforward_dump_test.go
+go test ./internal/provision/ -run 'TestPlan14RootReadCensus$|TestPlan9B8RecordWriteCensus$|TestPlan14' -count=1
+```
+Expected: PASS.
+
+- [ ] **Step 7: Write the census proposal**
+
+Create `docs/superpowers/notes/census-proposal-db-region-forward-1.md` in the form of `census-proposal-console-switcher-filter.md`: status PROPOSED, the branch and its base, the overlay's name and load position, and one table row per unit (unit, change, executing identity, proposed disposition), the H3 removal, the two unit files, the 9B8 entries, the envelopes, and the old and new seals. Post it as a reply on amadan discussion #280 (`amadan discuss reply carlos/platform 280 -body -`), as the branch's agent account.
+
+- [ ] **Step 8: Test weights**
+
+The new top-level tests in `internal/console` and the root package are `TestDBReadsParityWithTheInRegionDoor` and the copy gate. Measure the console one without a full run:
+
+```bash
+go test -count=1 -json -run 'TestDBReadsParityWithTheInRegionDoor' ./internal/console/ > "$TMPDIR/dbf-weights.json"
+grep '"Action":"pass"' "$TMPDIR/dbf-weights.json" | grep '"Test":"TestDBReadsParityWithTheInRegionDoor"'
+rm "$TMPDIR/dbf-weights.json"
+```
+If its `Elapsed` is 1.0 or more, add a row `github.com/carlosframework/platform/internal/console TestDBReadsParityWithTheInRegionDoor <seconds>` to `.amadan/test-weights.tsv` in descending order among the console rows, keeping the header. Do not rewrite the file with `.amadan/test-weights.sh`: that needs a full package run.
+
+- [ ] **Step 8b: Mutation checks (spec §11.4)**
+
+Each mutation below must turn its test red. Make them in a copy of the tree, never in the worktree: `cp -a . "$TMPDIR/dbf1-mut"` once, then for each mutation edit the copy, run the named test there, record red or green, and revert the edit (`git -C "$TMPDIR/dbf1-mut" checkout -- <file>`). Remove the copy by its name when done.
+
+| Mutation (in the copy) | Test that must fail |
+|---|---|
+| `Verify` returns the decoded request without calling `release.VerifyRootEnvelope` | `TestEnvelopeRefusesForgeryAndReplay`, `TestPassRefusesForgedEntriesAndUnsignedRequests` |
+| `check` drops its `rec.Host != req.Host` and incarnation comparisons | `TestCheckIsTheRecordAsTheEnvelopeNamedIt`, `TestRecordChangeAtEveryStageWritesNoChunk` (the prefix-from-the-request mutation is plan 2's, through T1's second tenant) |
+| `PrivateNetwork=yes` removed from `carlos-dbread@.service` | `TestDBReadUnitsCarryTheSandbox`; on the root runner, `TestRootDBReadWorkerSandbox` |
+| check B removed from `attempt` | `TestRecordChangeAtEveryStageWritesNoChunk` |
+| `putCreateOnly` called with a non-empty `ifMatch`, or a `Delete` added after `result.json` | `TestReadNeverDeletesOrReplaces` |
+| `elect` returns `false` without reading `picked` back | `TestElectionReadsBackALostAnswer` |
+| `reconcileLocked` ignores `n > a.conns` | `TestAdmissionRefusesBesideAnOrphan`; on the root runner, `TestRootDBReadAdmissionWithRealUnits` |
+| `MaxConnections` set to 2 | `TestAdmissionTwoReadsThenBusy`, `TestProbeOnTheThirdConnection` |
+| the worker's `time.After(opt.Grace)` exit removed | `TestWorkerExitsWhenTheAgentGoesMidRestore` |
+
+Record the table, with red for every row, in the branch description.
+
+- [ ] **Step 9: Final local checks, push, describe, CI**
+
+```bash
+gofmt -l . ; go vet ./internal/dbreads/... ./internal/dbquery/ ./internal/dbdoor/ ./internal/host/
+go build ./... && GOOS=darwin GOARCH=arm64 go build ./... && GOOS=windows GOARCH=amd64 go build ./...
+git add internal/provision docs/superpowers/notes/census-proposal-db-region-forward-1.md .amadan/test-weights.tsv
+git commit -m "census: db-region-forward-1 overlay (PROPOSED), 9B8 entries"
+git push origin db-region-forward-1-box
+amadan branch describe carlos/platform db-region-forward-1-box -body - < "$TMPDIR/dbf-1-description.md"
+amadan ci status carlos/platform -branch db-region-forward-1-box
+```
+The description says: what plan 1 builds and that it is dark (no console asks until plan 2; boxes publish `db-read-1` only while their probe passes); the spec and plan paths; the root-runner run ids for T2, T3, T12, T13; the census proposal and that the ruling is owed on #280; the copy state (DBF-31 approved and built; the rest pending for plan 2). Expected: CI green on the tip. A red run is read, not re-run.
+
+- [ ] **Step 10: Review, then merge**
+
+Ask for the whole-branch review (Astra, by a committed brief, or Opus per AGENTS.md's delegation rule). After fixes and the census ruling: merge main again (Step 1), rebuild the overlay's digests if main moved anything, push, wait for green, squash to one commit per AGENTS.md, and merge only with `git merge-base --is-ancestor origin/main HEAD && amadan branch merge carlos/platform db-region-forward-1-box -expect <tip> -message "..."`. Read main's CI afterwards.
+
+- [ ] **Step 11: The dev roll and the LIST-rate measurement (spec §5.8, §11.3)**
+
+At the next dev roll, in Paul's order (dev boxes, dev console, check), once Paul has said to roll: after the dev boxes run this build, confirm in each box's edges file that `db-read-1` is published (`carlos` operator view of the edges files, or the console's fleet page), and measure the LIST rate the loop adds: the S3 request metrics or access log for the dev deployment bucket, `ListObjectsV2` requests on prefix `db-reads/queue/`, per box per hour, over at least one hour with no reads. Expect about 2 LISTs every 30 s per capable box plus 2 per serial move. Report the number on the branch's amadan discussion and in the plan 2 branch description before the flagship roll. No flagship roll happens in this plan.
+