You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(deploy): SetTTL terminal/permanent guard authoritative in SQL — close TOCTOU (#237)
Design-review follow-up to #228. SetTTL guarded permanent + terminal deploys
via a PRE-READ of the looked-up row, then called SetDeploymentTTL whose UPDATE
only guarded `ttl_policy != 'permanent'`. Two problems the panel surfaced:
1. TOCTOU: a deploy reaped to expired/deleted/stopped (or made permanent)
between the handler's status read and the UPDATE would still get
expires_at re-armed + reminders_sent reset to 0 — re-entering the
6-email reminder cycle for a deploy the user thinks is gone. A lost race
also returned 200 "TTL set" for a write that never happened.
2. The terminal SQL guard was missing entirely (only permanent was guarded).
Fix: make the status-guarded UPDATE the AUTHORITATIVE guard. SetDeploymentTTL
now `WHERE id=$2 AND ttl_policy != 'permanent' AND status NOT IN
('deleted','expired','stopped')` and returns whether a row was actually
updated. The handler drops the stale pre-read; on !applied it re-reads (now
authoritative, not racy) ONLY to pick the precise code — already_permanent vs
invalid_state — so a concurrent change yields an honest 409, never a phantom
200.
Added lifecycleTerminalStatusesSQL (deleted/expired/stopped) as a NAMED const,
deliberately distinct from terminalDeploymentStatusesSQL (deleted/expired):
the former is "reject lifecycle mutations" (stopped is terminal), the latter
is "hide from the user's list" (stopped is still visible). Documented why they
differ — the panel's "unify them" call was wrong; they answer different
questions.
Tests: model SetDeploymentTTL now 100% (added the 0-rows→applied=false sqlmock
case); the existing TestSetTTL_{Permanent,Terminal}Rejected now deterministically
cover the handler's re-read !applied sub-branches (permanent→already_permanent,
terminal→invalid_state). Signature change threaded through 3 callers. Verified
vs real Postgres+Redis; all changed lines covered.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
0 commit comments