"""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"