carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

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.