fix(agent): keep pool entries when terminal-OAuth quarantine save fails - #44856
fix(agent): keep pool entries when terminal-OAuth quarantine save fails#44856roryford wants to merge 1 commit into
Conversation
The xAI, Codex, and Nous terminal-refresh handlers in credential_pool shared a copy-paste defect: the auth.json clear/quarantine ran inside a try/except that only logged at debug, and the handler then unconditionally removed the singleton-seeded entries from the pool and persisted. If the auth-store save failed (flock timeout, disk I/O on the atomic-replace path), the pool was wiped while auth.json still held the revoked refresh token, so the next load_pool() re-seeded the dead credential via _seed_from_singletons and the agent looped on terminal auth failures — exactly the re-seeding loop the quarantine was added to prevent. Gate the entry removal + persist on the auth-store clear succeeding, and log the failure at WARNING. On failure the entry stays in the pool so both sides keep the token consistently and the next refresh attempt retries the quarantine. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Verification: OAuth quarantine save-failure guard looks correct. Applied security checklist to this credential-pool PR:
Clean — no findings. |
tonydwb
left a comment
There was a problem hiding this comment.
Good fix for terminal OAuth quarantine save failures. The _refresh_entry was deleting pool entries even when the auth store clear failed (cleared=False). Now entries are only removed if the clear actually succeeded. Also upgrades debug logs to warning for better visibility. No issues found.
|
Superseded by #63131, which re-implements this credential-pool fix on current |
Problem
The three terminal-OAuth-error handlers in
agent/credential_pool.py— xAI, Codex, and Nous — share an identical structure: when a refresh token is terminally revoked, they (a) clear/quarantine the token inauth.jsoninside atry/exceptthat only logs at debug, then (b) unconditionally remove the matching entries from the in-memory pool and persist.If step (a) fails — the auth-store flock has a 15s timeout and is contended by the refresh path itself; the save path does
O_EXCLopen +fsync+ atomic replace, all of which can raise — the pool is wiped whileauth.jsonstill holds the revoked token. On the nextload_pool(),_seed_from_singletonsre-seeds the dead credential and the agent loops on terminal auth failures with no path to the clean "re-login required" state.This is exactly the re-seeding loop the quarantine logic (#27911, #28118, #28116) was added to prevent — it fails back into it when its own I/O fails.
Fix
Gate the pool-entry removal on the quarantine actually completing: a
clearedflag is set when the try block finishes without exception (including the no-op case where the store's refresh token doesn't match the entry's), and on exception the handler now logs at warning and leaves the pool entry in place. Pool andauth.jsonstay consistent either way, and the next refresh attempt retries the quarantine. Same minimal change applied to all three provider handlers.Tests
Added to
tests/agent/test_credential_pool.py, one per provider: monkeypatch_save_auth_storeto raiseOSError, trigger a terminal refresh error, assert the pool entry survives andauth.jsonis untouched. All three new tests fail against pre-fix code (verified via stash round-trip) and pass with the fix. Existing success-path quarantine tests pass unchanged — 89 total in the touched files; ruff clean.🤖 Generated with Claude Code