Skip to content

fix(anthropic): consult credential_pool in resolve_anthropic_token (#26344) - #26356

Closed
briandevans wants to merge 1 commit into
NousResearch:mainfrom
briandevans:fix/cron-anthropic-credential-pool-26344
Closed

fix(anthropic): consult credential_pool in resolve_anthropic_token (#26344)#26356
briandevans wants to merge 1 commit into
NousResearch:mainfrom
briandevans:fix/cron-anthropic-credential-pool-26344

Conversation

@briandevans

Copy link
Copy Markdown
Contributor

Summary

  • hermes auth add anthropic --type oauth (PKCE) stores its token only in the credential_pool. resolve_anthropic_token() never consulted that pool, so cron jobs (and any other call site that doesn't go through CredentialPool directly) raised AuthError: No Anthropic credentials found even when hermes auth status anthropic reported logged in.
  • Adds a 4th source between Claude Code creds and the legacy ANTHROPIC_API_KEY fallback that reads read_credential_pool(\"anthropic\") and returns the first usable access_token, skipping entries in exhaustion cooldown.

The bug

resolve_anthropic_token() at agent/anthropic_adapter.py:953 walked four sources:

  1. ANTHROPIC_TOKEN env var
  2. CLAUDE_CODE_OAUTH_TOKEN env var
  3. ~/.claude/.credentials.json (Claude Code OAuth file)
  4. ANTHROPIC_API_KEY env var

The Hermes-native PKCE flow (`hermes auth add anthropic --type oauth`) does not write to any of those — run_hermes_oauth_login_pure() returns the credential dict, and auth_commands.py stores it as a PooledCredential with source=\"manual:hermes_pkce\" directly into ~/.hermes/auth.json::credential_pool[\"anthropic\"][0].

For runtime paths that resolve credentials via resolve_anthropic_token() rather than CredentialPool.select() — cron via hermes_cli/runtime_provider.py:830, the auxiliary client at agent/auxiliary_client.py:1801/2423, hermes_cli/models.py:2307, agent/account_usage.py:176 — the token is invisible. The cron failure trace from the issue:

```
cron/scheduler.py:1281 run_job
→ hermes_cli/runtime_provider.py:1194 resolve_runtime_provider
→ agent/anthropic_adapter.py raise AuthError("No Anthropic credentials found…")
```

The fix

Add a 4th step that consults the credential pool. New helper _resolve_anthropic_token_from_pool():

  • Reads read_credential_pool(\"anthropic\") raw (no CredentialPool instantiation — keeps it cheap and side-effect-free; rotation/refresh/heartbeat still belong to CredentialPool.select()).
  • Skips entries with last_status == \"exhausted\" whose last_error_reset_at is still in the future, so the resolver doesn't hand back a token the pool already knows is rate-limited.
  • Wraps the read in a try/except so the test seat belt in _auth_file_path, malformed-JSON, missing-file, and any other failure degrade cleanly to "no pool token" rather than crashing the resolver.

The new step is purely additive — env-var and Claude Code paths still win in their original priority order, so the existing test matrix (env precedence, refreshable-Claude preference) is unchanged.

Test plan

  • Focused regression test: tests/agent/test_anthropic_adapter.py::TestResolveAnthropicToken::test_falls_back_to_credential_pool_when_only_hermes_pkce_exists — reproduces the cron scenario (no env vars, no ~/.claude/, only a manual:hermes_pkce entry in the pool) and asserts the token is returned.
  • Edge case: test_pool_lookup_skips_exhausted_entries — pool with one exhausted-with-future-reset entry and one fresh entry returns the fresh one.
  • Priority guard: test_env_anthropic_token_still_wins_over_poolANTHROPIC_TOKEN env wins over a pool entry, so users can override without editing auth.json.
  • Adjacent suite: tests/agent/test_credential_pool.py (54 passed) and tests/hermes_cli/test_auth_profile_fallback.py — confirm pool reads still work under profile + global fallback.
  • Regression guard: removing the new _resolve_anthropic_token_from_pool call makes test_falls_back_to_credential_pool_when_only_hermes_pkce_exists return None — proving the test exercises the new code path, not a coincidence.

Note: 2 existing tests (test_falls_back_to_claude_code_credentials, test_prefers_refreshable_claude_code_credentials_over_static_anthropic_token) fail on my macOS dev box because read_claude_code_credentials() hits the real Keychain before the file fixture. They fail on origin/main without my changes (verified via git stash) and pass on Linux CI.

Related

Sibling code paths that may need the same fix: none material — all callers of resolve_anthropic_token() (auxiliary client, model picker, account-usage, runtime_provider) transitively benefit from this fix. Happy to widen if a different angle is preferred.

…ousResearch#26344)

`hermes auth add anthropic --type oauth` (PKCE) stores its token only in
`~/.hermes/auth.json::credential_pool["anthropic"]` — not in env vars
and not in `~/.claude/.credentials.json`. `resolve_anthropic_token()`
walked four sources (ANTHROPIC_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, Claude
Code creds file, ANTHROPIC_API_KEY) but never consulted the pool, so
cron jobs that route through `resolve_runtime_provider` raised
`AuthError: No Anthropic credentials found` even after `hermes auth
status anthropic` reported `logged in`.

Add a 4th step that reads the pool via `read_credential_pool("anthropic")`
and returns the first entry's `access_token`, skipping entries currently
in exhaustion cooldown (so the resolver doesn't hand back a token the
pool already knows is rate-limited). Rotation/refresh/heartbeat
semantics still belong to `CredentialPool.select()` — this is a
lightweight read-only fallback for callers that only need a single
token string (auxiliary client fallback, cron, account-usage probe,
model picker).

The new step is purely additive: env-var and Claude Code paths still
win in their original priority order. Pool lookup wraps the read in
try/except so the test seat belt in `_auth_file_path` and any
malformed-JSON / missing-file case degrades cleanly to "no token"
rather than crashing the caller.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 15, 2026 13:23

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

Note

Copilot was unable to run its full agentic suite in this review.

Adds a credential-pool fallback in resolve_anthropic_token() so tokens stored only via hermes auth add anthropic (especially PKCE OAuth) are resolvable from contexts that don't go through CredentialPool (cron jobs, debug commands).

Changes:

  • New helper _resolve_anthropic_token_from_pool() reads ~/.hermes/auth.json credential pool, returns the first usable access token, skipping entries in exhaustion cooldown.
  • resolve_anthropic_token() consults the pool as priority 4, ahead of the legacy ANTHROPIC_API_KEY fallback; docstring updated.
  • New tests cover PKCE-only fallback, exhausted-entry skip, and env-var precedence.

Reviewed changes

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

File Description
agent/anthropic_adapter.py Adds pool lookup helper and wires it into the resolver priority chain.
tests/agent/test_anthropic_adapter.py Adds three regression tests for pool fallback, exhaustion skip, and env precedence.

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

Comment on lines +981 to +994
for entry in entries:
if not isinstance(entry, dict):
continue
token = (entry.get("access_token") or "").strip()
if not token:
continue
# Skip entries currently in exhaustion cooldown — pool selection
# would also skip them, and returning an exhausted token would
# produce a misleading 401 rather than a clean "no creds" error.
if entry.get("last_status") == "exhausted":
reset_at = entry.get("last_error_reset_at")
if isinstance(reset_at, (int, float)) and reset_at > now_s:
continue
return token
Comment on lines +979 to +980
import time as _time
now_s = _time.time()
Comment on lines +966 to +973
for callers that only need a single token string. Returns None on any
error (pool unavailable, malformed entries, test seat belt tripped).
"""
try:
from hermes_cli.auth import read_credential_pool
entries = read_credential_pool("anthropic")
except Exception as e:
logger.debug("Credential pool lookup failed: %s", e)
@briandevans

Copy link
Copy Markdown
Contributor Author

CI audit — all 7 test failures are pre-existing baselines on clean origin/main (9fb40e6a3). Zero failures are in touched code (agent/auth/anthropic.py).

Test Symptom Root cause on main
test_provider_parity.py::TestDeveloperRoleSwap::test_developer_role_via_nous_portal ValueError: Model has a context window of 15,000 tokens, which is below the minimum 64,000 Test constructs AIAgent(provider="nous", base_url="https://inference-api.nousresearch.com/v1") — the unmocked context-length probe caches 15,000 and trips the new 64K minimum guard in run_agent.py:2349.
test_provider_parity.py::TestBuildApiKwargsNousPortal::test_includes_nous_product_tags same same
test_provider_parity.py::TestBuildApiKwargsNousPortal::test_uses_chat_completions_format same same
test_discord_adapter.py::TestMentionStrippedCommandDispatch::test_mention_then_command TypeError: catching classes that do not inherit from BaseException is not allowed gateway/platforms/discord.py:3730 does except discord.Forbidden:, but the e2e harness stubs discord as a SimpleNamespace, so discord.Forbidden resolves to a non-exception sentinel.
test_discord_adapter.py::TestMentionStrippedCommandDispatch::test_nickname_mention_then_command same same
test_discord_adapter.py::TestMentionStrippedCommandDispatch::test_text_before_command_not_detected same same
test_discord_adapter.py::TestAutoThreadingPreservesCommand::test_command_detected_after_auto_thread same same

All seven reproduce on clean origin/main locally with identical error text. Already covered upstream by #26312 / #26048.

@alt-glitch

Copy link
Copy Markdown
Collaborator

Likely duplicate of #26351 — both add credential_pool lookup to resolve_anthropic_token() to fix #26344. Same bug, same fix approach (insert pool read between Claude Code creds and ANTHROPIC_API_KEY fallback).

@alt-glitch alt-glitch added P1 High — major feature broken, no workaround type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cron Cron scheduler and job management area/auth Authentication, OAuth, credential pools provider/anthropic Anthropic native Messages API labels May 15, 2026
@briandevans

Copy link
Copy Markdown
Contributor Author

Closing — superseded by @LeonSGP43's #26351, which was opened 11 minutes earlier and covers the same root cause (consult Anthropic credential_pool in resolve_anthropic_token() so cron/runtime can resolve PKCE-OAuth tokens stored via hermes auth add anthropic). Their implementation uses the canonical agent.credential_pool.load_pool / _available_entries API, which is the right entrypoint here. Thanks @LeonSGP43!

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/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cron Cron scheduler and job management P1 High — major feature broken, no workaround provider/anthropic Anthropic native Messages API type/bug Something isn't working

Projects

None yet

3 participants