fix(credential_pool): add Nous OAuth cross-process sync to prevent session revocation#10160
Conversation
|
Thanks for the automated review @mxnstrexgl — glad to see all security and code quality checks passing. Regarding the suggestion to add a test for malformed/null provider state in auth.json: the existing The |
f0fa6ee to
c6b78a3
Compare
|
Rebased onto main. Resolved conflicts: autocontrib · pr-repair-bad80b0b · 2026-04-19T22:33:06Z |
ab95cec to
8a69a8f
Compare
|
Rebased onto main. Fixed autocontrib · pr-repair-533b8c7d · 2026-04-20T01:53:34Z |
8baee58 to
6204a74
Compare
|
Rebased onto main. All 17 failures from the previous CI run were pre-existing issues on main that have since been fixed upstream (commits autocontrib · pr-repair-5d41682b · 2026-04-20T05:35:02Z |
|
Rebased onto latest main (clean, no conflicts, 244 commits). All 3 previously-failing tests ( autocontrib · pr-repair-4cb35959 · 2026-04-21T15:39:22Z |
6204a74 to
9cc823b
Compare
|
Rebased onto latest origin/main (clean, no conflicts). Fixed 5 pre-existing CI failures in unrelated test files — all caused by the same root issue: patches targeting source modules after production code moved to module-level imports:
The other 2 of the original 7 CI failures ( PR-owned credential pool tests: 35/35 pass locally. |
9cc823b to
584755e
Compare
584755e to
d6f1e60
Compare
|
Rebased onto latest origin/main (clean, no conflicts). Fixed 3 pre-existing CI test failures that existed on main before this PR:
All 3 previously-failing tests now pass. PR-owned credential pool tests: 35/35 pass locally. |
|
Rebased onto latest origin/main (clean, no conflicts, 162 commits). All 21 previously-failing CI tests ( |
d6f1e60 to
fff4c15
Compare
|
Rebased onto latest origin/main (60 commits). Fixed 2 pre-existing test failures that were not resolved by the rebase:
The other 7 of 9 previously-failing tests ( |
fff4c15 to
d72b909
Compare
Concurrent Hermes processes (e.g. cron jobs) refreshing a Nous OAuth token via resolve_nous_runtime_credentials() write the rotated tokens to auth.json. The calling process's pool entry becomes stale, and the next refresh against the already-rotated token triggers a 'refresh token reuse' revocation on the Nous Portal. _sync_nous_entry_from_auth_store() reads auth.json under the same lock used by resolve_nous_runtime_credentials, and adopts the newer token pair before refreshing the pool entry. This complements #15111 (which preserved the obtained_at timestamps through seeding). Partial salvage of #10160 by @konsisumer — only the agent/credential_pool.py changes + the 3 Nous-specific regression tests. The PR also touched 10 unrelated files (Dockerfile, tips.py, various tool tests) which were dropped as scope creep. Regression tests: - test_sync_nous_entry_from_auth_store_adopts_newer_tokens - test_sync_nous_entry_noop_when_tokens_match - test_nous_exhausted_entry_recovers_via_auth_store_sync
|
Thanks @konsisumer. Partial salvage merged in #15120 — cherry-picked the The other 10 files in your PR (Dockerfile, hermes_cli/tips.py, test_minimax_provider.py, test_agent_cache.py, test_concurrent_interrupt.py, test_flush_memories_codex.py, test_browser_camofox.py, test_browser_cdp_tool.py, test_write_deny.py, test_zombie_process_cleanup.py) looked like branch-drift noise from a stale fork and were dropped as scope creep. If any of those changes are intentional bug fixes, open focused PRs and we'll review each. Closing as primary fix merged. |
Concurrent Hermes processes (e.g. cron jobs) refreshing a Nous OAuth token via resolve_nous_runtime_credentials() write the rotated tokens to auth.json. The calling process's pool entry becomes stale, and the next refresh against the already-rotated token triggers a 'refresh token reuse' revocation on the Nous Portal. _sync_nous_entry_from_auth_store() reads auth.json under the same lock used by resolve_nous_runtime_credentials, and adopts the newer token pair before refreshing the pool entry. This complements NousResearch#15111 (which preserved the obtained_at timestamps through seeding). Partial salvage of NousResearch#10160 by @konsisumer — only the agent/credential_pool.py changes + the 3 Nous-specific regression tests. The PR also touched 10 unrelated files (Dockerfile, tips.py, various tool tests) which were dropped as scope creep. Regression tests: - test_sync_nous_entry_from_auth_store_adopts_newer_tokens - test_sync_nous_entry_noop_when_tokens_match - test_nous_exhausted_entry_recovers_via_auth_store_sync
Concurrent Hermes processes (e.g. cron jobs) refreshing a Nous OAuth token via resolve_nous_runtime_credentials() write the rotated tokens to auth.json. The calling process's pool entry becomes stale, and the next refresh against the already-rotated token triggers a 'refresh token reuse' revocation on the Nous Portal. _sync_nous_entry_from_auth_store() reads auth.json under the same lock used by resolve_nous_runtime_credentials, and adopts the newer token pair before refreshing the pool entry. This complements NousResearch#15111 (which preserved the obtained_at timestamps through seeding). Partial salvage of NousResearch#10160 by @konsisumer — only the agent/credential_pool.py changes + the 3 Nous-specific regression tests. The PR also touched 10 unrelated files (Dockerfile, tips.py, various tool tests) which were dropped as scope creep. Regression tests: - test_sync_nous_entry_from_auth_store_adopts_newer_tokens - test_sync_nous_entry_noop_when_tokens_match - test_nous_exhausted_entry_recovers_via_auth_store_sync
Concurrent Hermes processes (e.g. cron jobs) refreshing a Nous OAuth token via resolve_nous_runtime_credentials() write the rotated tokens to auth.json. The calling process's pool entry becomes stale, and the next refresh against the already-rotated token triggers a 'refresh token reuse' revocation on the Nous Portal. _sync_nous_entry_from_auth_store() reads auth.json under the same lock used by resolve_nous_runtime_credentials, and adopts the newer token pair before refreshing the pool entry. This complements NousResearch#15111 (which preserved the obtained_at timestamps through seeding). Partial salvage of NousResearch#10160 by @konsisumer — only the agent/credential_pool.py changes + the 3 Nous-specific regression tests. The PR also touched 10 unrelated files (Dockerfile, tips.py, various tool tests) which were dropped as scope creep. Regression tests: - test_sync_nous_entry_from_auth_store_adopts_newer_tokens - test_sync_nous_entry_noop_when_tokens_match - test_nous_exhausted_entry_recovers_via_auth_store_sync
Concurrent Hermes processes (e.g. cron jobs) refreshing a Nous OAuth token via resolve_nous_runtime_credentials() write the rotated tokens to auth.json. The calling process's pool entry becomes stale, and the next refresh against the already-rotated token triggers a 'refresh token reuse' revocation on the Nous Portal. _sync_nous_entry_from_auth_store() reads auth.json under the same lock used by resolve_nous_runtime_credentials, and adopts the newer token pair before refreshing the pool entry. This complements NousResearch#15111 (which preserved the obtained_at timestamps through seeding). Partial salvage of NousResearch#10160 by @konsisumer — only the agent/credential_pool.py changes + the 3 Nous-specific regression tests. The PR also touched 10 unrelated files (Dockerfile, tips.py, various tool tests) which were dropped as scope creep. Regression tests: - test_sync_nous_entry_from_auth_store_adopts_newer_tokens - test_sync_nous_entry_noop_when_tokens_match - test_nous_exhausted_entry_recovers_via_auth_store_sync
Concurrent Hermes processes (e.g. cron jobs) refreshing a Nous OAuth token via resolve_nous_runtime_credentials() write the rotated tokens to auth.json. The calling process's pool entry becomes stale, and the next refresh against the already-rotated token triggers a 'refresh token reuse' revocation on the Nous Portal. _sync_nous_entry_from_auth_store() reads auth.json under the same lock used by resolve_nous_runtime_credentials, and adopts the newer token pair before refreshing the pool entry. This complements NousResearch#15111 (which preserved the obtained_at timestamps through seeding). Partial salvage of NousResearch#10160 by @konsisumer — only the agent/credential_pool.py changes + the 3 Nous-specific regression tests. The PR also touched 10 unrelated files (Dockerfile, tips.py, various tool tests) which were dropped as scope creep. Regression tests: - test_sync_nous_entry_from_auth_store_adopts_newer_tokens - test_sync_nous_entry_noop_when_tokens_match - test_nous_exhausted_entry_recovers_via_auth_store_sync
Concurrent Hermes processes (e.g. cron jobs) refreshing a Nous OAuth token via resolve_nous_runtime_credentials() write the rotated tokens to auth.json. The calling process's pool entry becomes stale, and the next refresh against the already-rotated token triggers a 'refresh token reuse' revocation on the Nous Portal. _sync_nous_entry_from_auth_store() reads auth.json under the same lock used by resolve_nous_runtime_credentials, and adopts the newer token pair before refreshing the pool entry. This complements NousResearch#15111 (which preserved the obtained_at timestamps through seeding). Partial salvage of NousResearch#10160 by @konsisumer — only the agent/credential_pool.py changes + the 3 Nous-specific regression tests. The PR also touched 10 unrelated files (Dockerfile, tips.py, various tool tests) which were dropped as scope creep. Regression tests: - test_sync_nous_entry_from_auth_store_adopts_newer_tokens - test_sync_nous_entry_noop_when_tokens_match - test_nous_exhausted_entry_recovers_via_auth_store_sync
Concurrent Hermes processes (e.g. cron jobs) refreshing a Nous OAuth token via resolve_nous_runtime_credentials() write the rotated tokens to auth.json. The calling process's pool entry becomes stale, and the next refresh against the already-rotated token triggers a 'refresh token reuse' revocation on the Nous Portal. _sync_nous_entry_from_auth_store() reads auth.json under the same lock used by resolve_nous_runtime_credentials, and adopts the newer token pair before refreshing the pool entry. This complements NousResearch#15111 (which preserved the obtained_at timestamps through seeding). Partial salvage of NousResearch#10160 by @konsisumer — only the agent/credential_pool.py changes + the 3 Nous-specific regression tests. The PR also touched 10 unrelated files (Dockerfile, tips.py, various tool tests) which were dropped as scope creep. Regression tests: - test_sync_nous_entry_from_auth_store_adopts_newer_tokens - test_sync_nous_entry_noop_when_tokens_match - test_nous_exhausted_entry_recovers_via_auth_store_sync
Concurrent Hermes processes (e.g. cron jobs) refreshing a Nous OAuth token via resolve_nous_runtime_credentials() write the rotated tokens to auth.json. The calling process's pool entry becomes stale, and the next refresh against the already-rotated token triggers a 'refresh token reuse' revocation on the Nous Portal. _sync_nous_entry_from_auth_store() reads auth.json under the same lock used by resolve_nous_runtime_credentials, and adopts the newer token pair before refreshing the pool entry. This complements NousResearch#15111 (which preserved the obtained_at timestamps through seeding). Partial salvage of NousResearch#10160 by @konsisumer — only the agent/credential_pool.py changes + the 3 Nous-specific regression tests. The PR also touched 10 unrelated files (Dockerfile, tips.py, various tool tests) which were dropped as scope creep. Regression tests: - test_sync_nous_entry_from_auth_store_adopts_newer_tokens - test_sync_nous_entry_noop_when_tokens_match - test_nous_exhausted_entry_recovers_via_auth_store_sync
Summary
_sync_nous_entry_from_auth_store()to the credential pool, mirroring the existing_sync_anthropic_entry_from_credentials_file()and_sync_codex_entry_from_cli()patternsProblem
When multiple cron jobs refresh the Nous OAuth token concurrently via the credential pool, the second process sends an already-consumed single-use refresh token. The Nous Portal detects "Refresh token reuse", revokes the entire session, and all Nous access dies until manual
hermes modelre-auth.The Anthropic and Codex providers already had cross-process sync methods that check if another process refreshed first. Nous was missing this entirely.
Test plan
test_sync_nous_entry_from_auth_store_adopts_newer_tokens— verifies token adoption when auth.json has newer tokenstest_sync_nous_entry_noop_when_tokens_match— verifies no-op when tokens are unchangedtest_nous_refresh_race_adopts_winner_tokens— verifies the race condition recovery path (refresh fails, auth.json has winner's tokens)test_nous_exhausted_entry_recovers_via_auth_store_sync— verifies exhausted entries recover when another process refreshed successfullyCloses #10147