carlos / platform Public
| Earlier finding | Disposition at 9a11decc | |---|---| | R1.1 — Signature accepted as production-off | Fixed: no signed expiry object remains. | | R1.2 — Existing data passes creation checks | Fixed at creation: fresh row and empty-prefix checks. A separate account-move bypass remains below. | | R1.3 — Keep/TTL races expiry | Not fixed: the specified conditional update still matches after keep; finding 1. | | R1.4 — Purge misses the current deadline | Declined-with-reason acceptable: §11 explicitly defers the pre-existing box race. The unchanged reconcile.go:228 remains vulnerable. | | R1.5 — Expiry replay after keep | Fixed: no bucket expiry authority remains. | | R1.6 — Restore bypasses cap | Not fixed for failed console restores: finding 4. The direct-mode exception is acceptable. | | R1.7 — Restrictions enforced only in CLI | Fixed for the reported paths: §4 requires server enforcement. | | R1.8 — Browser/headless restore misses renewal | Fixed in the specified shared restore and catch-up behavior. | | R1.9 — Interrupted purge loses short deadline | Fixed: the deadline survives in the row. | | R1.10 — Migration disables expiry | Declined-with-reason acceptable: §4 accurately describes direct migration and accepts its outcome. | | R2.1 — Rename brings permanent data under expiry | Fixed: temporary-app rename is refused. | | R2.2 — Early purge skips email teardown | Fixed for the reported paths: provisioning is refused and early purge checks for email resources. | | R2.3 — Failed restore cancels purge deadline | Fixed: the deadline remains until the row restore succeeds. | | R2.4 — Missing marker mistaken for restore | Fixed for the reported completed-purge case: an empty prefix finalizes deletion. | | R2.5 — Materialization omits deadline | Fixed: §5 explicitly reconstructs the marker-bound deadline. | | R2.6 — Instance provisioning bypasses store prohibition | Fixed: both manifest enablement and implicit provisioning are covered. | | R2.7 — Old console silently creates permanent app | Fixed in §3: distinct endpoint and response verification. §7 still contains the obsolete /new instruction and should agree with §3. | | R2.8 — Expiry bypasses control-plane protection | Not fixed: post-creation flag setting remains available, and the backstop covers purge only; finding 5. |
- P1 — §5: the expiry update accepts a kept, permanent app. The pass selects an expired candidate, then keep acquires the mutex and sets
expires_at = 0. When the pass subsequently acquires it, the specifiedUPDATE … WHERE expires_at <= now AND deleted_at = 0 AND expiring = 0still matches: zero satisfies<= now. Step 2 then callsTrashApp, which writes the marker without checking lifecycle state (trash.go:43), and step 3 assigns the three-day deadline. The mutex prevents simultaneous transitions but does not invalidate an earlier candidate listing. An in-memory SQLite check confirms the specified update affects one row after keep. Fix: requireexpires_at > 0in the conditional update and revalidate eligibility under the mutex. Test keep between candidate selection and lock acquisition.
- P1 — §§3–4: moving the console record can put a permanent site under expiry. Create temporary
A/sitewithout deploying it. Suppose account B already has permanentsitereleases and a non-platform route, but no console app row. That is an eligible account-repair target: the review groups accessible same-named routes whose account has no app row (accountreview.go:65). The browser move callsMoveApp(appsettings.go:1350), whose update changes onlyaccount_id, preserving the proposed expiry fields (store.go:1705). B’s existing permanent releases now fall under automatic expiry and three-day purge. This is separate from the accepted bucket-copy migration outcome. Fix: require keep before moving a temporary row, enforce it in the shared move operation, and serialize the check with lifecycle transitions.
- P2 — §§5–6: recovered expiry leaves
expiring = 1across restore. Crash after writing the expiry tombstone but before completing step 3. Startup materialization can mark the row trashed (materialize.go:367); the proposed extension sets the purge fields but never clearsexpiring. Restore likewise clears onlydeleted_at,purge_afterandpurge_for. The existing row helper provides no additional transition handling (store.go:1469). The restored app therefore retains an outstanding instruction to retry expiry from step 2, allowing it to be trashed again despite its renewed deadline; keep and TTL also remain refused. Fix: settleexpiringwhen materialization completes the trash transition, clear it atomically during restore, and ensure retry eligibility cannot survive a completed restore.
- P2 — §§3, 5–6: failed console restore frees a capacity slot before recovery. With 19 live temporary apps, restore a twentieth. Tombstone deletion succeeds but the final row update fails—a failure explicitly handled by the existing API (apicli.go:1338). The restored app serves again, but its row remains excluded from the cap. Create another temporary app, bringing the counted total to 20. Later, §5’s missing-marker catch-up restores the first row without a cap check, producing 21 live apps. No direct-mode operation or simultaneous create is required. Fix: reserve capacity durably before removing the marker, count incomplete restores against the cap, and retain that reservation through recovery. Merely adding a late refusal cannot undo the already-serving restore.
- P2 — §§3–5: an existing temporary app can still become the control plane. Removing
control_planefrom the create request does not close the existing owner-facing setter:handleAppControlPlaneSetcallsSetAppControlPlane(..., true)on an existing app (controlplane.go:288). §4 does not restrict that path. §5 checks the flag only before early purge, so expiry still trashes an app that ordinary deletion explicitly protects (apicli.go:1209). Fix: require keep before setting the control-plane flag and recheck the flag before automatic trash as well as purge, under the lifecycle mutex.
- P3 — §7: the proposed listing assumes an API shape and command that do not exist.
carlos appscurrently has nolistsubcommand (apps.go:129). The existing app enumeration returns account entries containing app-name strings (apicli.go:548), and clients decode them as[]string(account_choose.go:27). Its store query excludes trashed apps (store.go:1362), so it cannot supply the advertised purge countdown. Fix: specify the new list command and an additive metadata endpoint or response field, including trash visibility and service-principal filtering; preserve the existing string-array contract.
Verdict: revise.