Skip to content

fix(copilot): fall back to credential_pool OAuth access_token for /model picker (#16708)#16868

Closed
briandevans wants to merge 2 commits into
NousResearch:mainfrom
briandevans:fix/copilot-catalog-oauth-fallback-16708
Closed

fix(copilot): fall back to credential_pool OAuth access_token for /model picker (#16708)#16868
briandevans wants to merge 2 commits into
NousResearch:mainfrom
briandevans:fix/copilot-catalog-oauth-fallback-16708

Conversation

@briandevans

Copy link
Copy Markdown
Contributor

Summary

  • _resolve_copilot_catalog_api_key now falls back to the OAuth access_token saved in auth.json by hermes auth add copilot (device-code flow) when env vars and gh auth token are unset.
  • Env-resolved tokens still win; pool read failures still return "" and let the picker fall through to the curated list.
  • Mirrors the Codex catalog resolver pattern (hermes_cli/models.py:1791).

The bug

Users whose only Copilot credential is the OAuth token Hermes itself stores see the /model picker drop back to a stale hardcoded list. provider_model_ids(\"copilot\") calls _resolve_copilot_catalog_api_key(), which only checks COPILOT_GITHUB_TOKEN / GH_TOKEN / GITHUB_TOKEN and gh auth token. With no token, _fetch_github_models silently 401s and the picker hides current models (claude-opus-4.7, claude-sonnet-4.6, gpt-5.5, grok-code-fast-1).

/model <id> works fine because runtime inference reads auth.json through a different code path — only the catalog fetch is broken.

The fix

hermes_cli/models.py::_resolve_copilot_catalog_api_key:

  1. Try resolve_api_key_provider_credentials(\"copilot\") first (existing behaviour).
  2. On miss, walk read_credential_pool(\"copilot\") for the first entry with a supported access token (gho_* / github_pat_* / ghu_*). Reject classic PATs (ghp_*) up-front via validate_copilot_token since the Copilot API doesn't accept them.
  3. Run the pool token through get_copilot_api_token() so catalog requests use the same exchanged token runtime inference uses.

Strictly additive: env still wins; non-dict entries, empty access_tokens, or a raising read_credential_pool all return \"\" so the caller falls through to the curated catalog (existing behaviour).

Test plan

  • New focused regression suite tests/hermes_cli/test_copilot_catalog_oauth_fallback.py (7 tests) covering: env-wins-over-pool, pool-fallback success, env-raises-still-falls-back, classic-PAT rejected, non-dict / empty / first-valid-wins iteration, no-credentials returns \"\", pool-read-raises returns \"\".
  • Adjacent suite — tests/hermes_cli/test_model_validation.py, test_copilot_in_model_list.py, test_copilot_auth.py, test_copilot_token_exchange.py — all 115 pass.
  • Regression guard: temporarily reverted hermes_cli/models.py to baseline and reran the new suite — 3 of 7 fail (test_falls_back_to_pool_oauth_token, test_falls_back_when_env_resolution_raises, test_skips_invalid_pool_entries), proving the tests exercise the new path. With the fix restored, all 7 pass.

Related

…del picker (NousResearch#16708)

Users whose only Copilot credential is the OAuth `access_token` saved by
`hermes auth add copilot` (device-code flow) saw the `/model` picker drop
back to a stale hardcoded list. Reason: `_resolve_copilot_catalog_api_key`
only consulted env vars (`COPILOT_GITHUB_TOKEN` / `GH_TOKEN` /
`GITHUB_TOKEN`) and the `gh auth token` CLI fallback, never the credential
pool that Hermes's own login flow writes into `auth.json`. With no token,
the live catalog fetch silently 401s and the picker hides current models
(claude-opus-4.7, claude-sonnet-4.6, gpt-5.5, grok-code-fast-1) — even
though `/model <id>` works fine because runtime inference reads the pool
through a different code path.

Mirror the Codex catalog resolver pattern: env-var first (unchanged), then
walk `read_credential_pool("copilot")` for the first entry with a
supported `access_token` (`gho_*` / `github_pat_*` / `ghu_*`). Run it
through `get_copilot_api_token()` so the catalog request uses the same
exchanged token the runtime path uses. Classic PATs (`ghp_*`) are still
rejected up-front via `validate_copilot_token` since the Copilot API
doesn't accept them.

Strictly additive: env still wins, and a missing/locked auth.json (or any
exception during pool read) still returns "" so the caller falls through
to the curated catalog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 28, 2026 07:13
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard provider/copilot GitHub Copilot (ACP + Chat) area/auth Authentication, OAuth, credential pools labels Apr 28, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes Copilot /model picker live-catalog discovery when the only available Copilot credential is stored in auth.json’s credential pool, by adding a fallback path to reuse pooled access_token entries (and exchange them via the Copilot token exchanger) after the existing env/gh auth token resolution misses.

Changes:

  • Updated hermes_cli.models._resolve_copilot_catalog_api_key() to fall back from env/gh-resolved credentials to read_credential_pool("copilot") and exchange the first acceptable pooled token via get_copilot_api_token().
  • Added a focused regression test suite covering env-wins, pool fallback behavior, classic PAT rejection, invalid entry skipping, and failure modes (env raises, pool read raises).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
hermes_cli/models.py Adds credential-pool fallback for Copilot catalog token resolution and exchanges pooled tokens for Copilot API use.
tests/hermes_cli/test_copilot_catalog_oauth_fallback.py New regression tests for Copilot catalog token resolution fallback behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread hermes_cli/models.py Outdated
Comment on lines +1704 to +1709
2. ``read_credential_pool("copilot")`` — OAuth ``access_token`` saved
in ``auth.json`` by ``hermes auth add copilot`` (device-code flow).

Without (2), users whose only Copilot credential is the OAuth token
Hermes itself stores see the ``/model`` picker fall back to a stale
hardcoded list because the live catalog fetch silently 401s.

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring says the OAuth access_token is saved in auth.json by hermes auth add copilot via a device-code flow, but hermes auth add for copilot currently just adds an API-key credential (it doesn’t run the device-code flow). Consider rewording to avoid implying auth add copilot performs OAuth login; it can store a gho_* token, but it doesn’t mint it.

Suggested change
2. ``read_credential_pool("copilot")``OAuth ``access_token`` saved
in ``auth.json`` by ``hermes auth add copilot`` (device-code flow).
Without (2), users whose only Copilot credential is the OAuth token
Hermes itself stores see the ``/model`` picker fall back to a stale
hardcoded list because the live catalog fetch silently 401s.
2. ``read_credential_pool("copilot")``a previously stored GitHub
OAuth ``access_token`` from ``auth.json`` (for example a ``gho_*``
token), if one is present.
Without (2), users whose only Copilot credential is a stored OAuth token
can see the ``/model`` picker fall back to a stale hardcoded list
because the live catalog fetch silently 401s.

Copilot uses AI. Check for mistakes.
Comment thread hermes_cli/models.py Outdated
Comment on lines +1731 to +1737
raw = str(entry.get("access_token") or "").strip()
if not raw:
continue
valid, _ = validate_copilot_token(raw)
if not valid:
continue
return get_copilot_api_token(raw)

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pool-walk treats any non-empty token as “valid” except ghp_* because validate_copilot_token() currently only rejects classic PATs/empty strings. That means a malformed/unsupported token in credential_pool.copilot could be selected first and prevent reaching a later valid gho_*/github_pat_*/ghu_* entry. Consider tightening the acceptance check for pool entries (e.g., enforce supported prefixes or strengthen validate_copilot_token) and add a regression test where the first pool token is unsupported but a later one is valid.

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +6
Regression for #16708: when the user's only Copilot credential is the
OAuth ``access_token`` saved in ``auth.json`` (the device-code flow that
``hermes auth add copilot`` itself produces), the picker was silently
dropping back to a stale hardcoded list because

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This module docstring states hermes auth add copilot “itself produces” the OAuth token via device-code flow, but that command path currently adds an API-key credential and doesn’t run OAuth login. Consider rephrasing to “a gho_* token (typically obtained via device-code login) stored in auth.json” to avoid confusing readers about where the token is minted.

Suggested change
Regression for #16708: when the user's only Copilot credential is the
OAuth ``access_token`` saved in ``auth.json`` (the device-code flow that
``hermes auth add copilot`` itself produces), the picker was silently
dropping back to a stale hardcoded list because
Regression for #16708: when the user's only Copilot credential is a
``gho_*`` OAuth ``access_token`` saved in ``auth.json`` (typically
obtained via device-code login), the picker was silently dropping back
to a stale hardcoded list because

Copilot uses AI. Check for mistakes.
…l catalog tokens

Address Copilot review on NousResearch#16868:

1. Tighten pool iteration. ``validate_copilot_token`` only rejects empty
   strings and classic PATs (``ghp_*``); a malformed/unsupported ``gho_*``
   token at ``credential_pool.copilot[0]`` would pass the gate and short-
   circuit the loop, hiding a later valid entry. Switch to calling
   ``exchange_copilot_token`` directly: only entries that actually exchange
   into a live Copilot API token are returned. Bad/expired entries fall
   through to the next, and an exhausted pool returns ``""`` so the picker
   falls back to the curated list (existing behaviour).

2. Reword the docstring + test module docstring to describe the pool seed
   path accurately — ``hermes auth add copilot`` adds an api-key-typed
   credential whose ``access_token`` field stores the pasted token, and
   ``_seed_from_env`` mirrors ``COPILOT_GITHUB_TOKEN`` from
   ``~/.hermes/.env`` into the pool. The previous wording implied
   ``auth add copilot`` itself ran the device-code flow, which it does
   not (the device-code flow lives in ``hermes model``).

Two new tests cover the iteration change:
  - ``test_skips_pool_entry_that_fails_to_exchange`` — pool[0] raises,
    pool[1] succeeds, picker uses pool[1].
  - ``test_all_pool_entries_fail_exchange_returns_empty`` — every entry
    raises, return ``""``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@briandevans

Copy link
Copy Markdown
Contributor Author

@copilot All three findings addressed in commit 24555cc3d:

Finding 1 + 3 (docstring inaccuracy — auth add copilot does not itself run the device-code flow): Reworded both the production docstring (hermes_cli/models.py) and the test module docstring to describe the pool-seed path accurately. The pool gets gho_*/github_pat_* tokens via two real paths: (a) hermes auth add copilot saves the pasted token under credential_pool.copilot[].access_token (it's an api-key-typed credential, not OAuth-typed — the field name is just shared), and (b) _seed_from_env mirrors COPILOT_GITHUB_TOKEN from ~/.hermes/.env into the pool. The actual device-code flow lives in hermes model and saves to .env, where _seed_from_env then promotes it to the pool.

Finding 2 (a malformed pool[0] could wedge a later valid pool[1]): Real concern. validate_copilot_token only rejects empty strings and ghp_*, so a bad gho_garbage would pass the gate and short-circuit. Tightened the iteration to call exchange_copilot_token directly and only return on successful exchange; on ValueError (e.g. HTTP 401 / 4xx from the token-exchange endpoint), continue to the next entry. If every entry fails, return "" so the picker falls back to the curated list (matching existing no-credential behaviour).

Two new tests cover the change:

  • test_skips_pool_entry_that_fails_to_exchange — pool[0] raises, pool[1] succeeds, picker uses pool[1].
  • test_all_pool_entries_fail_exchange_returns_empty — every entry raises, return "".

Full focused suite is now 9 tests; adjacent suite (test_model_validation.py, test_copilot_in_model_list.py, test_copilot_token_exchange.py) still green — 99 passing locally.

@briandevans

Copy link
Copy Markdown
Contributor Author

CI audit — all 24 test job failures on commit 24555cc3d are pre-existing baselines on clean origin/main (8081425a1). Zero failures intersect with touched code (hermes_cli/models.py::_resolve_copilot_catalog_api_key + tests/hermes_cli/test_copilot_catalog_oauth_fallback.py).

Failure set is the same one observable on other open PRs against 8081425a1 (e.g. #16851, #16786, #16770) — test_anthropic_adapter::test_custom_base_url, test_copilot_acp_client::test_read_text_file_redacts_sensitive_content, test_config_env_expansion::*, test_session_split_brain_11016::* (timeouts), test_clipboard::TestIsWsl::*, test_make_agent_provider::test_make_agent_passes_resolved_provider, test_protocol::test_session_resume_returns_hydrated_messages, etc. Locally on this branch the focused suite (9 tests) and adjacent suite (test_model_validation.py + test_copilot_in_model_list.py + test_copilot_token_exchange.py + test_copilot_auth.py, 99 tests total) are green.

teknium1 pushed a commit that referenced this pull request Apr 28, 2026
…l catalog tokens

Address Copilot review on #16868:

1. Tighten pool iteration. ``validate_copilot_token`` only rejects empty
   strings and classic PATs (``ghp_*``); a malformed/unsupported ``gho_*``
   token at ``credential_pool.copilot[0]`` would pass the gate and short-
   circuit the loop, hiding a later valid entry. Switch to calling
   ``exchange_copilot_token`` directly: only entries that actually exchange
   into a live Copilot API token are returned. Bad/expired entries fall
   through to the next, and an exhausted pool returns ``""`` so the picker
   falls back to the curated list (existing behaviour).

2. Reword the docstring + test module docstring to describe the pool seed
   path accurately — ``hermes auth add copilot`` adds an api-key-typed
   credential whose ``access_token`` field stores the pasted token, and
   ``_seed_from_env`` mirrors ``COPILOT_GITHUB_TOKEN`` from
   ``~/.hermes/.env`` into the pool. The previous wording implied
   ``auth add copilot`` itself ran the device-code flow, which it does
   not (the device-code flow lives in ``hermes model``).

Two new tests cover the iteration change:
  - ``test_skips_pool_entry_that_fails_to_exchange`` — pool[0] raises,
    pool[1] succeeds, picker uses pool[1].
  - ``test_all_pool_entries_fail_exchange_returns_empty`` — every entry
    raises, return ``""``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
teknium1 pushed a commit that referenced this pull request Apr 28, 2026
…l catalog tokens

Address Copilot review on #16868:

1. Tighten pool iteration. ``validate_copilot_token`` only rejects empty
   strings and classic PATs (``ghp_*``); a malformed/unsupported ``gho_*``
   token at ``credential_pool.copilot[0]`` would pass the gate and short-
   circuit the loop, hiding a later valid entry. Switch to calling
   ``exchange_copilot_token`` directly: only entries that actually exchange
   into a live Copilot API token are returned. Bad/expired entries fall
   through to the next, and an exhausted pool returns ``""`` so the picker
   falls back to the curated list (existing behaviour).

2. Reword the docstring + test module docstring to describe the pool seed
   path accurately — ``hermes auth add copilot`` adds an api-key-typed
   credential whose ``access_token`` field stores the pasted token, and
   ``_seed_from_env`` mirrors ``COPILOT_GITHUB_TOKEN`` from
   ``~/.hermes/.env`` into the pool. The previous wording implied
   ``auth add copilot`` itself ran the device-code flow, which it does
   not (the device-code flow lives in ``hermes model``).

Two new tests cover the iteration change:
  - ``test_skips_pool_entry_that_fails_to_exchange`` — pool[0] raises,
    pool[1] succeeds, picker uses pool[1].
  - ``test_all_pool_entries_fail_exchange_returns_empty`` — every entry
    raises, return ``""``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@teknium1

Copy link
Copy Markdown
Contributor

Merged via PR #16901. Your commits were cherry-picked onto current main with your authorship preserved in git log — both commits show as yours. Thanks for the fix (and the follow-up catching the skip-and-try-next edge case)!

@teknium1 teknium1 closed this Apr 28, 2026
cluricaun28 referenced this pull request in cluricaun28/Logos Apr 28, 2026
…l catalog tokens

Address Copilot review on #16868:

1. Tighten pool iteration. ``validate_copilot_token`` only rejects empty
   strings and classic PATs (``ghp_*``); a malformed/unsupported ``gho_*``
   token at ``credential_pool.copilot[0]`` would pass the gate and short-
   circuit the loop, hiding a later valid entry. Switch to calling
   ``exchange_copilot_token`` directly: only entries that actually exchange
   into a live Copilot API token are returned. Bad/expired entries fall
   through to the next, and an exhausted pool returns ``""`` so the picker
   falls back to the curated list (existing behaviour).

2. Reword the docstring + test module docstring to describe the pool seed
   path accurately — ``hermes auth add copilot`` adds an api-key-typed
   credential whose ``access_token`` field stores the pasted token, and
   ``_seed_from_env`` mirrors ``COPILOT_GITHUB_TOKEN`` from
   ``~/.hermes/.env`` into the pool. The previous wording implied
   ``auth add copilot`` itself ran the device-code flow, which it does
   not (the device-code flow lives in ``hermes model``).

Two new tests cover the iteration change:
  - ``test_skips_pool_entry_that_fails_to_exchange`` — pool[0] raises,
    pool[1] succeeds, picker uses pool[1].
  - ``test_all_pool_entries_fail_exchange_returns_empty`` — every entry
    raises, return ``""``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ulasbilgen pushed a commit to ulasbilgen/hermes-adhd-agent that referenced this pull request May 1, 2026
…l catalog tokens

Address Copilot review on NousResearch#16868:

1. Tighten pool iteration. ``validate_copilot_token`` only rejects empty
   strings and classic PATs (``ghp_*``); a malformed/unsupported ``gho_*``
   token at ``credential_pool.copilot[0]`` would pass the gate and short-
   circuit the loop, hiding a later valid entry. Switch to calling
   ``exchange_copilot_token`` directly: only entries that actually exchange
   into a live Copilot API token are returned. Bad/expired entries fall
   through to the next, and an exhausted pool returns ``""`` so the picker
   falls back to the curated list (existing behaviour).

2. Reword the docstring + test module docstring to describe the pool seed
   path accurately — ``hermes auth add copilot`` adds an api-key-typed
   credential whose ``access_token`` field stores the pasted token, and
   ``_seed_from_env`` mirrors ``COPILOT_GITHUB_TOKEN`` from
   ``~/.hermes/.env`` into the pool. The previous wording implied
   ``auth add copilot`` itself ran the device-code flow, which it does
   not (the device-code flow lives in ``hermes model``).

Two new tests cover the iteration change:
  - ``test_skips_pool_entry_that_fails_to_exchange`` — pool[0] raises,
    pool[1] succeeds, picker uses pool[1].
  - ``test_all_pool_entries_fail_exchange_returns_empty`` — every entry
    raises, return ``""``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
donald131 pushed a commit to donald131/hermes-agent that referenced this pull request May 2, 2026
…l catalog tokens

Address Copilot review on NousResearch#16868:

1. Tighten pool iteration. ``validate_copilot_token`` only rejects empty
   strings and classic PATs (``ghp_*``); a malformed/unsupported ``gho_*``
   token at ``credential_pool.copilot[0]`` would pass the gate and short-
   circuit the loop, hiding a later valid entry. Switch to calling
   ``exchange_copilot_token`` directly: only entries that actually exchange
   into a live Copilot API token are returned. Bad/expired entries fall
   through to the next, and an exhausted pool returns ``""`` so the picker
   falls back to the curated list (existing behaviour).

2. Reword the docstring + test module docstring to describe the pool seed
   path accurately — ``hermes auth add copilot`` adds an api-key-typed
   credential whose ``access_token`` field stores the pasted token, and
   ``_seed_from_env`` mirrors ``COPILOT_GITHUB_TOKEN`` from
   ``~/.hermes/.env`` into the pool. The previous wording implied
   ``auth add copilot`` itself ran the device-code flow, which it does
   not (the device-code flow lives in ``hermes model``).

Two new tests cover the iteration change:
  - ``test_skips_pool_entry_that_fails_to_exchange`` — pool[0] raises,
    pool[1] succeeds, picker uses pool[1].
  - ``test_all_pool_entries_fail_exchange_returns_empty`` — every entry
    raises, return ``""``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
02356abc pushed a commit to 02356abc/hermes-agent that referenced this pull request May 14, 2026
…l catalog tokens

Address Copilot review on NousResearch#16868:

1. Tighten pool iteration. ``validate_copilot_token`` only rejects empty
   strings and classic PATs (``ghp_*``); a malformed/unsupported ``gho_*``
   token at ``credential_pool.copilot[0]`` would pass the gate and short-
   circuit the loop, hiding a later valid entry. Switch to calling
   ``exchange_copilot_token`` directly: only entries that actually exchange
   into a live Copilot API token are returned. Bad/expired entries fall
   through to the next, and an exhausted pool returns ``""`` so the picker
   falls back to the curated list (existing behaviour).

2. Reword the docstring + test module docstring to describe the pool seed
   path accurately — ``hermes auth add copilot`` adds an api-key-typed
   credential whose ``access_token`` field stores the pasted token, and
   ``_seed_from_env`` mirrors ``COPILOT_GITHUB_TOKEN`` from
   ``~/.hermes/.env`` into the pool. The previous wording implied
   ``auth add copilot`` itself ran the device-code flow, which it does
   not (the device-code flow lives in ``hermes model``).

Two new tests cover the iteration change:
  - ``test_skips_pool_entry_that_fails_to_exchange`` — pool[0] raises,
    pool[1] succeeds, picker uses pool[1].
  - ``test_all_pool_entries_fail_exchange_returns_empty`` — every entry
    raises, return ``""``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dannyJ848 pushed a commit to dannyJ848/hermes-agent that referenced this pull request May 17, 2026
…l catalog tokens

Address Copilot review on NousResearch#16868:

1. Tighten pool iteration. ``validate_copilot_token`` only rejects empty
   strings and classic PATs (``ghp_*``); a malformed/unsupported ``gho_*``
   token at ``credential_pool.copilot[0]`` would pass the gate and short-
   circuit the loop, hiding a later valid entry. Switch to calling
   ``exchange_copilot_token`` directly: only entries that actually exchange
   into a live Copilot API token are returned. Bad/expired entries fall
   through to the next, and an exhausted pool returns ``""`` so the picker
   falls back to the curated list (existing behaviour).

2. Reword the docstring + test module docstring to describe the pool seed
   path accurately — ``hermes auth add copilot`` adds an api-key-typed
   credential whose ``access_token`` field stores the pasted token, and
   ``_seed_from_env`` mirrors ``COPILOT_GITHUB_TOKEN`` from
   ``~/.hermes/.env`` into the pool. The previous wording implied
   ``auth add copilot`` itself ran the device-code flow, which it does
   not (the device-code flow lives in ``hermes model``).

Two new tests cover the iteration change:
  - ``test_skips_pool_entry_that_fails_to_exchange`` — pool[0] raises,
    pool[1] succeeds, picker uses pool[1].
  - ``test_all_pool_entries_fail_exchange_returns_empty`` — every entry
    raises, return ``""``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
…l catalog tokens

Address Copilot review on NousResearch#16868:

1. Tighten pool iteration. ``validate_copilot_token`` only rejects empty
   strings and classic PATs (``ghp_*``); a malformed/unsupported ``gho_*``
   token at ``credential_pool.copilot[0]`` would pass the gate and short-
   circuit the loop, hiding a later valid entry. Switch to calling
   ``exchange_copilot_token`` directly: only entries that actually exchange
   into a live Copilot API token are returned. Bad/expired entries fall
   through to the next, and an exhausted pool returns ``""`` so the picker
   falls back to the curated list (existing behaviour).

2. Reword the docstring + test module docstring to describe the pool seed
   path accurately — ``hermes auth add copilot`` adds an api-key-typed
   credential whose ``access_token`` field stores the pasted token, and
   ``_seed_from_env`` mirrors ``COPILOT_GITHUB_TOKEN`` from
   ``~/.hermes/.env`` into the pool. The previous wording implied
   ``auth add copilot`` itself ran the device-code flow, which it does
   not (the device-code flow lives in ``hermes model``).

Two new tests cover the iteration change:
  - ``test_skips_pool_entry_that_fails_to_exchange`` — pool[0] raises,
    pool[1] succeeds, picker uses pool[1].
  - ``test_all_pool_entries_fail_exchange_returns_empty`` — every entry
    raises, return ``""``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Egavasyug pushed a commit to Egavasyug/hermes-agent that referenced this pull request Jun 10, 2026
…l catalog tokens

Address Copilot review on NousResearch#16868:

1. Tighten pool iteration. ``validate_copilot_token`` only rejects empty
   strings and classic PATs (``ghp_*``); a malformed/unsupported ``gho_*``
   token at ``credential_pool.copilot[0]`` would pass the gate and short-
   circuit the loop, hiding a later valid entry. Switch to calling
   ``exchange_copilot_token`` directly: only entries that actually exchange
   into a live Copilot API token are returned. Bad/expired entries fall
   through to the next, and an exhausted pool returns ``""`` so the picker
   falls back to the curated list (existing behaviour).

2. Reword the docstring + test module docstring to describe the pool seed
   path accurately — ``hermes auth add copilot`` adds an api-key-typed
   credential whose ``access_token`` field stores the pasted token, and
   ``_seed_from_env`` mirrors ``COPILOT_GITHUB_TOKEN`` from
   ``~/.hermes/.env`` into the pool. The previous wording implied
   ``auth add copilot`` itself ran the device-code flow, which it does
   not (the device-code flow lives in ``hermes model``).

Two new tests cover the iteration change:
  - ``test_skips_pool_entry_that_fails_to_exchange`` — pool[0] raises,
    pool[1] succeeds, picker uses pool[1].
  - ``test_all_pool_entries_fail_exchange_returns_empty`` — every entry
    raises, return ``""``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists provider/copilot GitHub Copilot (ACP + Chat) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Copilot /model picker should fall back to OAuth access_token from auth.json

4 participants