Move provider adapters (anthropic, bedrock, azure), platform adapters (telegram, slack, discord, feishu, dingtalk, matrix), and terminal backends (modal, daytona) out of core into plugins/ workspace members. Core references them via the plugin registries (get_provider_namespace / get_provider_service / get_tool_provider / get_credential_pool_hook) instead of direct imports. - Provider/platform/terminal adapters relocated under plugins/; pyproject extras reference workspace members; nix variants aggregate per-platform extras. - Anthropic credential discovery + OAuth-masquerade guard live in the plugin's credential_pool_hook; browser-open guarded by _can_open_graphical_browser. - Vercel AI Gateway + Vercel Sandbox removed (upstream deletion); get_bedrock_model_ids removed (replaced by bedrock_model_ids_or_none + discover_bedrock_models). - Terminal backends resolve ModalEnvironment / DaytonaEnvironment lazily from the plugin registry. - uv.lock regenerated against the pluginified workspace. Plugin test suites updated for the relocation: imports point at hermes_agent_<plat>.adapter, caplog logger-name filters and monkeypatch targets use the new module paths, and credential/rollback tests patch registries.get_provider_service rather than the removed agent.*_adapter modules. Verified: zero dead imports of relocated modules in core (import smoke test + rename-map grep); nix develop succeeds; targeted plugin suites green (bedrock, anthropic-auxiliary, matrix, dingtalk, feishu, credential_pool, switch_model_rollback). Remaining full-suite failures are pre-existing on the pre-merge tree (telegram setUpModule __code__) or environmental (voice/media/ PTY/network-dependent), not introduced here.
99 lines
3.5 KiB
Python
99 lines
3.5 KiB
Python
"""Anthropic-specific auth command tests moved from tests/hermes_cli/test_auth_commands.py."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import json
|
|
|
|
import pytest
|
|
|
|
|
|
def _write_auth_store(tmp_path, payload: dict) -> None:
|
|
hermes_home = tmp_path / "hermes"
|
|
hermes_home.mkdir(parents=True, exist_ok=True)
|
|
(hermes_home / "auth.json").write_text(json.dumps(payload, indent=2))
|
|
|
|
|
|
def _jwt_with_email(email: str) -> str:
|
|
header = base64.urlsafe_b64encode(b'{"alg":"RS256","typ":"JWT"}').rstrip(b"=").decode()
|
|
payload = base64.urlsafe_b64encode(
|
|
json.dumps({"email": email}).encode()
|
|
).rstrip(b"=").decode()
|
|
return f"{header}.{payload}.signature"
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clear_provider_env(monkeypatch):
|
|
for key in (
|
|
"OPENROUTER_API_KEY",
|
|
"OPENAI_API_KEY",
|
|
"ANTHROPIC_API_KEY",
|
|
"ANTHROPIC_TOKEN",
|
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
):
|
|
monkeypatch.delenv(key, raising=False)
|
|
|
|
|
|
def test_auth_add_anthropic_oauth_persists_pool_entry(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
|
|
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
|
monkeypatch.delenv("ANTHROPIC_TOKEN", raising=False)
|
|
monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False)
|
|
_write_auth_store(tmp_path, {"version": 1, "providers": {}})
|
|
token = _jwt_with_email("claude@example.com")
|
|
monkeypatch.setattr(
|
|
"hermes_agent_anthropic.adapter.run_hermes_oauth_login_pure",
|
|
lambda: {
|
|
"access_token": token,
|
|
"refresh_token": "refresh-token",
|
|
"expires_at_ms": 1711234567000,
|
|
},
|
|
)
|
|
|
|
from hermes_cli.auth_commands import auth_add_command
|
|
|
|
class _Args:
|
|
provider = "anthropic"
|
|
auth_type = "oauth"
|
|
api_key = None
|
|
label = None
|
|
|
|
auth_add_command(_Args())
|
|
|
|
payload = json.loads((tmp_path / "hermes" / "auth.json").read_text())
|
|
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 entry["source"] == "manual:hermes_pkce"
|
|
assert entry["refresh_token"] == "refresh-token"
|
|
assert entry["expires_at_ms"] == 1711234567000
|
|
|
|
|
|
def test_seed_from_singletons_respects_hermes_pkce_suppression(tmp_path, monkeypatch):
|
|
"""anthropic hermes_pkce must not re-seed from ~/.hermes/.anthropic_oauth.json when suppressed."""
|
|
hermes_home = tmp_path / "hermes"
|
|
hermes_home.mkdir(parents=True, exist_ok=True)
|
|
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
|
|
|
import yaml
|
|
(hermes_home / "config.yaml").write_text(yaml.dump({"model": {"provider": "anthropic", "model": "claude"}}))
|
|
(hermes_home / "auth.json").write_text(json.dumps({
|
|
"version": 1,
|
|
"providers": {},
|
|
"suppressed_sources": {"anthropic": ["hermes_pkce"]},
|
|
}))
|
|
|
|
# Stub the readers so only hermes_pkce is "available"; claude_code returns None
|
|
import hermes_agent_anthropic as aa
|
|
monkeypatch.setattr(aa, "read_hermes_oauth_credentials", lambda: {
|
|
"accessToken": "tok", "refreshToken": "r", "expiresAt": 9999999999000,
|
|
})
|
|
monkeypatch.setattr(aa, "read_claude_code_credentials", lambda: None)
|
|
|
|
from agent.credential_pool import _seed_from_singletons
|
|
entries = []
|
|
changed, active = _seed_from_singletons("anthropic", entries)
|
|
# hermes_pkce suppressed, claude_code returns None → nothing should be seeded
|
|
assert entries == []
|
|
assert "hermes_pkce" not in active
|