Files
hermes-agent/tests/hermes_cli/test_auth_model_key_env.py
Teknium 6f3e630b47 fix(auth): honor Desktop-saved model.key_env for registry providers
The Desktop settings UI persists a registry provider's API key as a
credential pointer -- model.key_env in config.yaml pointing at an env var
in $HERMES_HOME/.env (e.g. HERMES_CUSTOM_LMSTUDIO_API_KEY) -- while
keeping model.provider on the registry id. _resolve_api_key_provider_secret
resolved registry providers exclusively from
PROVIDER_REGISTRY[...].api_key_env_vars, so the UI-saved key was silently
ignored; for lmstudio the runtime then substituted the no-auth placeholder
(dummy-lm-api-key) and auth-enabled LM Studio servers rejected every
request with an opaque, retryable-flagged 401.

Fix: consult model.key_env (only when config.yaml's main model targets the
provider being resolved, so the pointer never leaks across providers)
before the registry env vars, running the value through the same
_usable_declared_secret prefix validation. Precedence for the canonical
env vars and the credential pool is unchanged; a valid canonical key still
wins when no pointer is set.

Complements #75373, which covers providers.<id>.key_env at the provider
level; the Desktop save path writes the pointer at the model level, which
that PR does not consult.

Fixes #106336
2026-09-09 12:33:15 -07:00

61 lines
2.4 KiB
Python

"""Regression tests for #106336: Desktop-saved registry-provider credentials.
The Desktop settings UI persists a registry provider's API key as a credential
pointer — ``model.key_env`` in config.yaml → the env var in ``$HERMES_HOME/.env``
(e.g. ``HERMES_CUSTOM_LMSTUDIO_API_KEY``) — while keeping ``model.provider`` on
the registry id. Before the fix, ``_resolve_api_key_provider_secret`` consulted
only ``PROVIDER_REGISTRY[...].api_key_env_vars``, silently ignored the UI-saved
key, and lmstudio fell through to its no-auth placeholder (``dummy-lm-api-key``),
producing an opaque 401 against auth-enabled LM Studio servers.
"""
from pathlib import Path
import pytest
@pytest.fixture
def hermes_home(tmp_path, monkeypatch):
home = tmp_path / ".hermes"
home.mkdir()
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setenv("HERMES_HOME", str(home))
monkeypatch.delenv("LM_API_KEY", raising=False)
monkeypatch.delenv("HERMES_CUSTOM_LMSTUDIO_API_KEY", raising=False)
return home
def _write(home: Path, config: str, env: str) -> None:
(home / "config.yaml").write_text(config, encoding="utf-8")
(home / ".env").write_text(env, encoding="utf-8")
def test_model_key_env_pointer_is_honored_for_registry_provider(hermes_home):
_write(
hermes_home,
"model:\n default: some-model\n provider: lmstudio\n"
" base_url: http://127.0.0.1:1234/v1\n"
" key_env: HERMES_CUSTOM_LMSTUDIO_API_KEY\n",
"HERMES_CUSTOM_LMSTUDIO_API_KEY=sk-lm-desktop-saved\n",
)
from hermes_cli.auth import resolve_api_key_provider_credentials
creds = resolve_api_key_provider_credentials("lmstudio")
assert creds["api_key"] == "sk-lm-desktop-saved"
assert creds["source"] == "HERMES_CUSTOM_LMSTUDIO_API_KEY"
def test_model_key_env_does_not_leak_across_providers(hermes_home):
# model targets a DIFFERENT provider: its key_env must not be consulted for
# lmstudio, which falls back to its documented no-auth placeholder.
_write(
hermes_home,
"model:\n default: gpt-x\n provider: openai\n"
" key_env: HERMES_CUSTOM_LMSTUDIO_API_KEY\n",
"HERMES_CUSTOM_LMSTUDIO_API_KEY=sk-lm-should-not-leak\n",
)
from hermes_cli.auth import LMSTUDIO_NOAUTH_PLACEHOLDER, resolve_api_key_provider_credentials
creds = resolve_api_key_provider_credentials("lmstudio")
assert creds["api_key"] == LMSTUDIO_NOAUTH_PLACEHOLDER