fix(auth): explicit-provider gate uses the credential resolver's reader

get_env_value stops at the first environ hit, so a shell that exports
DEEPSEEK_API_KEY= (empty) hides a real key in .env from the gate while
resolve_api_key_provider_credentials() finds it: the picker omits a provider
the chat path would authenticate with. Read through
get_env_value_prefer_dotenv, the same chain auth.py already uses to resolve
the key, so the two can never disagree (#77007).

Co-authored-by: webtecnica <webtecnica@gmail.com>
This commit is contained in:
Austin Pickett
2026-09-20 22:05:22 -04:00
parent 4ea57fa61b
commit 1a1f4a59e2
2 changed files with 22 additions and 4 deletions

View File

@@ -1023,11 +1023,13 @@ def _env_secret(name: str) -> bool:
process environ is the *launch* profile, so a DeepSeek key pasted into another
profile's ``.env`` would be invisible to ``explicit_only`` Settings → Model
until a Bot-chat Refresh ran against that profile's own backend.
``get_env_value`` is the scope-aware reader (#67027): secret scope, then the
current HERMES_HOME ``.env``.
Same reader as the credential resolver (``get_env_value_prefer_dotenv``: the current
HERMES_HOME ``.env`` first, then the scope-checked environ) so the gate and the key that
actually authenticates never disagree — an empty ``DEEPSEEK_API_KEY=`` export in the parent
shell must not hide a real key in ``.env`` (#77007).
"""
from hermes_cli.config import get_env_value
return has_usable_secret(get_env_value(name) or "")
from hermes_cli.config import get_env_value_prefer_dotenv
return has_usable_secret(get_env_value_prefer_dotenv(name) or "")
def _explicit_env_credentials_present(normalized: str) -> bool:

View File

@@ -222,6 +222,22 @@ def test_profile_dotenv_key_counts_as_explicit_when_process_env_lacks_it(tmp_pat
assert is_provider_explicitly_configured("deepseek") is False
def test_dotenv_key_counts_when_shell_exports_the_var_empty(tmp_path, monkeypatch):
"""The gate resolves through the same reader as the credential resolver
(get_env_value_prefer_dotenv), so it agrees with the key that authenticates:
an empty ``DEEPSEEK_API_KEY=`` inherited from the parent shell must not hide
a real key in .env (#77007) — the resolver would use that key, so the picker
must list the provider."""
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
monkeypatch.setenv("DEEPSEEK_API_KEY", "")
_write_config(tmp_path, {"model": {}})
(tmp_path / "hermes" / ".env").write_text("DEEPSEEK_API_KEY=sk-dotenv-only-secret\n")
from hermes_cli.auth import is_provider_explicitly_configured, resolve_api_key_provider_credentials
assert resolve_api_key_provider_credentials("deepseek").get("api_key") == "sk-dotenv-only-secret"
assert is_provider_explicitly_configured("deepseek") is True
# ─── aws_sdk providers (Bedrock) ─────────────────────────────────────────
#
# Bedrock is registered with auth_type="aws_sdk" and an empty