carlos / platform Public

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>
Paul Campbell 0b086e82880d3b1f306b1f1879e296ba4620fbaf parent 09a14d6
5 files changed, +30 −15
  • apimode.go +1 −1
  • cmd/console/main.go +8 −5
  • docs/superpowers/specs/2026-08-10-object-storage-primitive-design.md +16 −5
  • internal/objstore/objstore.go +1 −1
  • store.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)
}