Files
hermes-agent/tests/hermes_cli/test_external_process_cli_prefixes.py
Teknium fc4c176eec fix(auth): find the Claude CLI outside PATH in external-process and setup-token checks
_external_process_spec asked PATH only (shutil.which) for a provider's
process_command, so a backend started by a LaunchAgent or Hermes Desktop
(bare PATH) reported 'claude' missing even when it lived in
~/.local/bin, ~/.claude/local, ... -- the prefixes agent/anthropic_adapter
already probes for version detection. run_oauth_setup_token had the same
PATH-only lookup.

Add anthropic_adapter.find_claude_code_cli(), which resolves a bare
claude/claude-code name via hermes_platform's locate_command over the
adapter's existing _CLAUDE_CODE_PREFIXES, and use it on a PATH miss in
_external_process_spec and in run_oauth_setup_token. PATH hits and every
other command are unchanged.
2026-10-04 17:51:24 -07:00

71 lines
2.9 KiB
Python

"""A Claude Code CLI outside PATH is found by every core check, not only by the Anthropic adapter.
A backend started by a macOS LaunchAgent or spawned by Hermes Desktop inherits a bare PATH that
carries none of Claude Code's install prefixes (``~/.local/bin``, ``~/.claude/local``, ...).
``agent.anthropic_adapter`` probes those prefixes after PATH; ``_external_process_spec`` and
``run_oauth_setup_token`` asked PATH only, so a provider whose ``process_command`` is ``claude``
was reported missing ("Could not find the ... CLI command 'claude'") on a machine where the
adapter could run it. Reported against hermes-plugin-claude-subscription-directsdk#32.
"""
from __future__ import annotations
import subprocess
import pytest
from providers import register_provider
from providers.base import ProviderProfile
register_provider(
ProviderProfile(
name="claude-cli-acp-test",
display_name="Claude CLI (test)",
base_url="process://claude-cli-acp-test",
auth_type="external_process",
process_command="claude",
)
)
def _install(directory) -> str:
directory.mkdir(parents=True, exist_ok=True)
exe = directory / "claude"
exe.write_text("#!/bin/sh\nexit 0\n")
exe.chmod(0o755)
return str(exe)
@pytest.fixture
def service_launch(tmp_path, monkeypatch):
"""The user's home holds ``~/.local/bin/claude``; the process PATH carries no install prefix."""
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setenv("PATH", str(tmp_path / "empty-path"))
return _install(tmp_path / ".local" / "bin")
@pytest.mark.platforms("posix") # HOME-relative prefixes and the exec bit are the POSIX layout
def test_external_process_provider_finds_claude_in_an_install_prefix(service_launch, tmp_path, monkeypatch):
from hermes_cli.auth import get_external_process_provider_status, resolve_external_process_provider_credentials
assert resolve_external_process_provider_credentials("claude-cli-acp-test")["command"] == service_launch
status = get_external_process_provider_status("claude-cli-acp-test")
assert (status["configured"], status["resolved_command"]) == (True, service_launch)
# A PATH hit is still what runs: the prefix probe only answers a PATH miss.
on_path = _install(tmp_path / "path-bin")
monkeypatch.setenv("PATH", str(tmp_path / "path-bin"))
assert resolve_external_process_provider_credentials("claude-cli-acp-test")["command"] == on_path
@pytest.mark.platforms("posix")
def test_setup_token_runs_claude_from_an_install_prefix(service_launch, monkeypatch):
import agent.anthropic_credentials as creds
ran: list[list[str]] = []
monkeypatch.setattr(creds.subprocess, "run", lambda argv, *a, **k: ran.append(argv) or subprocess.CompletedProcess(argv, 0))
monkeypatch.setattr(creds, "read_claude_code_credentials", lambda: None)
creds.run_oauth_setup_token()
assert ran == [[service_launch, "setup-token"]]