fix(web_server): stop Codex OAuth worker from finishing after cancel - #73914
Conversation
Cancelling a pending OpenAI Codex device-code login only popped the session dict; the background worker had no way to observe the cancellation and kept polling, exchanging the code, and saving tokens regardless. Once the session was gone, _oauth_session_profile() returned None and the save fell back to the caller's current profile scope instead of the profile the login was started in. Fix: cancel_oauth_session marks the dict cancelled=True before popping it, and _codex_full_login_worker (which holds a reference to the same dict object) checks that flag before every remaining sleep/poll, before the token exchange, and before saving. The profile is captured once up front so it can never be re-derived from a session that no longer exists.
The prior CI run's only failure was tests/gateway/test_streaming_tts_consumer.py ::TestConsumerLifecycle::test_pre_audio_timeout_aborts_before_fallback_can_replay, a file this PR does not touch. Confirmed as a pre-existing timing flake on main (5/5 local passes on a fresh origin/main worktree, unrelated to gateway/streaming_tts_consumer.py tightening a 0.05s sleep margin under CI's 8-way parallel load) — not a regression from this change.
teknium1
left a comment
There was a problem hiding this comment.
Thanks for targeting a real current-main OAuth/profile-isolation bug.
Problems
- The final guard at
hermes_cli/web_server.py:11392is not synchronized with_save_codex_tokens()at:11398-11402. DELETE can set cancellation and return success after that read but before persistence, so the stated no-write-after-cancel guarantee still does not hold. This is also the gap identified in the #73912 discussion. - The endpoint pops before it sets
cancelledathermes_cli/web_server.py:11516-11518, despite the new docstring claiming the reverse. The worker reads that field outside the lock. - The new worker test directly mutates the dict at
tests/hermes_cli/test_web_oauth_dispatch.py:443-446; it does not exercise DELETE/removal or force the final-check-to-save interleaving. - The shared endpoint leaves analogous profile re-resolution paths in the Nous, MiniMax, and xAI pollers (
hermes_cli/web_server.py:10410,:10500,:10550).
Suggested changes
- Define an atomic cancellation/persistence boundary and add a deterministic real-DELETE regression test at that boundary.
- Audit or separately track the sibling device-code pollers.
Automated hermes-sweeper review.
# Conflicts: # tests/hermes_cli/test_web_oauth_dispatch.py
|
suggesting changes The PR closes the user-initiated DELETE race for the OpenAI Codex device-code worker: it preserves the originating profile, exposes cancellation through the retained session object, and makes the final cancellation check and credential save atomic. A source-extracted runtime probe reproduced the current-main token save after DELETE and showed that PR head suppresses it. However, the same session-removal invariant is still bypassed by TTL garbage collection:
Security evidence:
Uncertainty: The repository's pytest suite could not be executed locally because no pytest-capable virtual environment is available in the leased checkout or configured runner environment.; No live OpenAI OAuth exchange was performed; provider behavior was deterministically simulated because network access is prohibited.; The maximum real-world GC race window depends on initial device-code request latency and scheduler timing, although the source establishes that it can be non-zero. Signed: GPT-5.6-sol-xhigh in Codex |
…th-cancel-race-ia01 fix(web_server): stop Codex OAuth worker from finishing after cancel
Summary
Fixes IA-01 (closes #73912): cancelling a pending OpenAI Codex device-code OAuth login did not stop the background worker. If the user approved the device code at OpenAI after clicking Cancel, the worker still exchanged the code and saved tokens — and could save them into the wrong profile, since profile resolution fell back to the caller's current active profile once the session entry was gone.
Root cause
DELETE /api/providers/oauth/sessions/{session_id}(cancel_oauth_session) only popped the session from_oauth_sessions— it never signalled the worker._codex_full_login_workernever re-checked cancellation during its poll loop._oauth_session_profile(session_id)) ran after the session was gone, returningNone→_profile_scope(None)silently fell back to the current active profile.Fix
cancel_oauth_sessionnow setssess["cancelled"] = Trueon the shared session dict before popping it, so the worker (which holds a reference to that same dict object) can observe it._codex_full_login_workercapturessession_profile = sess.get("profile")once, right after publishing theuser_code— never re-derived later, so a cancelled/popped session can no longer fall back to the caller's current profile scope.sess.get("cancelled")before/after each poll sleep, before the authorization-code exchange, and immediately before calling_save_codex_tokens()._save_codex_tokens()itself is unchanged — the guard lives entirely in the worker's call sites.Infographic
Test plan
test_codex_dashboard_worker_stops_polling_after_cancel— new: simulates a concurrent DELETE firing mid-poll, asserts_save_codex_tokensis never called and status stays"pending".test_cancel_oauth_session_marks_dict_cancelled_before_popping— new: asserts the DELETE endpoint mutates the shared dict'scancelledflag before removing it from_oauth_sessions.pytest tests/hermes_cli/test_web_oauth_dispatch.py— 26/26 passing.tests/hermes_cli/test_web_server_oauth_write.py::test_dashboard_oauth_write_uses_owner_only_permissionspre-exists onmain(Windows doesn't enforce POSIXchmod 0o600) viagit stash+ re-run — unrelated to this change.Out of scope
This PR only addresses IA-01. Other findings from the same investigation (fallback secret-scope isolation, Azure hostname classification, compression race, etc.) are tracked separately and not part of this change.