From 5a1cce53e4b255d9fd2c9b667f33e448f18419d5 Mon Sep 17 00:00:00 2001 From: xwp Date: Fri, 10 Apr 2026 15:16:18 +0800 Subject: [PATCH] fix(auxiliary): skip anthropic in fallback chain when not explicitly configured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _resolve_api_key_provider() now checks is_provider_explicitly_configured before calling _try_anthropic(). Previously, any auxiliary fallback (e.g. when kimi-coding key was invalid) would silently discover and use Claude Code OAuth tokens — consuming the user's Claude Max subscription without their knowledge. This is the auxiliary-client counterpart of the setup-wizard gate in PR #4210. Co-Authored-By: Claude Opus 4.6 (1M context) --- agent/auxiliary_client.py | 9 ++++++ tests/agent/test_auxiliary_client.py | 42 ++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 8797926016..a7a4639784 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -687,6 +687,15 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]: if pconfig.auth_type != "api_key": continue if provider_id == "anthropic": + # Only try anthropic when the user has explicitly configured it. + # Without this gate, Claude Code credentials get silently used + # as auxiliary fallback when the user's primary provider fails. + try: + from hermes_cli.auth import is_provider_explicitly_configured + if not is_provider_explicitly_configured("anthropic"): + continue + except ImportError: + pass return _try_anthropic() pool_present, entry = _select_pool_entry(provider_id) diff --git a/tests/agent/test_auxiliary_client.py b/tests/agent/test_auxiliary_client.py index 5b2da840ce..17f4dc3c87 100644 --- a/tests/agent/test_auxiliary_client.py +++ b/tests/agent/test_auxiliary_client.py @@ -1111,3 +1111,45 @@ class TestCallLlmPaymentFallback: task="compression", messages=[{"role": "user", "content": "hello"}], ) + + +# --------------------------------------------------------------------------- +# Gate: _resolve_api_key_provider must skip anthropic when not configured +# --------------------------------------------------------------------------- + + +def test_resolve_api_key_provider_skips_unconfigured_anthropic(monkeypatch): + """_resolve_api_key_provider must not try anthropic when user never configured it.""" + from collections import OrderedDict + from hermes_cli.auth import ProviderConfig + + # Build a minimal registry with only "anthropic" so the loop is guaranteed + # to reach it without being short-circuited by earlier providers. + fake_registry = OrderedDict({ + "anthropic": ProviderConfig( + id="anthropic", + name="Anthropic", + auth_type="api_key", + inference_base_url="https://api.anthropic.com", + api_key_env_vars=("ANTHROPIC_API_KEY",), + ), + }) + + called = [] + + def mock_try_anthropic(): + called.append("anthropic") + return None, None + + monkeypatch.setattr("agent.auxiliary_client._try_anthropic", mock_try_anthropic) + monkeypatch.setattr("hermes_cli.auth.PROVIDER_REGISTRY", fake_registry) + monkeypatch.setattr( + "hermes_cli.auth.is_provider_explicitly_configured", + lambda pid: False, + ) + + from agent.auxiliary_client import _resolve_api_key_provider + _resolve_api_key_provider() + + assert "anthropic" not in called, \ + "_try_anthropic() should not be called when anthropic is not explicitly configured"