review pass: bucket provenance honesty, spec amendments, operator-comment scope
- store.go/apimode.go/objstore.go: Grant.Bucket is always the resolved bucket (minted or adopted; a crashed grant's teardown witness) — the "adopting bucket" wording claimed provenance the record doesn't carry. Fixed in the CLI status print and both doc comments. - spec §2/§6/§10: amended-during-implementation notes correcting the same bucket-provenance claim, which store verbs actually 503 without a configured backend (declare/status work; provisioning verbs don't), and which write handlers bump the serial (grant/rotate/destroy, not create/rotate-finish). - cmd/console/main.go: CARLOS_STORE_OPERATORS gates the GRANT door only — rotate is the member's verb, destroy is the owner's browser ceremony — matching server.go's Config comment instead of claiming all three.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 files changed,
+30
−15
apimode.go+1 −1cmd/console/main.go+8 −5docs/superpowers/specs/2026-08-10-object-storage-primitive-design.md+16 −5internal/objstore/objstore.go+1 −1store.go+4 −3
diff --git a/apimode.go b/apimode.go| index 55f89e685..26dc0db4c 100644 |
| --- a/apimode.go |
| +++ b/apimode.go |
| @@ -694,7 +694,7 @@ type apiStoreDeclared struct { |
| type apiStoreGrantBlock struct { |
| Backend string `json:"backend"` |
| - Bucket string `json:"bucket,omitempty"` // set only when the grant ADOPTED an existing bucket |
| + Bucket string `json:"bucket,omitempty"` // the resolved bucket (minted or adopted; the record does not distinguish) |
| At time.Time `json:"at"` |
| By string `json:"by"` |
| } |
diff --git a/cmd/console/main.go b/cmd/console/main.go| index b42f710fa..0de48d4a1 100644 |
| --- a/cmd/console/main.go |
| +++ b/cmd/console/main.go |
| @@ -44,7 +44,8 @@ |
| // which case it must be "aws" and CARLOS_STORE_REGION and |
| // CARLOS_STORE_BUCKET_PREFIX are required (CARLOS_STORE_ENDPOINT is optional |
| // — set it for an S3-compatible backend). CARLOS_STORE_OPERATORS |
| -// (comma-separated addresses) is who may grant, rotate and destroy a store; |
| +// (comma-separated addresses) gates the GRANT door only — rotate is the |
| +// member's own verb and destroy is the account owner's browser ceremony; |
| // it is read whether or not a backend is configured. |
| // |
| // grant/revoke/owners need only CONSOLE_DB. |
| @@ -396,10 +397,12 @@ func storeBackend() (objstore.Provisioner, objstore.Deployment) { |
| } |
| // storeOperators parses CARLOS_STORE_OPERATORS (comma-separated addresses) — |
| -// who may grant, rotate and destroy an app's store. Read REGARDLESS of the |
| -// backend, and deliberately so: the list is an authority statement about |
| -// people, it costs nothing to hold, and an operator who sets it before wiring |
| -// the backend should not have to remember to come back for it. |
| +// who may drive the GRANT door for an app's store, and only that one door; |
| +// rotate is the member's own verb and destroy is the account owner's browser |
| +// ceremony. Read REGARDLESS of the backend, and deliberately so: the list is |
| +// an authority statement about people, it costs nothing to hold, and an |
| +// operator who sets it before wiring the backend should not have to remember |
| +// to come back for it. |
| // |
| // Unset is fail-closed rather than fail-open — no operators means no grants — |
| // which is the right direction for a list whose whole purpose is to gate |
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 b43bae50c..05939b0ea 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 |
| @@ -75,7 +75,11 @@ read-modify-write through the same If-Match discipline as |
| - `grant` — the operator's half, written by the grant door (§4). |
| `bucket` set = adopt this existing bucket; empty = mint the name. |
| `backend` is recorded so the record is self-describing even though v1 |
| - only ever writes `"aws"`. |
| + only ever writes `"aws"`. (amended during implementation, 2026-08-10: |
| + the walk records the RESOLVED bucket here always — minted or |
| + adopted — because a crashed grant's teardown needs a bucket witness |
| + to clean up after; empty-vs-set no longer distinguishes mint from |
| + adopt.) |
| - `provisioned` — written only by the console after a successful |
| provisioning walk. It records coordinates and the access-key **id**; |
| the secret key exists nowhere except sealed inside `config.json`. |
| @@ -195,8 +199,13 @@ the same walk, and the walk is ordered so the *record of the secret* |
| Console environment only — the inert-until-configured convention: |
| - `CARLOS_STORE_BACKEND` — `aws` is the only accepted value in v1. |
| - Unset: every store verb answers 503 "object storage is not configured |
| - for this deployment". |
| + Unset: the provisioning verbs (grant, rotate, rotate-finish) answer |
| + 503 "object storage is not configured for this deployment". (amended |
| + during implementation, 2026-08-10: declare and status work without a |
| + configured backend — a member's declaration is exactly what the |
| + operator needs to act on, and the reply's `backend` field is `""` so |
| + the member can tell for themselves; only the provisioning verbs |
| + 503.) |
| - `CARLOS_STORE_REGION` — bucket + credential region. |
| - `CARLOS_STORE_ENDPOINT` — empty for AWS; recorded into |
| `provisioned.endpoint` and delivered (§7) when set. |
| @@ -326,8 +335,10 @@ with a verb-specific message): |
| `carlos store status` reports: declared/granted/provisioned state, the |
| delivered env names, key id + age, pending-finish nag, and drift (a |
| -store var missing from `config.json`). Every write handler audits and |
| -bumps the serial. The console app page gains a read-only **Store** |
| +store var missing from `config.json`). Every write handler audits; |
| +bumps the serial exactly when a box-consumed object changed (grant, |
| +rotate, destroy — not create, not rotate-finish). (amended during |
| +implementation, 2026-08-10.) The console app page gains a read-only **Store** |
| card (backend, bucket, key age, pending-finish nag); management stays |
| CLI-first in v1. |
diff --git a/internal/objstore/objstore.go b/internal/objstore/objstore.go| index 8478248f0..71d913ea8 100644 |
| --- a/internal/objstore/objstore.go |
| +++ b/internal/objstore/objstore.go |
| @@ -31,7 +31,7 @@ type Declared struct { |
| type GrantBlock struct { |
| Backend string `json:"backend"` |
| - Bucket string `json:"bucket,omitempty"` // set = adopt this existing bucket |
| + Bucket string `json:"bucket,omitempty"` // the resolved bucket (minted or adopted; the record does not distinguish) |
| At time.Time `json:"at"` |
| By string `json:"by"` |
| } |
diff --git a/store.go b/store.go| index ae24517f8..3192348bb 100644 |
| --- a/store.go |
| +++ b/store.go |
| @@ -323,9 +323,10 @@ func printStoreStatus(w io.Writer, res apiStoreReply) { |
| if res.Grant != nil { |
| fmt.Fprintf(w, " grant: by %s at %s", res.Grant.By, res.Grant.At.Format(time.RFC3339)) |
| if res.Grant.Bucket != "" { |
| - // Only set when the grant ADOPTED an existing bucket — a minted |
| - // one's name lives in Provisioned.Bucket, printed below. |
| - fmt.Fprintf(w, " (adopting bucket %s)", res.Grant.Bucket) |
| + // The walk always records the resolved bucket here — minted or |
| + // adopted, the record does not distinguish — so this is not a |
| + // claim about provenance, just the bucket the grant resolved to. |
| + fmt.Fprintf(w, " (bucket %s)", res.Grant.Bucket) |
| } |
| fmt.Fprintln(w) |
| } |