fix(auth): preserve independent Codex pool entries on re-auth (#39236) - #39243
fix(auth): preserve independent Codex pool entries on re-auth (#39236)#39243temalo wants to merge 1 commit into
Conversation
…search#39236) The NousResearch#33538 fix refreshed every credential_pool entry with source "manual:device_code" on every Codex OAuth re-auth, on the assumption that such entries were always legacy aliases of the singleton from the NousResearch#33000 workaround era. That assumption is no longer true: `hermes auth add openai-codex` also produces "manual:device_code" entries for independent ChatGPT accounts, and the broad sync silently clobbered them with the latest-authenticated token pair (labels preserved, token material overwritten, status / quota readings then lie). Narrow the sync: refresh a "manual:device_code" entry only when its existing access_token matches the previous singleton access_token (true legacy alias). Entries with distinct token material represent independent accounts and are now left alone. Error markers are cleared only on entries actually rewritten, so an independent account's own 429 / 401 state survives a re-auth that targeted a different account. Tests: * New: independent acctB/acctC are not overwritten when acctA re-auths. * New: legacy singleton-alias still refreshed (preserves NousResearch#33538). * New: missing previous singleton state handled (no crash, no false alias match). * New: access_token-only alias match (legacy schema without refresh_token still recognized). * New: error markers cleared only on entries actually refreshed. * Updated: existing manual-device-code sync test now covers both the legacy-alias path AND the independent-account path in one fixture. Behaviour change is zero for users with a single Codex account and zero for users whose only "manual:device_code" entry is the legacy alias of the singleton. Users with multiple independent Codex accounts added via `hermes auth add` now keep their distinct token material across re-auths. Local: 29 passed in tests/hermes_cli/test_auth_codex_provider.py, no new failures in tests/hermes_cli/ vs upstream/main baseline. Fixes NousResearch#39236.
|
Thanks — I tested this locally and I think it correctly addresses the re-auth half of #39236. One extra detail from my repro: the bug is not only in What I saw on current main:
The combined fix that worked for me locally was:
With that combined approach, both of these cases pass:
I also added focused regression tests for both paths locally:
So from my side: this PR looks like the right fix for the re-auth half, but I think #39236 is only fully closed once the auth-add path is covered too. |
hermes auth add openai-codex now creates an independent manual:device_code pool entry per account instead of routing through the singleton _save_codex_tokens save path, which collapsed every added account into the latest login (the second add overwrote the first account's singleton-mirrored device_code entry). This is the add-path half of #39236; PR #39243 (already on this branch) fixes the re-auth half. manual:device_code entries refresh from their own token pair (_sync_codex_entry_from_auth_store only adopts the singleton for source=="device_code"), so they need no providers.openai-codex shadow. Adding the first credential marks openai-codex active (the singleton path did this implicitly) so the setup wizard's get_active_provider() check still passes; subsequent adds leave the active provider untouched. Adds SOURCE_MANUAL_DEVICE_CODE constant and a regression test that two distinct accounts keep distinct token pairs. Updates two existing add tests to the pool-only behavior. Co-authored-by: glesperance <info@glesperance.com>
hermes auth add openai-codex now creates an independent manual:device_code pool entry per account instead of routing through the singleton _save_codex_tokens save path, which collapsed every added account into the latest login (the second add overwrote the first account's singleton-mirrored device_code entry). This is the add-path half of #39236; PR #39243 (already on this branch) fixes the re-auth half. manual:device_code entries refresh from their own token pair (_sync_codex_entry_from_auth_store only adopts the singleton for source=="device_code"), so they need no providers.openai-codex shadow. Adding the first credential marks openai-codex active (the singleton path did this implicitly) so the setup wizard's get_active_provider() check still passes; subsequent adds leave the active provider untouched. Adds SOURCE_MANUAL_DEVICE_CODE constant and a regression test that two distinct accounts keep distinct token pairs. Updates two existing add tests to the pool-only behavior. Co-authored-by: glesperance <info@glesperance.com>
|
Merged via #42316 (rebase, your commit landed on main as 761b744 with your authorship preserved). Your narrowing of |
hermes auth add openai-codex now creates an independent manual:device_code pool entry per account instead of routing through the singleton _save_codex_tokens save path, which collapsed every added account into the latest login (the second add overwrote the first account's singleton-mirrored device_code entry). This is the add-path half of NousResearch#39236; PR NousResearch#39243 (already on this branch) fixes the re-auth half. manual:device_code entries refresh from their own token pair (_sync_codex_entry_from_auth_store only adopts the singleton for source=="device_code"), so they need no providers.openai-codex shadow. Adding the first credential marks openai-codex active (the singleton path did this implicitly) so the setup wizard's get_active_provider() check still passes; subsequent adds leave the active provider untouched. Adds SOURCE_MANUAL_DEVICE_CODE constant and a regression test that two distinct accounts keep distinct token pairs. Updates two existing add tests to the pool-only behavior. Co-authored-by: glesperance <info@glesperance.com>
hermes auth add openai-codex now creates an independent manual:device_code pool entry per account instead of routing through the singleton _save_codex_tokens save path, which collapsed every added account into the latest login (the second add overwrote the first account's singleton-mirrored device_code entry). This is the add-path half of NousResearch#39236; PR NousResearch#39243 (already on this branch) fixes the re-auth half. manual:device_code entries refresh from their own token pair (_sync_codex_entry_from_auth_store only adopts the singleton for source=="device_code"), so they need no providers.openai-codex shadow. Adding the first credential marks openai-codex active (the singleton path did this implicitly) so the setup wizard's get_active_provider() check still passes; subsequent adds leave the active provider untouched. Adds SOURCE_MANUAL_DEVICE_CODE constant and a regression test that two distinct accounts keep distinct token pairs. Updates two existing add tests to the pool-only behavior. Co-authored-by: glesperance <info@glesperance.com>
hermes auth add openai-codex now creates an independent manual:device_code pool entry per account instead of routing through the singleton _save_codex_tokens save path, which collapsed every added account into the latest login (the second add overwrote the first account's singleton-mirrored device_code entry). This is the add-path half of #39236; PR #39243 (already on this branch) fixes the re-auth half. manual:device_code entries refresh from their own token pair (_sync_codex_entry_from_auth_store only adopts the singleton for source=="device_code"), so they need no providers.openai-codex shadow. Adding the first credential marks openai-codex active (the singleton path did this implicitly) so the setup wizard's get_active_provider() check still passes; subsequent adds leave the active provider untouched. Adds SOURCE_MANUAL_DEVICE_CODE constant and a regression test that two distinct accounts keep distinct token pairs. Updates two existing add tests to the pool-only behavior. Co-authored-by: glesperance <info@glesperance.com>
hermes auth add openai-codex now creates an independent manual:device_code pool entry per account instead of routing through the singleton _save_codex_tokens save path, which collapsed every added account into the latest login (the second add overwrote the first account's singleton-mirrored device_code entry). This is the add-path half of NousResearch#39236; PR NousResearch#39243 (already on this branch) fixes the re-auth half. manual:device_code entries refresh from their own token pair (_sync_codex_entry_from_auth_store only adopts the singleton for source=="device_code"), so they need no providers.openai-codex shadow. Adding the first credential marks openai-codex active (the singleton path did this implicitly) so the setup wizard's get_active_provider() check still passes; subsequent adds leave the active provider untouched. Adds SOURCE_MANUAL_DEVICE_CODE constant and a regression test that two distinct accounts keep distinct token pairs. Updates two existing add tests to the pool-only behavior. Co-authored-by: glesperance <info@glesperance.com>
hermes auth add openai-codex now creates an independent manual:device_code pool entry per account instead of routing through the singleton _save_codex_tokens save path, which collapsed every added account into the latest login (the second add overwrote the first account's singleton-mirrored device_code entry). This is the add-path half of NousResearch#39236; PR NousResearch#39243 (already on this branch) fixes the re-auth half. manual:device_code entries refresh from their own token pair (_sync_codex_entry_from_auth_store only adopts the singleton for source=="device_code"), so they need no providers.openai-codex shadow. Adding the first credential marks openai-codex active (the singleton path did this implicitly) so the setup wizard's get_active_provider() check still passes; subsequent adds leave the active provider untouched. Adds SOURCE_MANUAL_DEVICE_CODE constant and a regression test that two distinct accounts keep distinct token pairs. Updates two existing add tests to the pool-only behavior. Co-authored-by: glesperance <info@glesperance.com>
hermes auth add openai-codex now creates an independent manual:device_code pool entry per account instead of routing through the singleton _save_codex_tokens save path, which collapsed every added account into the latest login (the second add overwrote the first account's singleton-mirrored device_code entry). This is the add-path half of NousResearch#39236; PR NousResearch#39243 (already on this branch) fixes the re-auth half. manual:device_code entries refresh from their own token pair (_sync_codex_entry_from_auth_store only adopts the singleton for source=="device_code"), so they need no providers.openai-codex shadow. Adding the first credential marks openai-codex active (the singleton path did this implicitly) so the setup wizard's get_active_provider() check still passes; subsequent adds leave the active provider untouched. Adds SOURCE_MANUAL_DEVICE_CODE constant and a regression test that two distinct accounts keep distinct token pairs. Updates two existing add tests to the pool-only behavior. Co-authored-by: glesperance <info@glesperance.com>
hermes auth add openai-codex now creates an independent manual:device_code pool entry per account instead of routing through the singleton _save_codex_tokens save path, which collapsed every added account into the latest login (the second add overwrote the first account's singleton-mirrored device_code entry). This is the add-path half of NousResearch#39236; PR NousResearch#39243 (already on this branch) fixes the re-auth half. manual:device_code entries refresh from their own token pair (_sync_codex_entry_from_auth_store only adopts the singleton for source=="device_code"), so they need no providers.openai-codex shadow. Adding the first credential marks openai-codex active (the singleton path did this implicitly) so the setup wizard's get_active_provider() check still passes; subsequent adds leave the active provider untouched. Adds SOURCE_MANUAL_DEVICE_CODE constant and a regression test that two distinct accounts keep distinct token pairs. Updates two existing add tests to the pool-only behavior. Co-authored-by: glesperance <info@glesperance.com>
hermes auth add openai-codex now creates an independent manual:device_code pool entry per account instead of routing through the singleton _save_codex_tokens save path, which collapsed every added account into the latest login (the second add overwrote the first account's singleton-mirrored device_code entry). This is the add-path half of NousResearch#39236; PR NousResearch#39243 (already on this branch) fixes the re-auth half. manual:device_code entries refresh from their own token pair (_sync_codex_entry_from_auth_store only adopts the singleton for source=="device_code"), so they need no providers.openai-codex shadow. Adding the first credential marks openai-codex active (the singleton path did this implicitly) so the setup wizard's get_active_provider() check still passes; subsequent adds leave the active provider untouched. Adds SOURCE_MANUAL_DEVICE_CODE constant and a regression test that two distinct accounts keep distinct token pairs. Updates two existing add tests to the pool-only behavior. Co-authored-by: glesperance <info@glesperance.com>
hermes auth add openai-codex now creates an independent manual:device_code pool entry per account instead of routing through the singleton _save_codex_tokens save path, which collapsed every added account into the latest login (the second add overwrote the first account's singleton-mirrored device_code entry). This is the add-path half of NousResearch#39236; PR NousResearch#39243 (already on this branch) fixes the re-auth half. manual:device_code entries refresh from their own token pair (_sync_codex_entry_from_auth_store only adopts the singleton for source=="device_code"), so they need no providers.openai-codex shadow. Adding the first credential marks openai-codex active (the singleton path did this implicitly) so the setup wizard's get_active_provider() check still passes; subsequent adds leave the active provider untouched. Adds SOURCE_MANUAL_DEVICE_CODE constant and a regression test that two distinct accounts keep distinct token pairs. Updates two existing add tests to the pool-only behavior. Co-authored-by: glesperance <info@glesperance.com>
hermes auth add openai-codex now creates an independent manual:device_code pool entry per account instead of routing through the singleton _save_codex_tokens save path, which collapsed every added account into the latest login (the second add overwrote the first account's singleton-mirrored device_code entry). This is the add-path half of NousResearch#39236; PR NousResearch#39243 (already on this branch) fixes the re-auth half. manual:device_code entries refresh from their own token pair (_sync_codex_entry_from_auth_store only adopts the singleton for source=="device_code"), so they need no providers.openai-codex shadow. Adding the first credential marks openai-codex active (the singleton path did this implicitly) so the setup wizard's get_active_provider() check still passes; subsequent adds leave the active provider untouched. Adds SOURCE_MANUAL_DEVICE_CODE constant and a regression test that two distinct accounts keep distinct token pairs. Updates two existing add tests to the pool-only behavior. Co-authored-by: glesperance <info@glesperance.com>
hermes auth add openai-codex now creates an independent manual:device_code pool entry per account instead of routing through the singleton _save_codex_tokens save path, which collapsed every added account into the latest login (the second add overwrote the first account's singleton-mirrored device_code entry). This is the add-path half of NousResearch#39236; PR NousResearch#39243 (already on this branch) fixes the re-auth half. manual:device_code entries refresh from their own token pair (_sync_codex_entry_from_auth_store only adopts the singleton for source=="device_code"), so they need no providers.openai-codex shadow. Adding the first credential marks openai-codex active (the singleton path did this implicitly) so the setup wizard's get_active_provider() check still passes; subsequent adds leave the active provider untouched. Adds SOURCE_MANUAL_DEVICE_CODE constant and a regression test that two distinct accounts keep distinct token pairs. Updates two existing add tests to the pool-only behavior. Co-authored-by: glesperance <info@glesperance.com>
Fixes #39236.
Why
hermes auth add openai-codexfor a second/third OpenAI account silently overwrites earlier independent accounts in the credential pool. The visible labels stay, but the token material behind each label gets replaced with whatever was just authenticated — sohermes auth list openai-codexlies, quota checks lie, and rotation lies.The root cause is a too-broad sync introduced by #33538 to fix #33000 / #33538. That fix treated every
credential_poolentry whose source string ismanual:device_codeas a legacy alias of the singleton-seededdevice_codeentry and refreshed them all on every Codex OAuth save. This was correct whenmanual:device_codeonly meant "legacy workaround entry" — but the same source string is now also produced byhermes auth add openai-codexfor independent ChatGPT accounts. Refreshing every match conflates the two cases and clobbers distinct accounts.Repro confirmed on
upstream/main(a3fb48b) with the issue's synthetic_save_codex_tokensscript — twomanual:device_codeentries (acctA,acctB) end up sharing the latest-authenticated token pair after a third_save_codex_tokenscall. After the fix in this PR the same script preserves both entries' original tokens.What
hermes_cli/auth.py—_save_codex_tokensnow captures the previous singleton tokens before overwriting them, and passes them into_sync_codex_pool_entriesas a new keyword arg.hermes_cli/auth.py—_sync_codex_pool_entriesnow treats amanual:device_codeentry as a singleton-alias (and refreshes it) only when its currentaccess_tokenmatches the previous singletonaccess_token. Entries with distinct token material are left alone.hermes_cli/auth.py— error-marker clearing (last_status,last_error_*) is now scoped to entries that were actually refreshed by the current re-auth. An independent account's own 429 quota state survives a re-auth that targeted a different account.tests/hermes_cli/test_auth_codex_provider.py— 5 new tests covering the regression and edge cases, plus the existingtest_save_codex_tokens_syncs_manual_device_code_entriesrewritten to cover BOTH the legacy-alias path and the independent-account path in one fixture.The source-string contract (
device_codefor singleton,manual:device_codefor manually-added device-code OAuth) is unchanged. No schema migration. No call-site changes outside_save_codex_tokens. The fix lives entirely inside the helper.Behaviour change
Zero for users with a single Codex account.
Zero for users whose only
manual:device_codepool entry is a legacy alias of the singleton (the #33538 use case) — that entry's tokens still matched the previous singleton at the moment of re-auth, so it still gets refreshed.Users with multiple independent Codex accounts added via
hermes auth add openai-codexnow keep their distinct token material across re-auths. Previously each re-auth silently overwrote them.Test coverage (added)
test_save_codex_tokens_does_not_overwrite_independent_manual_entriestest_save_codex_tokens_still_refreshes_legacy_manual_aliasmanual:device_codeentry whose tokens DO match the previous singleton is still refreshed on re-auth (preserves the #33538 fix and clears stale error markers).test_save_codex_tokens_handles_missing_previous_singleton_tokensproviders.openai-codex.tokensblock does not crash and does not falsely alias any pre-existing pool entry.test_save_codex_tokens_alias_match_uses_access_token_onlyaccess_tokenbut norefresh_token(older schema) is still recognized as an alias and refreshed.test_save_codex_tokens_clears_error_markers_only_on_refreshed_entriesquota_exhaustedmarkers survive a re-auth that targeted a different account; the seeded singleton's 401 markers are cleared as before.test_save_codex_tokens_syncs_manual_device_code_entries(rewritten)manual:api_keyentry (untouched) in one pass.Local: 29 passed in
tests/hermes_cli/test_auth_codex_provider.py. Pristine-vs-branch diff overtests/hermes_cli/shows zero new failures introduced by this change.Out of scope
auth_add_commandforopenai-codexstill routes through_save_codex_tokens(the singleton save path). The narrowed sync makes that safe — a re-auth that targets account N no longer clobbers accounts M ≠ N — but a cleaner long-term design would havehermes auth add openai-codexadd a freshPooledCredentialdirectly (matching thexai-oauth/google-gemini-clipatterns) instead of routing through the singleton. That refactor changes the singleton-vs-pool contract for Codex and warrants its own discussion, so it's deferred to a follow-up.The CLI message printed after
auth add openai-codexstill resolves the displayed label from the firstdevice_codepool entry (auth_commands.py:321). With the sync narrowed, the wrong-label-shown path identified in the issue is now harder to hit in practice, but the label-resolution code itself is unchanged — that's a separate small fix.No change to
agent/credential_pool.pyseeding behavior. The_seed_from_singletonspath that creates the initialdevice_codemirror entry is untouched.No change to the
manual:api_keyhandling — those entries were already excluded from the broad sync and remain so.