Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions agent/anthropic_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ def _generate_pkce() -> tuple:
return verifier, challenge


def run_hermes_oauth_login_pure() -> Optional[Dict[str, Any]]:
def run_hermes_oauth_login_pure(open_browser: bool = True) -> Optional[Dict[str, Any]]:
"""Run Hermes-native OAuth PKCE flow and return credential state."""
import webbrowser
from urllib.parse import urlencode
Expand All @@ -529,7 +529,7 @@ def run_hermes_oauth_login_pure() -> Optional[Dict[str, Any]]:
from hermes_cli.auth import _can_open_graphical_browser as _can_open_gui
except Exception:
_can_open_gui = lambda: True # noqa: E731 — degrade to prior behavior
if _can_open_gui():
if open_browser and _can_open_gui():
with contextlib.suppress(Exception):
webbrowser.open(auth_url)
print(" (Browser opened automatically)")
Expand Down
4 changes: 3 additions & 1 deletion hermes_cli/auth_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,9 @@ def _format_exhausted_status(entry) -> str:

def _anthropic_oauth_login(args) -> dict:
from agent import anthropic_credentials as anthropic_mod
creds = anthropic_mod.run_hermes_oauth_login_pure()
creds = anthropic_mod.run_hermes_oauth_login_pure(
open_browser=not getattr(args, "no_browser", False),
)
if not creds:
raise SystemExit("Anthropic OAuth login did not return credentials.")
return creds
Expand Down
14 changes: 14 additions & 0 deletions tests/agent/test_anthropic_oauth_pkce.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,20 @@ def fake_urlopen(req, *_a, **_kw):
monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen)


def test_no_browser_skips_automatic_browser_open(monkeypatch):
opened_urls = []
monkeypatch.setattr("webbrowser.open", opened_urls.append)
monkeypatch.setattr(
"hermes_cli.auth._can_open_graphical_browser", lambda: True
)
monkeypatch.setattr("builtins.input", lambda *_a, **_kw: "")

from agent.anthropic_credentials import run_hermes_oauth_login_pure

assert run_hermes_oauth_login_pure(open_browser=False) is None
assert opened_urls == []


def test_authorization_url_state_is_not_pkce_verifier(monkeypatch, tmp_path):
"""The ``state`` parameter in the authorization URL must NOT equal the
PKCE ``code_verifier``.
Expand Down
39 changes: 39 additions & 0 deletions tests/hermes_cli/test_auth_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,45 @@ class _Args:
assert entry["access_token"] == "sk-or-manual"


def test_auth_add_anthropic_oauth_forwards_no_browser(tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
_write_auth_store(tmp_path, {"version": 1, "providers": {}})
token = _jwt_with_email("claude@example.com")
oauth_options = {}

def fake_oauth_login(*, open_browser=True):
oauth_options["open_browser"] = open_browser
return {
"access_token": token,
"refresh_token": "refresh-token",
"expires_at_ms": 1711234567000,
}

monkeypatch.setattr(
"agent.anthropic_credentials.run_hermes_oauth_login_pure",
fake_oauth_login,
)

from hermes_cli.auth_commands import auth_add_command

class _Args:
provider = "anthropic"
auth_type = "oauth"
api_key = None
label = None
no_browser = True

auth_add_command(_Args())

payload = json.loads(
(tmp_path / "hermes" / "auth.json").read_text(encoding="utf-8")
)
entries = payload["credential_pool"]["anthropic"]
entry = next(item for item in entries if item["source"] == "manual:hermes_pkce")
assert entry["label"] == "claude@example.com"
assert oauth_options == {"open_browser": False}


def test_auth_add_configured_provider_uses_canonical_pool_key(tmp_path, monkeypatch):
"""A keyed providers row must keep its runtime slug in the auth pool."""
hermes_home = tmp_path / "hermes"
Expand Down