carlos / platform Public

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>
Paul Campbell c06997f10af7f0afff5c16563269335199f0a77c parent d41b501
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