Each removed test either asserts nothing reachable or repeats another test's exact body: - auxiliary_client empty-base_url: body only writes config.yaml; a merge (890bbbda1f) restored the header of a testb3d4f67b20had purged. The non-Anthropic-host test beside it drives the same default fallback. - background_review no-session-db: _record_review_usage_to_parent swallows every exception and there is no DB to observe, so removing the guard still passes; the path has no observable effect to pin. - code_execution Windows "would have failed" pair: every branch returns, skips or passes; they exercise CPython defaults, not Hermes code. - cron test_should_use_direct_api_call_*: byte-identical copy of the test in tests/agent/test_cron_inline_api_call.py (0213b8b77bhad to edit both). - cron liveness no-multiplexer: identical body to test_no_lock_no_pids_is_false; its multiplexer patch is never reached. - browser console/get_images "private URLs allowed": identical to the local-backend test in each file; both mock the predicate the name promises. - hermes_state TestTitleUniqueness: the duplicate-title case is a strict subset of TestSessionTitleLineage::test_unrelated_session_still_conflicts; the NULL-title case never writes a title.
206 lines
8.3 KiB
Python
206 lines
8.3 KiB
Python
"""Regression tests for issue #52608.
|
|
|
|
auxiliary_client `_try_anthropic()` must NOT apply `cfg["model"]["base_url"]`
|
|
when the configured base_url host is not an Anthropic-compatible endpoint
|
|
(e.g. OpenRouter, OpenAI). Operators routing main traffic through a
|
|
non-Anthropic provider's endpoint while keeping `provider: anthropic` would
|
|
otherwise have every side-channel call (memory extractors, reflection,
|
|
vision, title generation) 401 from the foreign host.
|
|
"""
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
def _extract_base_url_passed_to_build(mock_build):
|
|
"""Pull the base_url that `_try_anthropic()` actually handed to build_anthropic_client."""
|
|
args, _kwargs = mock_build.call_args
|
|
# build_anthropic_client(token, base_url) per agent/auxiliary_client.py line 2180
|
|
assert len(args) >= 2, f"expected (token, base_url), got args={args}"
|
|
return args[1]
|
|
|
|
|
|
class TestTryAnthropicBaseUrlHostValidation:
|
|
"""Issue #52608: side-channel calls must not be sent to a non-Anthropic host."""
|
|
|
|
def test_openrouter_base_url_does_not_leak_into_auxiliary(self, tmp_path, monkeypatch):
|
|
"""cfg.model.base_url=https://openrouter.ai/api/v1 must NOT override aux base_url."""
|
|
import hermes_yaml as yaml
|
|
from agent.auxiliary_client import _try_anthropic
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
(tmp_path / "config.yaml").write_text(yaml.safe_dump({
|
|
"model": {
|
|
"provider": "anthropic",
|
|
"model": "claude-haiku-4-5-20251001",
|
|
"base_url": "https://openrouter.ai/api/v1",
|
|
}
|
|
}))
|
|
|
|
with (
|
|
patch(
|
|
"agent.auxiliary_client._select_pool_entry", return_value=(False, None)
|
|
),
|
|
patch(
|
|
"agent.anthropic_credentials.resolve_anthropic_token",
|
|
return_value="***",
|
|
),
|
|
patch(
|
|
"agent.anthropic_adapter.build_anthropic_client"
|
|
) as mock_build,
|
|
):
|
|
mock_build.return_value = MagicMock()
|
|
client, _model = _try_anthropic()
|
|
|
|
assert client is not None, "auxiliary client must still be created"
|
|
actual = _extract_base_url_passed_to_build(mock_build)
|
|
assert actual == "https://api.anthropic.com", (
|
|
f"Auxiliary client must use the Anthropic default base_url, "
|
|
f"not the operator's main-session override. Got: {actual!r}"
|
|
)
|
|
|
|
def test_anthropic_default_host_is_preserved(self, tmp_path, monkeypatch):
|
|
"""The common case (operator sets model.base_url to api.anthropic.com) must still apply."""
|
|
import hermes_yaml as yaml
|
|
from agent.auxiliary_client import _try_anthropic
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
(tmp_path / "config.yaml").write_text(yaml.safe_dump({
|
|
"model": {
|
|
"provider": "anthropic",
|
|
"model": "claude-haiku-4-5-20251001",
|
|
"base_url": "https://api.anthropic.com",
|
|
}
|
|
}))
|
|
|
|
with (
|
|
patch(
|
|
"agent.auxiliary_client._select_pool_entry", return_value=(False, None)
|
|
),
|
|
patch(
|
|
"agent.anthropic_credentials.resolve_anthropic_token",
|
|
return_value="***",
|
|
),
|
|
patch(
|
|
"agent.anthropic_adapter.build_anthropic_client"
|
|
) as mock_build,
|
|
):
|
|
mock_build.return_value = MagicMock()
|
|
client, _model = _try_anthropic()
|
|
|
|
assert client is not None
|
|
actual = _extract_base_url_passed_to_build(mock_build)
|
|
assert actual == "https://api.anthropic.com"
|
|
|
|
def test_openai_base_url_does_not_leak(self, tmp_path, monkeypatch):
|
|
"""Generic non-Anthropic host must not be applied as auxiliary base_url."""
|
|
import hermes_yaml as yaml
|
|
from agent.auxiliary_client import _try_anthropic
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
(tmp_path / "config.yaml").write_text(yaml.safe_dump({
|
|
"model": {
|
|
"provider": "anthropic",
|
|
"model": "claude-haiku-4-5-20251001",
|
|
"base_url": "https://api.openai.com/v1",
|
|
}
|
|
}))
|
|
|
|
with (
|
|
patch(
|
|
"agent.auxiliary_client._select_pool_entry", return_value=(False, None)
|
|
),
|
|
patch(
|
|
"agent.anthropic_credentials.resolve_anthropic_token",
|
|
return_value="***",
|
|
),
|
|
patch(
|
|
"agent.anthropic_adapter.build_anthropic_client"
|
|
) as mock_build,
|
|
):
|
|
mock_build.return_value = MagicMock()
|
|
client, _model = _try_anthropic()
|
|
|
|
assert client is not None
|
|
actual = _extract_base_url_passed_to_build(mock_build)
|
|
assert actual == "https://api.anthropic.com", (
|
|
f"Non-Anthropic host must not be applied. Got: {actual!r}"
|
|
)
|
|
|
|
|
|
def test_anthropic_suffix_gateway_base_url_is_applied(self, tmp_path, monkeypatch):
|
|
"""A gateway exposing the Messages protocol under a ``/anthropic`` suffix
|
|
must be honored — the same convention the primary path already trusts —
|
|
so auxiliary/fallback calls hit the configured endpoint, not the default."""
|
|
import hermes_yaml as yaml
|
|
from agent.auxiliary_client import _try_anthropic
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
(tmp_path / "config.yaml").write_text(yaml.safe_dump({
|
|
"model": {
|
|
"provider": "anthropic",
|
|
"model": "claude-haiku-4-5-20251001",
|
|
"base_url": "https://gateway.example.com/anthropic",
|
|
}
|
|
}))
|
|
|
|
with (
|
|
patch(
|
|
"agent.auxiliary_client._select_pool_entry", return_value=(False, None)
|
|
),
|
|
patch(
|
|
"agent.anthropic_credentials.resolve_anthropic_token",
|
|
return_value="***",
|
|
),
|
|
patch(
|
|
"agent.anthropic_adapter.build_anthropic_client"
|
|
) as mock_build,
|
|
):
|
|
mock_build.return_value = MagicMock()
|
|
client, _model = _try_anthropic()
|
|
|
|
assert client is not None
|
|
actual = _extract_base_url_passed_to_build(mock_build)
|
|
assert actual == "https://gateway.example.com/anthropic", (
|
|
f"/anthropic-suffixed gateway base_url must be applied. Got: {actual!r}"
|
|
)
|
|
|
|
def test_anthropic_suffix_host_check_direct(self):
|
|
"""Unit-level: the host check trusts native hosts and /anthropic gateways,
|
|
and still rejects a bare non-Anthropic host (the #52608 guard)."""
|
|
from agent.auxiliary_client import _is_anthropic_compatible_host as ok
|
|
assert ok("https://api.anthropic.com") is True
|
|
assert ok("https://gateway.example.com/anthropic") is True
|
|
assert ok("http://127.0.0.1:8080/anthropic/v1") is True
|
|
assert ok("https://openrouter.ai/api/v1") is False
|
|
assert ok("https://api.openai.com/v1") is False
|
|
assert ok("") is False
|
|
|
|
def test_anthropic_host_with_path_is_preserved(self, tmp_path, monkeypatch):
|
|
"""api.anthropic.com with a path suffix must still pass the host check."""
|
|
import hermes_yaml as yaml
|
|
from agent.auxiliary_client import _try_anthropic
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
(tmp_path / "config.yaml").write_text(yaml.safe_dump({
|
|
"model": {
|
|
"provider": "anthropic",
|
|
"model": "claude-haiku-4-5-20251001",
|
|
"base_url": "https://api.anthropic.com/v1/messages",
|
|
}
|
|
}))
|
|
|
|
with (
|
|
patch(
|
|
"agent.auxiliary_client._select_pool_entry", return_value=(False, None)
|
|
),
|
|
patch(
|
|
"agent.anthropic_credentials.resolve_anthropic_token",
|
|
return_value="***",
|
|
),
|
|
patch(
|
|
"agent.anthropic_adapter.build_anthropic_client"
|
|
) as mock_build,
|
|
):
|
|
mock_build.return_value = MagicMock()
|
|
client, _model = _try_anthropic()
|
|
|
|
assert client is not None
|
|
actual = _extract_base_url_passed_to_build(mock_build)
|
|
assert actual == "https://api.anthropic.com/v1/messages", (
|
|
f"Anthropic host with path must be preserved. Got: {actual!r}"
|
|
)
|