diff --git a/agent/credential_pool.py b/agent/credential_pool.py index c4905fc3f5..e5127ad7dd 100644 --- a/agent/credential_pool.py +++ b/agent/credential_pool.py @@ -1152,6 +1152,30 @@ def _seed_from_singletons(provider: str, entries: List[PooledCredential]) -> Tup }, ) + elif provider == "copilot": + # Copilot tokens are resolved dynamically via `gh auth token` or + # env vars (COPILOT_GITHUB_TOKEN / GH_TOKEN). They don't live in + # the auth store or credential pool, so we resolve them here. + try: + from hermes_cli.copilot_auth import resolve_copilot_token + token, source = resolve_copilot_token() + if token: + source_name = "gh_cli" if "gh" in source.lower() else f"env:{source}" + active_sources.add(source_name) + changed |= _upsert_entry( + entries, + provider, + source_name, + { + "source": source_name, + "auth_type": AUTH_TYPE_API_KEY, + "access_token": token, + "label": source, + }, + ) + except Exception as exc: + logger.debug("Copilot token seed failed: %s", exc) + elif provider == "openai-codex": state = _load_provider_state(auth_store, "openai-codex") tokens = state.get("tokens") if isinstance(state, dict) else None diff --git a/tests/agent/test_credential_pool.py b/tests/agent/test_credential_pool.py index de6ffba5c5..466d921534 100644 --- a/tests/agent/test_credential_pool.py +++ b/tests/agent/test_credential_pool.py @@ -1071,3 +1071,40 @@ def test_load_pool_does_not_seed_claude_code_when_anthropic_not_configured(tmp_p # Should NOT have seeded the claude_code entry assert pool.entries() == [] + + +def test_load_pool_seeds_copilot_via_gh_auth_token(tmp_path, monkeypatch): + """Copilot credentials from `gh auth token` should be seeded into the pool.""" + monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes")) + _write_auth_store(tmp_path, {"version": 1, "credential_pool": {}}) + + monkeypatch.setattr( + "hermes_cli.copilot_auth.resolve_copilot_token", + lambda: ("gho_fake_token_abc123", "gh auth token"), + ) + + from agent.credential_pool import load_pool + pool = load_pool("copilot") + + assert pool.has_credentials() + entries = pool.entries() + assert len(entries) == 1 + assert entries[0].source == "gh_cli" + assert entries[0].access_token == "gho_fake_token_abc123" + + +def test_load_pool_does_not_seed_copilot_when_no_token(tmp_path, monkeypatch): + """Copilot pool should be empty when resolve_copilot_token() returns nothing.""" + monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes")) + _write_auth_store(tmp_path, {"version": 1, "credential_pool": {}}) + + monkeypatch.setattr( + "hermes_cli.copilot_auth.resolve_copilot_token", + lambda: ("", ""), + ) + + from agent.credential_pool import load_pool + pool = load_pool("copilot") + + assert not pool.has_credentials() + assert pool.entries() == []