plan: Task 6/10 rulings from Task 5-6 reviews (grant-block guard, audit nil-safety)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 file changed,
+15
−2
docs/superpowers/plans/2026-08-10-object-storage-primitive.md+15 −2
diff --git a/docs/superpowers/plans/2026-08-10-object-storage-primitive.md b/docs/superpowers/plans/2026-08-10-object-storage-primitive.md| index ea810a912..fdc98cdf8 100644 |
| --- a/docs/superpowers/plans/2026-08-10-object-storage-primitive.md |
| +++ b/docs/superpowers/plans/2026-08-10-object-storage-primitive.md |
| @@ -549,6 +549,7 @@ func Grant(ctx context.Context, s release.Store, p Provisioner, d Deployment, |
| **Files:** |
| - Modify: `internal/release/trash.go` (PurgeApp, after the production-flag check, before the List) |
| +- CONTROLLER RULING (2026-08-10, from Task 5 review): the guard refuses on a `provisioned` block OR a `grant` block — a grant that crashed before recording `Provisioned` has already created backend estate (bucket, user, delivered key), and `grant` is the earliest bucket-visible witness. All guard code and tests in this task check BOTH keys. |
| - Modify: `internal/release/trash_test.go` |
| **Interfaces:** |
| @@ -725,7 +726,10 @@ An UNREADABLE record refuses too — same direction as the production flag. |
| // is the only purge path with backend authority (objstore spec §9; |
| // box-side auto-purge refuses via release.ErrStoreProvisioned). Failure |
| // aborts the purge with the record intact: re-entering resumes. |
| - if rec, _, rerr := objstore.Get(r.Context(), s.cfg.Releases, acct.Sqid, app); rerr == nil && rec != nil && rec.Provisioned != nil { |
| + // Guard on Provisioned OR Grant (controller ruling, Task 5 review): a |
| + // grant block without a provisioned block is a crashed grant whose |
| + // backend estate is real. |
| + if rec, _, rerr := objstore.Get(r.Context(), s.cfg.Releases, acct.Sqid, app); rerr == nil && rec != nil && (rec.Provisioned != nil || rec.Grant != nil) { |
| if s.cfg.StoreProvisioner == nil { |
| fail("“" + app + "” has a provisioned object store, and this console has no storage backend configured (CARLOS_STORE_BACKEND) — it cannot be deleted here until the store is torn down.") |
| return |
| @@ -735,7 +739,16 @@ An UNREADABLE record refuses too — same direction as the production flag. |
| fail("Could not tear down “" + app + "”'s object store — nothing was deleted; try again.") |
| return |
| } |
| - s.audit(acct, app, addr, "store-destroy", "tore down the object store (bucket "+rec.Provisioned.Bucket+") during permanent deletion") |
| + // rec.Provisioned may be nil here (crashed-grant shape) — name the |
| + // bucket from Grant, which the guard condition proves non-nil when |
| + // Provisioned is. |
| + bucket := "" |
| + if rec.Provisioned != nil { |
| + bucket = rec.Provisioned.Bucket |
| + } else if rec.Grant != nil { |
| + bucket = rec.Grant.Bucket |
| + } |
| + s.audit(acct, app, addr, "store-destroy", "tore down the object store (bucket "+bucket+") during permanent deletion") |
| } else if rerr != nil { |
| log.Printf("console: purge store read %s/%s: %v", acct.Sqid, app, rerr) |
| fail("Could not read “" + app + "”'s object-store record — nothing was deleted; try again.") |