fix(run_agent): refresh Copilot token and base_url together during credential rotation - #61754
fix(run_agent): refresh Copilot token and base_url together during credential rotation#61754liuhao1024 wants to merge 2 commits into
Conversation
…edential rotation When rotating to a Copilot credential pool entry, perform a live token exchange to fetch both the API token and base_url from a single exchange result. This prevents pairing a valid Enterprise credential with a stale public endpoint (https://api.githubcopilot.com), which causes HTTP 403 and incorrectly marks the credential exhausted. Fixes NousResearch#61746
Add regression tests for NousResearch#61746: - test_copilot_swap_refreshes_token_and_base_url: verifies that Copilot rotation fetches both token and base_url from a live exchange - test_copilot_swap_falls_back_on_exchange_failure: verifies fallback behavior when the exchange fails - test_non_copilot_swap_does_not_call_exchange: verifies non-Copilot providers are unaffected
Core fix for #61746. Complementary/competing with #61757 (which builds on this to also cover the already-exchanged-token endpoint path and the |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for targeting the current credential-rotation site; the stale-entry premise is still present in run_agent.py:4505-4532.
Problems
- The added
get_copilot_api_token(entry.access_token)call receives an already-exchanged API token in normal Copilot pool operation: seeding storesapi_tokeninaccess_token(agent/credential_pool.py:1931-1949). - When that re-exchange fails,
get_copilot_api_token()catches the error and returns(raw_token, None)rather than raising (hermes_cli/copilot_auth.py:415-434). The new assignment would therefore clearruntime_base, not fall back to the selected entry as the comment claims. - The added failure test mocks an exception from the wrapper, so it does not exercise that real fail-soft tuple contract.
Suggested changes
- Handle raw and already-exchanged Copilot tokens separately, preserving or deriving the selected endpoint for an exchanged token; preserve the existing runtime endpoint when resolution supplies none.
- Add regression coverage for the helper returning
(existing_api_token, None).
This is an automated hermes-sweeper review.
| from hermes_cli.copilot_auth import get_copilot_api_token | ||
| runtime_key, runtime_base = get_copilot_api_token(entry.access_token or "") | ||
| except Exception: | ||
| # Fall back to the existing entry values if exchange fails. |
There was a problem hiding this comment.
entry.access_token is normally already the exchanged Copilot API token (agent/credential_pool.py:1931-1949). Re-exchange failure is swallowed by get_copilot_api_token() and returns (same_token, None) (hermes_cli/copilot_auth.py:415-434), so this assignment clears the selected endpoint instead of reaching the except fallback. Preserve/derive the selected endpoint for exchanged tokens.
|
This is a critical fix that #62467's unified resolver handles at the architectural level. What this PR fixes: Copilot token refresh doesn't update How #62467 prevents this class of bug: The unified resolver Additionally, the resolver's source labeling ( Copilot is covered under Category C (generic API-key with OAuth awareness) in the unified resolver. If this PR's token-refresh fix merges, the resolver will consume the refreshed Complementary — both fixes are needed for fully correct Copilot Enterprise support. |
What does this PR do?
Fixes a Copilot credential rotation bug where a valid Enterprise credential could be marked exhausted after a single 403 error, even though it succeeds on a fresh turn.
Root cause: when rotating to a Copilot pool entry,
_swap_credential()trusted the entry's persistedbase_url(which might still carry the old public defaulthttps://api.githubcopilot.com). The subsequent request sent the Enterprise API token to the public endpoint, received HTTP 403, and the credential was marked exhausted. A new agent turn succeeds because normal initialization performs a live token exchange and receives the account-specific Enterprise endpoint.This change makes Copilot credential rotation perform a live token exchange at rotation time, ensuring the API token and
base_urlare updated together from a single exchange result before rebuilding the client.Related Issue
Fixes #61746
Type of Change
Changes Made
run_agent.py::_swap_credential(): Forprovider == "copilot", callget_copilot_api_token()to refresh both the API token and base_url from a live exchange before updating the client.tests/agent/test_credential_pool_routing.py::TestCopilotCredentialRefresh: Add 3 regression tests for the Copilot credential rotation behavior.How to Test
pytest tests/agent/test_credential_pool_routing.py::TestCopilotCredentialRefresh -v. Expected result: All 3 tests pass.pytest tests/agent/test_credential_pool_routing.py -v. Expected result: All 13 tests pass.base_url(e.g.,https://api.githubcopilot.com) while the second entry is fresh. Trigger credential rotation (e.g., by sending a malformed request that causes a 403). Observed result: The rotated credential now fetches both the API token and base_url from a live token exchange, so the request goes to the Enterprise endpoint (https://api.enterprise.githubcopilot.com) and succeeds with HTTP 200 instead of failing with HTTP 403 on the stale public endpoint.Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/agent/test_credential_pool_routing.pyand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/A