From 8443998dc3bf89e453152389bec79351d2cae710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E6=B3=A5=E8=B1=86?= <1243352777@qq.com> Date: Sun, 26 Apr 2026 14:35:55 +0800 Subject: [PATCH] fix(auth): resolve API keys from ~/.hermes/.env and credential_pool _resolve_api_key_provider_secret() and _seed_from_env() only checked os.environ for provider API keys. When keys exist in ~/.hermes/.env but are not loaded into the process environment (e.g. ACP adapter entry point, post-session-start .env edits, or non-CLI entry points), the resolution returns an empty string, causing HTTP 401 failures. Changes: - credential_pool._seed_from_env: use get_env_value() which checks both os.environ and ~/.hermes/.env file, preventing _prune_stale_seeded_entries from removing valid entries whose env var isn't in os.environ - credential_pool._seed_from_env: same fix for openrouter and base_url_env_var resolution - auth._resolve_api_key_provider_secret: use get_env_value() instead of os.getenv(), and add credential_pool fallback when env resolution fails Fixes #15914 --- agent/credential_pool.py | 20 +++++++++++++++++--- hermes_cli/auth.py | 21 ++++++++++++++++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/agent/credential_pool.py b/agent/credential_pool.py index f6cb24dd..dcdd2971 100644 --- a/agent/credential_pool.py +++ b/agent/credential_pool.py @@ -1273,7 +1273,12 @@ def _seed_from_env(provider: str, entries: List[PooledCredential]) -> Tuple[bool def _is_source_suppressed(_p, _s): # type: ignore[misc] return False if provider == "openrouter": - token = os.getenv("OPENROUTER_API_KEY", "").strip() + # Check both os.environ and ~/.hermes/.env file + try: + from hermes_cli.config import get_env_value + token = (get_env_value("OPENROUTER_API_KEY") or "").strip() + except Exception: + token = os.getenv("OPENROUTER_API_KEY", "").strip() if token: source = "env:OPENROUTER_API_KEY" if _is_source_suppressed(provider, source): @@ -1299,7 +1304,11 @@ def _seed_from_env(provider: str, entries: List[PooledCredential]) -> Tuple[bool env_url = "" if pconfig.base_url_env_var: - env_url = os.getenv(pconfig.base_url_env_var, "").strip().rstrip("/") + try: + from hermes_cli.config import get_env_value + env_url = (get_env_value(pconfig.base_url_env_var) or "").strip().rstrip("/") + except Exception: + env_url = os.getenv(pconfig.base_url_env_var, "").strip().rstrip("/") env_vars = list(pconfig.api_key_env_vars) if provider == "anthropic": @@ -1310,7 +1319,12 @@ def _seed_from_env(provider: str, entries: List[PooledCredential]) -> Tuple[bool ] for env_var in env_vars: - token = os.getenv(env_var, "").strip() + # Check both os.environ and ~/.hermes/.env file + try: + from hermes_cli.config import get_env_value + token = (get_env_value(env_var) or "").strip() + except Exception: + token = os.getenv(env_var, "").strip() if not token: continue source = f"env:{env_var}" diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index eeccbece..0ac6c64a 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -468,10 +468,29 @@ def _resolve_api_key_provider_secret( return "", "" for env_var in pconfig.api_key_env_vars: - val = os.getenv(env_var, "").strip() + # Check both os.environ and ~/.hermes/.env file + try: + from hermes_cli.config import get_env_value + val = (get_env_value(env_var) or "").strip() + except Exception: + val = os.getenv(env_var, "").strip() if has_usable_secret(val): return val, env_var + # Fallback: try credential pool (e.g. zai key stored via auth.json) + try: + from agent.credential_pool import load_pool + pool = load_pool(provider_id) + if pool and pool.has_credentials(): + entry = pool.peek() + if entry: + key = getattr(entry, "access_token", "") or getattr(entry, "runtime_api_key", "") + key = str(key).strip() + if has_usable_secret(key): + return key, f"credential_pool:{provider_id}" + except Exception: + pass + return "", ""