carlos / platform Public

brief: scope the codex build to stages 4-6 of the self-serve spec

The brief names the two review blockers by name — the mail-shaped
authority gate and the grant-written-but-not-provisioned walk — because
both are shapes an implementer reading only the code would reinvent
wrong, and both cost a review round already.

Committed beside the spec so a quota-interrupted run restarts from the
same instructions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CfUNFHm89j2mj5q7vBHsfH
Paul Campbell pushed by paul@keymail.dev 4e6ea53646e1a0941ea5113acf3b0b0965775217 parent af8d996
1 file changed, +92 −0
  • docs/superpowers/specs/CODEX-BUILD-BRIEF-store-self-serve.md +92 −0
diff --git a/docs/superpowers/specs/CODEX-BUILD-BRIEF-store-self-serve.md b/docs/superpowers/specs/CODEX-BUILD-BRIEF-store-self-serve.md
new file mode 100644
index 000000000..c4cacb3dd
--- /dev/null
+++ b/docs/superpowers/specs/CODEX-BUILD-BRIEF-store-self-serve.md
@@ -0,0 +1,92 @@
+# Build brief: self-serve object storage, stages 4–6
+
+**For:** an independent implementer (GPT-6 via codex) building from a fixed
+commit. **Reviewer:** a second model reads the result afterwards; write for that
+reading.
+
+**Repo:** carlos/platform. **Branch:** `worktree-store-self-serve`.
+**Base commit:** `af8d996`. **Spec:**
+`docs/superpowers/specs/2026-09-02-object-storage-self-serve-design.md` —
+read it whole before writing anything. It is the contract; this brief only
+scopes the slice and names the traps.
+
+## The one-paragraph shape
+
+An account applies for object storage. A deployment operator approves the
+account once. From then on any app in that account gets a store from
+`carlos store create` alone, and never learns the word "grant". The per-app
+operator `grant` verb survives unchanged for unapproved accounts, `--bucket`
+adoption, and repair.
+
+## Already done — do not rebuild
+
+Stages 1–3 of §14 are committed and green:
+
+- `09d18f5` — `Grant`'s final Put retries a lost CAS, as `Rotate`'s does.
+- `8e3482f` — the console serialises store walks per app (the mutex of §9).
+- `0294fa3` — the `Entitlement` record, its accessors, `GrantBlock.Source`
+ and `Grant`'s new `source` parameter across all callers.
+
+So `objstore.Entitlement`, `GetEntitlement`/`PutEntitlement`,
+`GrantSourceOperator` and `GrantSourceAccountApproval` all exist. Read them
+before adding anything adjacent.
+
+## Build these three stages, one commit each
+
+**Stage 4 — console doors: apply, approvals, approve, decline.** No behaviour
+change anywhere else; the doors are dormant until stage 5 reads them.
+
+**Stage 5 — the provision door and the CLI's two-door `create`.** Behaviour
+changes here. Everything before it is inert.
+
+**Stage 6 — CLI verbs, the status line, and the two changed printer sentences**
+(`store.go:261` and `:348`).
+
+## Traps the spec already paid for — honour them exactly
+
+- **§5's authority shape is `apimail.go`'s pause door, NOT `apiAccountApp`'s.**
+ `cliUser` → fleet-operator bit → `CARLOS_STORE_OPERATORS` → bare
+ byte-identical `http.NotFound`; then resolve the account
+ viewer-independently via `AccountBySqid`, as `mailPauseTarget` does. Routing
+ approve/decline through a member gate deadlocks approval — an operator has no
+ reason to be a member of the account they are approving. This was blocker 2
+ of the 2026-09-03 review; do not reintroduce it.
+- **§4's implementation gate comes first.** Before the record is used, prove by
+ test that `store/entitlements/` is unreachable through the edge store door.
+ Read `canonicalKey`/`aclRead` in `internal/edge/storedoor.go` rather than
+ trusting the prefix's shape. The review flagged this suspected, not
+ confirmed.
+- **A grant already written but not yet provisioned must let the walk RUN to
+ completion**, not refuse it. That was blocker 1; §13 names the regression
+ test.
+- **State is derived from the blocks, never stored beside them.** No object =
+ never applied; `applied` alone = pending; `decision` present = decided.
+- **A declined owner may re-apply** — `apply` overwrites `applied` and clears
+ `decision` under CAS. The anti-spam control is the existing `write` budget
+ keyed on the address, not a new rule.
+
+## Out of scope for this run — do not touch
+
+- Stage 7 (console UI, the notification of §15, `StoreProvisioned` and the
+ bucket count) and stage 8 (`docs/site/cli.md`). A later run.
+- Anything under `infra/`. The host role's provisioning IAM and its permissions
+ boundary are real and blocking, but they ride with #368 — see §1's third
+ blocker and §12.
+- New root env vars. The spec adds none, deliberately.
+- `main`, and any other branch or worktree.
+
+## How to work
+
+- Test-first, following §13's split: walks against `MemProvisioner`, doors
+ against the console fixture, printers against literal replies with no stub
+ server. §13 lists the cases; treat that list as the minimum, not the target.
+- `AGENTS.md` governs — read it. In particular: `go build ./...`,
+ `go vet ./...`, `go test ./... -count=1` and `gofmt -l .` all clean before
+ any commit. Sandbox note in its Build & test section:
+ `GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off`.
+- One commit per stage, in order, each one green on its own. Commit messages in
+ this repo's voice: what changed and the reasoning that made it that shape —
+ read `git log` for the register.
+- If the spec is wrong or underdetermined, say so in the commit message and in
+ your final summary rather than inventing a rule silently. A named gap is
+ worth more than a guess.