Skip to content

Commit 819585c

Browse files
committed
fix(claude-code): guard unrecognized authMethod from legacy fallback
Future authMethod values (e.g. oauth/sso) emitted alongside a populated apiKeySource were skipping both explicit branches and landing on the legacy heuristic, mis-reporting the env var as the active source — the same kind of mis-attribution this PR fixes for claude.ai. Surface the authMethod verbatim instead. Adds the missing stderr-only-JSON contract test so a future stderr-parsing refactor cannot silently change _probe_cli_auth's return value.
1 parent f0bfcf2 commit 819585c

2 files changed

Lines changed: 53 additions & 0 deletions

File tree

app/integrations/llm_cli/claude_code.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ def _auth_status_from_json_payload(data: dict) -> tuple[bool, str]:
9797
return True, f"Authenticated via {source}."
9898
if auth_method == "claude.ai":
9999
return True, f"Authenticated via Claude subscription{f' ({email})' if email else ''}."
100+
if auth_method:
101+
# Unrecognized but non-empty authMethod (e.g. a future "oauth" / "sso"):
102+
# surface it verbatim instead of leaning on apiKeySource, which the CLI
103+
# also populates for env-supplied API keys regardless of the active
104+
# method and would mis-report the source.
105+
return True, f"Authenticated via {auth_method}{f' ({email})' if email else ''}."
100106
# Older CLI versions may omit authMethod — fall back to legacy heuristic.
101107
if api_key_source:
102108
return True, f"Authenticated via {api_key_source}."

tests/integrations/llm_cli/test_claude_code_adapter.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,53 @@ def test_probe_cli_auth_api_key_only_uses_authmethod(mock_run: MagicMock) -> Non
247247
assert "ANTHROPIC_API_KEY" in detail
248248

249249

250+
@patch("app.integrations.llm_cli.claude_code.subprocess.run")
251+
def test_probe_cli_auth_unrecognized_authmethod_does_not_use_apikeysource(
252+
mock_run: MagicMock,
253+
) -> None:
254+
"""A future authMethod (e.g. ``oauth``) must not fall through to apiKeySource.
255+
256+
The CLI populates ``apiKeySource`` whenever the env contributes an API key,
257+
so a logged-in subscription/OAuth user with ``ANTHROPIC_API_KEY`` set would
258+
otherwise be reported as "Authenticated via ANTHROPIC_API_KEY" — the exact
259+
mis-reporting the legacy heuristic produced for ``claude.ai``.
260+
"""
261+
m = MagicMock()
262+
m.returncode = 0
263+
m.stdout = (
264+
'{"loggedIn": true, "authMethod": "oauth", '
265+
'"apiKeySource": "ANTHROPIC_API_KEY", "email": "[email protected]"}\n'
266+
)
267+
m.stderr = ""
268+
mock_run.return_value = m
269+
logged_in, detail = _probe_cli_auth("/usr/bin/claude")
270+
assert logged_in is True
271+
assert "ANTHROPIC_API_KEY" not in detail
272+
assert "oauth" in detail
273+
assert "[email protected]" in detail
274+
275+
276+
@patch("app.integrations.llm_cli.claude_code.subprocess.run")
277+
def test_probe_cli_auth_exit_1_json_on_stderr_only_is_probe_failure(
278+
mock_run: MagicMock,
279+
) -> None:
280+
"""Exit 1 with JSON only on stderr is treated as an opaque probe failure.
281+
282+
``_try_parse_auth_status_stdout`` reads stdout only, so JSON that lands on
283+
stderr falls through to the exit-code branch and returns ``(None, "claude
284+
auth status failed: …")``. Pinning this contract guards against a future
285+
refactor that starts parsing stderr and silently changes the return value.
286+
"""
287+
m = MagicMock()
288+
m.returncode = 1
289+
m.stdout = ""
290+
m.stderr = '{"loggedIn": false, "authMethod": "none"}'
291+
mock_run.return_value = m
292+
logged_in, detail = _probe_cli_auth("/usr/bin/claude")
293+
assert logged_in is None
294+
assert "failed" in detail
295+
296+
250297
@patch("app.integrations.llm_cli.claude_code.subprocess.run")
251298
def test_probe_cli_auth_timeout(mock_run: MagicMock) -> None:
252299
import subprocess

0 commit comments

Comments
 (0)