fix(copilot): require successful exchange when walking credential_pool 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>
This commit is contained in:
briandevans
2026-04-28 00:42:37 -07:00
committed by Teknium
parent fdfe40a48b
commit 66a05e44d6
2 changed files with 82 additions and 25 deletions

View File

@@ -1706,12 +1706,18 @@ def _resolve_copilot_catalog_api_key() -> str:
1. ``resolve_api_key_provider_credentials("copilot")`` — env vars
(``COPILOT_GITHUB_TOKEN`` / ``GH_TOKEN`` / ``GITHUB_TOKEN``) plus
the ``gh auth token`` CLI fallback.
2. ``read_credential_pool("copilot")`` — OAuth ``access_token`` saved
in ``auth.json`` by ``hermes auth add copilot`` (device-code flow).
2. ``read_credential_pool("copilot")`` — a token (typically a
``gho_*`` from device-code login, or a fine-grained PAT) stored in
``auth.json`` under ``credential_pool.copilot[]``. The pool is
populated by ``hermes auth add copilot`` and by ``_seed_from_env``
when the env var is set in ``~/.hermes/.env``.
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.
Without (2), users whose only Copilot credential is in the pool see
the ``/model`` picker fall back to a stale hardcoded list because the
live catalog fetch silently 401s. To avoid wedging on a malformed pool
entry, each candidate is exchanged via ``exchange_copilot_token`` —
only entries that actually exchange successfully are returned, so a
later valid entry is reachable when an earlier one is unsupported.
"""
try:
from hermes_cli.auth import resolve_api_key_provider_credentials
@@ -1726,7 +1732,7 @@ def _resolve_copilot_catalog_api_key() -> str:
try:
from hermes_cli.auth import read_credential_pool
from hermes_cli.copilot_auth import (
get_copilot_api_token,
exchange_copilot_token,
validate_copilot_token,
)
@@ -1739,7 +1745,12 @@ def _resolve_copilot_catalog_api_key() -> str:
valid, _ = validate_copilot_token(raw)
if not valid:
continue
return get_copilot_api_token(raw)
try:
api_token, _expires_at = exchange_copilot_token(raw)
except Exception:
continue
if api_token:
return api_token
except Exception:
pass