carlos / platform Public

objstore: pin the purge guard's provisioned key on its own

The mirror test purged a record carrying BOTH blocks, so the refusal could
come from `grant` and Record.Provisioned's json tag was never actually
pinned -- while objstore.go promises that renaming EITHER key fails loudly.
Drop the grant block (CAS on the current etag) so `provisioned` stands
alone. Mutation-verified: renaming either tag in the release guard now
fails its own subtest.

Spec §9 folded in to match the widened guard.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell f375fcea70c996757daebe5007dda99c87770a7c parent e57d616
2 files changed, +18 −4
  • docs/superpowers/specs/2026-08-10-object-storage-primitive-design.md +5 −3
  • internal/objstore/walk_test.go +13 −1
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 c7f806045..b43bae50c 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
@@ -280,9 +280,11 @@ The split that makes it honest: auto-purge runs **box-side**
(`domains.Reconcile`), and boxes have no backend authority — so:
- **Box-side `PurgeApp` skips** any app whose `store.json` carries a
- `provisioned` block, logging one line per pass naming the console
- door as the fix. The app stays trashed and unreachable; only the
- final sweep waits.
+ `provisioned` **or `grant`** block (widened during implementation: a
+ grant block without provisioned is a crashed grant whose backend
+ estate is real), logging one line per pass naming the console door as
+ the fix. The app stays trashed and unreachable; only the final sweep
+ waits.
- **Console "Delete now"** (`handlePurgeApp`) does the full job inline,
before the prefix sweep: `EmptyBucket` → `DeleteBucket` →
`DeleteUser` (keys + policy first), all idempotent, then the normal
diff --git a/internal/objstore/walk_test.go b/internal/objstore/walk_test.go
index 1bd07b52d..c1e73c6ad 100644
--- a/internal/objstore/walk_test.go
+++ b/internal/objstore/walk_test.go
@@ -1320,10 +1320,22 @@ func TestPurgeGuardSeesOurRecord(t *testing.T) {
if rec.Provisioned == nil {
t.Fatal("fixture produced no provisioned block")
}
+ // Drop the grant block so `provisioned` stands ALONE. A real record
+ // carries both, which would let this subtest pass on the grant key and
+ // leave Provisioned's json tag unpinned — the one thing this test
+ // exists to catch.
+ stored, etag, err := Get(ctx, s, "bab", "titogo")
+ if err != nil || stored == nil {
+ t.Fatalf("re-read: %+v, %v", stored, err)
+ }
+ stored.Grant = nil
+ if _, err := Put(ctx, s, "bab", "titogo", stored, etag); err != nil {
+ t.Fatal(err)
+ }
if err := release.TrashApp(ctx, s, "bab", "titogo", "paul@keymail.dev", backdated); err != nil {
t.Fatal(err)
}
- err := release.PurgeApp(ctx, s, relPub, "bab", "titogo", time.Now())
+ err = release.PurgeApp(ctx, s, relPub, "bab", "titogo", time.Now())
if !errors.Is(err, release.ErrStoreProvisioned) {
t.Fatalf("PurgeApp over a provisioned record = %v, want release.ErrStoreProvisioned", err)
}