# 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:** `4e6ea53`. **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
  you call a stage done. Use
  `GOCACHE=/home/paulca/.claude/tmp/gocache-codex GOFLAGS=-mod=mod GOPROXY=off`.
- **Do not run `git commit`, `git add`, or any other writing git command.**
  Your sandbox holds `.git` read-only on purpose and that is the right
  division of labour: you write the code, the orchestrator makes the commits.
  Leave the tree dirty and finish with a **commit plan** — for each stage, the
  exact file list and a commit message in this repo's voice (read `git log` for
  the register: what changed, and the reasoning that made it that shape).
- **User-facing copy.** This repo's owner gates new user-facing wording behind
  a review he runs himself. The wording the spec already fixes — §6's two
  changed sentences at `store.go:261` and `:348`, and any string the spec
  quotes — is settled; write it as specified. For any string you must invent
  (a new CLI message, a new error), write the clearest thing you can and
  **list every one of them in your final summary** under a heading
  `NEW USER-FACING COPY`, with its file and line. They go to review as a batch
  after the build. Do not block on this, and do not start a review server.
- If the spec is wrong or underdetermined, say so in the commit plan and in
  your final summary rather than inventing a rule silently. A named gap is
  worth more than a guess.

## Resuming from the first run

A first run (2026-09-08) was stopped by sandbox blockers before it could
commit. Its test drafts are **already applied to the working tree**, uncommitted:

- `internal/edge/storedoor_test.go` — the §4 implementation-gate test. It found
  that stage 3 *claimed* this test and it did not exist; the test now exists
  and passes, and `aclRead` already excludes `store/entitlements/`, so no
  policy change is needed.
- `internal/console/apistore_entitlement_test.go` — stage 4 door tests.
- `internal/console/apistore_provision_test.go` — stage 5 provision tests.
- `internal/console/svcroutes_test.go` — four closed routes, count 17 → 21.

**These drafts never ran** — the console fixture could not open a listener in
that run. Treat them as a starting point to be run, corrected and extended,
not as verified work. The first run's own notes flag two gaps in them: stage 5
still needs a concurrent *two successful provisions* regression that mints one
key (the existing queued test revokes before the second request), and stage 6
has no tests yet.

That run's full findings — twelve of them, including that the spec's route
paths (`/a/{sqid}/apps/{app}/store/provision`) do not match the actual CLI
route family (`/api/cli/...`) — are in `/tmp/store-self-serve-resume.md`.
**Read that file.** Where it proposes a resolution, the proposal is sound
unless the spec contradicts it; say in your commit plan which ones you took.
