Files
aiturk-hermes-ide/tests/hermes_cli/test_auth_key_prefix_skip.py

177 lines
6.9 KiB
Python

"""Regression tests for #93593: a malformed provider key in .env must not
silently shadow a valid credential-pool key.
Before the fix, ``_resolve_api_key_provider_secret`` returned the FIRST env
value that passed ``has_usable_secret`` (length + placeholder check only), so
an obviously malformed OPENROUTER_API_KEY (e.g. a truncated paste or another
provider's key) in ~/.hermes/.env won over a valid pool entry and produced
opaque ``401 Missing Authentication header`` errors.
The fix:
- providers with a declared key prefix (KNOWN_PROVIDER_KEY_PREFIXES) skip
env values that don't match, with a WARNING naming the env var, and fall
through to the next env var / credential pool;
- the credential-pool fallback iterates entries instead of only peek(), so a
malformed pool entry doesn't block a valid one either;
- providers WITHOUT a declared prefix are fail-open (unchanged behavior);
- a VALID env key still wins over the pool (precedence unchanged).
"""
import logging
import os
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
def _make_pconfig(provider_id, env_vars=None):
from hermes_cli.auth import ProviderConfig
return ProviderConfig(
id=provider_id,
name=provider_id.title(),
auth_type="api_key",
api_key_env_vars=tuple(env_vars or [f"{provider_id.upper()}_API_KEY"]),
)
@pytest.fixture
def isolated_hermes_home(tmp_path, monkeypatch):
home = tmp_path / ".hermes"
home.mkdir()
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setenv("HERMES_HOME", str(home))
for key in ["OPENROUTER_API_KEY", "DEEPSEEK_API_KEY", "OPENAI_API_KEY"]:
monkeypatch.delenv(key, raising=False)
return home
def _write_env_file(home: Path, **kwargs) -> None:
lines = [f"{k}={v}" for k, v in kwargs.items()]
(home / ".env").write_text("\n".join(lines) + "\n")
def _mock_pool(*entries):
pool = MagicMock()
pool.has_credentials.return_value = bool(entries)
pool.peek.return_value = entries[0] if entries else None
pool.entries.return_value = list(entries)
return pool
def _entry(token):
e = MagicMock()
e.access_token = token
e.runtime_api_key = ""
return e
class TestMalformedEnvKeySkipped:
"""Declared-prefix mismatch in .env → warn + fall through to pool."""
def test_malformed_openrouter_env_key_falls_back_to_pool(
self, isolated_hermes_home, caplog
):
_write_env_file(isolated_hermes_home, OPENROUTER_API_KEY="not-a-real-openrouter-key")
pool = _mock_pool(_entry("sk-or-v1-valid-pool-key-abc123"))
from hermes_cli.auth import _resolve_api_key_provider_secret
with patch("agent.credential_pool.load_pool", return_value=pool):
with caplog.at_level(logging.WARNING):
key, source = _resolve_api_key_provider_secret(
provider_id="openrouter",
pconfig=_make_pconfig("openrouter"),
)
assert key == "sk-or-v1-valid-pool-key-abc123"
assert source == "credential_pool:openrouter"
warnings = [r for r in caplog.records if "OPENROUTER_API_KEY" in r.getMessage()]
assert warnings, "expected a WARNING naming the malformed env var"
assert "sk-or-" in warnings[0].getMessage()
def test_malformed_env_key_with_empty_pool_returns_empty(
self, isolated_hermes_home, caplog
):
"""Malformed env key + no pool → empty result, never the bad key."""
_write_env_file(isolated_hermes_home, OPENROUTER_API_KEY="sk-proj-wrong-provider-key")
pool = _mock_pool()
from hermes_cli.auth import _resolve_api_key_provider_secret
with patch("agent.credential_pool.load_pool", return_value=pool):
with caplog.at_level(logging.WARNING):
key, source = _resolve_api_key_provider_secret(
provider_id="openrouter",
pconfig=_make_pconfig("openrouter"),
)
assert key == ""
assert source == ""
def test_malformed_pool_entry_skipped_for_valid_one(self, isolated_hermes_home):
"""Pool iteration: a malformed first entry must not block a valid second."""
pool = _mock_pool(
_entry("garbage-pool-entry"),
_entry("sk-or-v1-second-entry-good"),
)
from hermes_cli.auth import _resolve_api_key_provider_secret
with patch("agent.credential_pool.load_pool", return_value=pool):
key, source = _resolve_api_key_provider_secret(
provider_id="openrouter",
pconfig=_make_pconfig("openrouter"),
)
assert key == "sk-or-v1-second-entry-good"
assert source == "credential_pool:openrouter"
class TestNoDeclaredPrefixUnaffected:
"""Providers without a declared prefix keep today's fail-open behavior."""
def test_undeclared_provider_env_key_returned_verbatim(self, isolated_hermes_home):
_write_env_file(isolated_hermes_home, DEEPSEEK_API_KEY="totally-unknown-format-key")
from hermes_cli.auth import _resolve_api_key_provider_secret
pool = _mock_pool(_entry("pool-key-should-not-win"))
with patch("agent.credential_pool.load_pool", return_value=pool) as mp:
key, source = _resolve_api_key_provider_secret(
provider_id="deepseek",
pconfig=_make_pconfig("deepseek"),
)
assert key == "totally-unknown-format-key"
assert source == "DEEPSEEK_API_KEY"
mp.assert_not_called()
class TestValidEnvKeyStillWins:
"""Precedence unchanged: a valid env key beats the credential pool."""
def test_valid_openrouter_env_key_wins_over_pool(self, isolated_hermes_home):
_write_env_file(isolated_hermes_home, OPENROUTER_API_KEY="sk-or-v1-env-key-wins")
pool = _mock_pool(_entry("sk-or-v1-pool-key-loses"))
from hermes_cli.auth import _resolve_api_key_provider_secret
with patch("agent.credential_pool.load_pool", return_value=pool) as mp:
key, source = _resolve_api_key_provider_secret(
provider_id="openrouter",
pconfig=_make_pconfig("openrouter"),
)
assert key == "sk-or-v1-env-key-wins"
assert source == "OPENROUTER_API_KEY"
mp.assert_not_called()
def test_second_env_var_wins_when_first_is_malformed(self, isolated_hermes_home):
"""Malformed first env var falls through to a valid SECOND env var."""
_write_env_file(
isolated_hermes_home,
OPENROUTER_API_KEY="malformed-first-var",
OPENROUTER_KEY="sk-or-v1-second-var-good",
)
from hermes_cli.auth import _resolve_api_key_provider_secret
key, source = _resolve_api_key_provider_secret(
provider_id="openrouter",
pconfig=_make_pconfig(
"openrouter", env_vars=["OPENROUTER_API_KEY", "OPENROUTER_KEY"]
),
)
assert key == "sk-or-v1-second-var-good"
assert source == "OPENROUTER_KEY"