Summary
On Python 3.10, workbench_db.py cannot parse the timestamps it writes itself, so remediation claim leases never expire and abandoned claims permanently block a finding.
Every workbench timestamp is written with a Z suffix (workbench_db.py line 122):
def now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
and read back with a bare datetime.fromisoformat (lines 132-144):
def remediation_claim_is_active(remediation: sqlite3.Row) -> bool:
...
try:
parsed = datetime.fromisoformat(claimed_at)
if parsed.tzinfo is None:
return True
except ValueError:
return True
datetime.fromisoformat only accepts the Z suffix from Python 3.11 onward. On 3.10 the call raises ValueError, the handler returns True, and the lease check becomes unconditional: a claim is always reported as active regardless of CLAIM_LEASE_SECONDS or DELIVERED_ACTION_LEASE_SECONDS.
The package supports Python 3.10 (README.md states 3.10 or later, and the interpreter probe in src/runtime.ts enforces >= 3.10), so this is a supported configuration.
The rest of the codebase already handles this correctly — finalize_scan_contract.py line 649 normalizes the suffix before parsing:
datetime.fromisoformat(value[:-1] + "+00:00" if value[-1] in "Zz" else value)
This call site was missed.
Affected version and environment
- Released package:
@openai/codex-security@0.1.1
- Confirmed on current
main at f22d4a36f26d16287bcdfd707b369116e02a08c3
- macOS 26.5.2 (build 25F84)
- Python 3.10.20 (affected) and Python 3.12.13 (not affected)
Steps to reproduce
Using the real workbench_db.py, with a claim far beyond the 120-second lease:
import sys
from datetime import datetime, timedelta, timezone
sys.path.insert(0, "sdk/typescript/_bundled_plugin/scripts")
import workbench_db as w
old = (datetime.now(timezone.utc) - timedelta(days=30)).isoformat().replace("+00:00", "Z")
row = {
"pending_action_claim_token": "tok_abandoned",
"pending_action_delivered_at": None,
"pending_action_claimed_at": old,
}
print(sys.version_info[:2], w.CLAIM_LEASE_SECONDS, w.remediation_claim_is_active(row))
Observed result:
python 3.10 CLAIM_LEASE_SECONDS=120 claimed_at=2026-06-29T01:32:39.732153Z (30 days old)
remediation_claim_is_active -> True BUG: stale claim reported as active
python 3.12 CLAIM_LEASE_SECONDS=120 claimed_at=2026-06-29T01:32:39.796613Z (30 days old)
remediation_claim_is_active -> False correct: lease expired
(On 3.10 this requires tomli, as documented, because workbench_db.py transitively imports deep_scan_config.)
The underlying parse failure in isolation:
python 3.10 stamp=2026-07-29T01:32:09.047781Z -> ValueError: Invalid isoformat string: '2026-07-29T01:32:09.047781Z'
python 3.12 stamp=2026-07-29T01:32:09.072262Z -> PARSED OK
Expected behavior
A remediation claim older than its lease should be treated as expired on every supported interpreter, so the stale-claim recovery paths work.
Actual behavior
On Python 3.10 the claim is always active. Both recovery paths that depend on lease expiry become unreachable:
- Closing the finding is refused with "Wait for the pending remediation operation to finish before closing this finding." (line 1878)
- Regenerating the remediation is refused with "Finish or retry the active remediation operation before regenerating." (line 1987)
Both guards are written so that a failed state plus an expired lease is the escape hatch, which is exactly the condition that can never become true here.
Impact
If a remediation worker dies while holding a claim, pending_action_claim_token stays set and the lease can never clear it. On Python 3.10 the finding is permanently stuck: it can be neither closed nor regenerated, and the only remaining path is manual database intervention.
Note that the SQL-based staleness checks elsewhere in the file compare timestamps as strings and are unaffected, so the two mechanisms disagree with each other on 3.10.
Suggested direction
Normalize the suffix before parsing at this call site, matching what finalize_scan_contract.py already does. It may be worth factoring that normalization into a single shared helper so a third call site cannot drift again.
Summary
On Python 3.10,
workbench_db.pycannot parse the timestamps it writes itself, so remediation claim leases never expire and abandoned claims permanently block a finding.Every workbench timestamp is written with a
Zsuffix (workbench_db.pyline 122):and read back with a bare
datetime.fromisoformat(lines 132-144):datetime.fromisoformatonly accepts theZsuffix from Python 3.11 onward. On 3.10 the call raisesValueError, the handler returnsTrue, and the lease check becomes unconditional: a claim is always reported as active regardless ofCLAIM_LEASE_SECONDSorDELIVERED_ACTION_LEASE_SECONDS.The package supports Python 3.10 (
README.mdstates 3.10 or later, and the interpreter probe insrc/runtime.tsenforces>= 3.10), so this is a supported configuration.The rest of the codebase already handles this correctly —
finalize_scan_contract.pyline 649 normalizes the suffix before parsing:This call site was missed.
Affected version and environment
@openai/codex-security@0.1.1mainatf22d4a36f26d16287bcdfd707b369116e02a08c3Steps to reproduce
Using the real
workbench_db.py, with a claim far beyond the 120-second lease:Observed result:
(On 3.10 this requires
tomli, as documented, becauseworkbench_db.pytransitively importsdeep_scan_config.)The underlying parse failure in isolation:
Expected behavior
A remediation claim older than its lease should be treated as expired on every supported interpreter, so the stale-claim recovery paths work.
Actual behavior
On Python 3.10 the claim is always active. Both recovery paths that depend on lease expiry become unreachable:
Both guards are written so that a
failedstate plus an expired lease is the escape hatch, which is exactly the condition that can never become true here.Impact
If a remediation worker dies while holding a claim,
pending_action_claim_tokenstays set and the lease can never clear it. On Python 3.10 the finding is permanently stuck: it can be neither closed nor regenerated, and the only remaining path is manual database intervention.Note that the SQL-based staleness checks elsewhere in the file compare timestamps as strings and are unaffected, so the two mechanisms disagree with each other on 3.10.
Suggested direction
Normalize the suffix before parsing at this call site, matching what
finalize_scan_contract.pyalready does. It may be worth factoring that normalization into a single shared helper so a third call site cannot drift again.