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>
2 files changed,
+18
−4
docs/superpowers/specs/2026-08-10-object-storage-primitive-design.md+5 −3internal/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) |
| } |