carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

| Round 1 finding | Disposition | |---|---| | 1. Expiry signature accepted as production-off | Fixed: §2 removes the signed expiry record. | | 2. Existing data passes temporary creation checks | Fixed at the create door: §3 requires an empty prefix and a fresh row. A separate rename bypass is described below. | | 3. Keep/TTL races expiry | Fixed: §5 serializes these console transitions and records expiry in progress. | | 4. Purge re-read misses the destruction deadline | Not fixed across all consumers: the unchanged box pass checks an old tombstone’s age, then calls unguarded PurgeApp (reconcile.go:228). Restore→keep→trash can still substitute a fresh permanent tombstone. Update or gate that automatic purge path too; the console mutex cannot protect it. | | 5. Deleted expiry record remains replayable | Fixed: no bucket expiry authority remains. | | 6. Restore bypasses the cap | Fixed for console restores: §6 applies §3’s admission check. The explicit direct-mode exception is acceptable. | | 7. Static restrictions exist only in the CLI | Fixed for the reported kind/host paths: §4 requires server enforcement. | | 8. Browser/headless restore retains stale expiry | Fixed in the specified behavior: shared console restore and explicit catch-up renewal. Catch-up’s interpretation of missing markers has a separate flaw below. | | 9. Interrupted purge loses the short deadline | Fixed for an interrupted payload sweep: the deadline survives in the row. Completion recovery remains incomplete below. | | 10. Migration disables expiry | Declined-with-reason acceptable: §4 explicitly accepts direct operator migration. However, its API-mode refusal describes a mode that does not currently exist: migration opens the registry and bucket directly (accounts.go:330, accounts.go:346). |

  1. P1 — §§3, 5: renaming a temporary row can put existing permanent data under expiry. A fresh claim can succeed despite failure to write its default pipeline, leaving an empty prefix (server.go:3571). Rename checks the source prefix for emptiness (appsettings.go:785); its destination checks do not reject shipped-but-unclaimed releases. RenameAppIn changes only the name, preserving the proposed expiry columns (store.go:1787). Rename that empty temporary app onto a name with permanent releases but no row or routes, and those releases become eligible for destruction. Fix: require keep before renaming a temporary app, or apply the full fresh-prefix reservation checks to the destination and serialize rename with lifecycle transitions.
  1. P1 — §§4–5, 8: early purge can erase the records needed to revoke email infrastructure. Contrary to §8’s premise, email provisioning paths exist: domain creation calls mail.EnsureDomain (apimail.go:1016), and standalone credential creation calls mail.CreateStandalone (apimail.go:1391). Neither is prohibited by §4. Ordinary console purge runs mail.Teardown before sweeping the prefix specifically to avoid orphaned credentials and unreleasable domain claims (ownership.go:410). The proposed early purge skips that teardown and deletes the records (trash.go:257). Fix: require keep before creating email resources, and refuse automatic purge if any email resources or claims nevertheless exist.
  1. P2 — §6: a failed restore permanently cancels the three-day purge. Step 3 clears purge_after and purge_for before step 4 deletes the marker. That deletion can fail; RestoreApp directly returns the storage error (trash.go:98). After failure or a crash between those steps, the app remains trashed with no early-purge deadline. Neither §5 query selects it, so cleanup falls back to 30 days. Fix: retain the purge deadline until restore completes, using a durable restoring state to make crash recovery unambiguous. Clear the deadline atomically with the successful row restore.
  1. P2 — §5: ErrNotTrashed does not prove that somebody restored the app. Purge itself deletes the marker last (trash.go:265). If the sweep succeeds but PurgeAppRow fails, the next pass receives ErrNotTrashed and follows the proposed restore path: it renews an empty app, retains its name, and counts it against the live cap. An independent box purge produces the same ambiguity. Existing materialization distinguishes absent-marker-with-objects from absent-marker-without-objects (materialize.go:376). Fix: record purge progress durably and finalize completed purges; do not infer restore from marker absence alone.
  1. P2 — §§2, 5–6: materializing a trash marker can omit the early-purge deadline. Manual trash writes the bucket marker before updating the row; row failure explicitly relies on materialization for recovery (apicli.go:1279). Startup materialization calls TrashAppRow (materialize.go:367), which updates only deleted_at (store.go:1460). The spec extends the two manual handlers but does not specify equivalent trash recovery here. A temporary app recovered this way has deleted_at > 0 and purge_after = 0, excluding it from both new passes. Fix: include materialization in the lifecycle protocol and recover the original marker-bound deadline for temporary trash transitions.
  1. P2 — §4: instance creation bypasses the object-store prohibition. A caller can enable private_storage through the instance-manifest endpoint (apiinstances.go:1751), then create a static instance at the permitted platform host. Instance creation provisions its store directly (apiinstances.go:995), without entering the object-store create handler. All listed temporary-app checks pass, but automatic purge subsequently refuses the provisioned store (trash.go:248). Fix: prohibit private-storage enablement and implicit provisioning while temporary; enforce the restriction in the shared provisioning path.
  1. P2 — §§3, 7: an older console silently creates a permanent app for --temporary. The existing create request has no temporary field (apicli.go:359), and its JSON decoder accepts unknown fields (authcli.go:940). Sending the proposed request to that console succeeds as an ordinary permanent creation, after which the specified CLI flow ships and promotes. Fix: use a distinct temporary-create endpoint, or require capability confirmation before creation and verify the returned expiry before shipping. Missing support must produce an explicit refusal.
  1. P2 — §§3–5: expiry bypasses the control-plane deletion protection. The existing create endpoint accepts control_plane: true from an owner (apicli.go:373); the proposed temporary field is not declared incompatible with it. Ordinary trash and purge refuse control-plane apps (apicli.go:1209, ownership.go:335). The new loop calls release helpers directly, bypassing that database-backed protection. Fix: reject temporary/control-plane combinations and recheck the control-plane flag before automatic trash or purge.

Verdict: revise.