spec: object-storage gate reality amendments
The console has no operator identity; requireFreshAuth is a browser passkey ceremony bearer tokens cannot perform. Grant becomes a bearer door behind a CARLOS_STORE_OPERATORS allowlist; destroy becomes a console browser door on the purge precedent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 file changed,
+31
−14
docs/superpowers/specs/2026-08-10-object-storage-primitive-design.md+31 −14
diff --git a/docs/superpowers/specs/2026-08-10-object-storage-primitive-design.md b/docs/superpowers/specs/2026-08-10-object-storage-primitive-design.md| index e18424b1a..c95cd5bd3 100644 |
| --- a/docs/superpowers/specs/2026-08-10-object-storage-primitive-design.md |
| +++ b/docs/superpowers/specs/2026-08-10-object-storage-primitive-design.md |
| @@ -119,13 +119,20 @@ successes, because every walk that calls them must be re-runnable (§5). |
| Ordinary member auth (the `apiAccountApp` token + byte-identical-404 |
| gate). Declaring writes `declared` and provisions nothing. |
| - **Operator grants**: `carlos store grant -app titogo |
| - [-bucket titogo-uploads]` → operator-gated console endpoint, same |
| - gate class as production-mode flag flips: operator identity + |
| - `requireFreshAuth`, ordered after the owner-404 gate per the console |
| - gate-order rule. The grant handler runs the provisioning walk inline |
| - (§5) — grant and provision are one verb, because a recorded desire |
| - with no acted-on effect is the provisioned-but-not-plumbed gap this |
| - issue exists to close. |
| + [-bucket titogo-uploads]` → bearer endpoint gated on a new |
| + deployment-level allowlist, `CARLOS_STORE_OPERATORS` (comma-separated |
| + member addresses, console env). *(Amended 2026-08-10 during |
| + planning: the console has no operator identity today — its heavy |
| + doors are account-owner + browser passkey step-up, and |
| + `requireFreshAuth` is a browser ceremony a bearer request cannot |
| + perform. The allowlist is the first explicit operator-identity |
| + concept, deliberately scoped to store grants; the caller must still |
| + pass the member `apiAccountApp` gate first, so in v1 the granting |
| + operator must also be a member of the account — true on tito, and |
| + recorded as a known limit.)* The grant handler runs the provisioning |
| + walk inline (§5) — grant and provision are one verb, because a |
| + recorded desire with no acted-on effect is the |
| + provisioned-but-not-plumbed gap this issue exists to close. |
| - **Console provisions**: the console is the single control-plane actor |
| holding backend authority (§6). Boxes need zero new authority; |
| delivery rides existing rails end to end. |
| @@ -191,6 +198,10 @@ Console environment only — the inert-until-configured convention: |
| - `CARLOS_STORE_BUCKET_PREFIX` — minted-name prefix, e.g. |
| `carlos-store-tito-`. Required when the backend is set; refusing to |
| default it keeps two deployments in one AWS account from colliding. |
| +- `CARLOS_STORE_OPERATORS` — comma-separated member addresses allowed |
| + through the grant door (§4). Empty list = nobody can grant, which is |
| + the safe default for a deployment that configured a backend but no |
| + operators yet. |
| The console role gains, in pinfra tofu: IAM verbs |
| (`CreateUser/DeleteUser/PutUserPolicy/DeleteUserPolicy/CreateAccessKey/ |
| @@ -273,11 +284,17 @@ The split that makes it honest: auto-purge runs **box-side** |
| `release.PurgeApp`. Teardown failure aborts the purge — a retry |
| re-enters cleanly — rather than sweeping `store.json` and stranding |
| an unrecorded bucket. |
| -- **`carlos store destroy -app X`** — operator door, fresh auth, and |
| - the CLI requires typing the bucket name back — tears down the store |
| - while keeping the app: backend teardown, delete the store env vars |
| - from `config.json`, delete `store.json`, bump serial. This is the |
| - undo for adoption mistakes and the migration verb. |
| +- **Store destroy** — a console browser door on the app settings |
| + page, following the purge precedent exactly (purge "deliberately has |
| + no API at all"): owner gate → CSRF → type the bucket name back → |
| + `requireFreshAuth` LAST. *(Amended 2026-08-10 during planning: this |
| + was drafted as a `carlos store destroy` CLI verb, but fresh auth is |
| + a browser passkey ceremony bearer tokens cannot perform, and |
| + data-destroying doors follow purge's shape, not restart's.)* It |
| + tears down the store while keeping the app: backend teardown, delete |
| + the store env vars from `config.json`, delete `store.json` last, |
| + bump serial. This is the undo for adoption mistakes and the |
| + migration verb. |
| **Ordering rule, stated once**: any path that destroys backend state |
| reads `store.json` first and deletes it last. A missing or unreadable |
| @@ -297,8 +314,8 @@ with a verb-specific message): |
| | `GET …/store` | member | `carlos store status` | |
| | `POST …/store/rotate` | member | `carlos store rotate` | |
| | `POST …/store/rotate/finish` | member | `carlos store rotate -finish` | |
| -| `POST …/store/grant` | operator + fresh auth | `carlos store grant` | |
| -| `DELETE …/store` | operator + fresh auth | `carlos store destroy` | |
| +| `POST …/store/grant` | member + `CARLOS_STORE_OPERATORS` allowlist | `carlos store grant` | |
| +| *(browser)* `POST /a/{sqid}/apps/{app}/store/destroy` | owner + CSRF + typed bucket name + fresh auth | — (console only, purge precedent) | |
| `carlos store status` reports: declared/granted/provisioned state, the |
| delivered env names, key id + age, pending-finish nag, and drift (a |