From 2ef1ad280beee581e0f023901d0d040efec380ac Mon Sep 17 00:00:00 2001 From: Frank Song Date: Fri, 1 May 2026 13:42:50 +0800 Subject: [PATCH] fix: prefer ~/.hermes/.env over os.environ when seeding credential pool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When _seed_from_env() reads API keys to populate the credential pool, it should treat ~/.hermes/.env as the authoritative source — not os.environ. Stale env vars inherited from parent shell processes (Codex CLI, test scripts, etc.) can shadow deliberate changes to the .env file, causing auth.json to cache an outdated key that leads to silent 401 errors. This is especially visible with OpenRouter: if a parent process exported OPENROUTER_API_KEY=test-key-fresh and the user later updates .env with a valid key, restarting Hermes still picks up the stale os.environ value, writes it back to auth.json, and all API calls fail with 401. Fixes #18254 --- agent/credential_pool.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/agent/credential_pool.py b/agent/credential_pool.py index 004b5749..27a16bd4 100644 --- a/agent/credential_pool.py +++ b/agent/credential_pool.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +import os import random import threading import time @@ -13,7 +14,7 @@ from datetime import datetime from typing import Any, Dict, List, Optional, Set, Tuple from hermes_constants import OPENROUTER_BASE_URL -from hermes_cli.config import get_env_value +from hermes_cli.config import get_env_value, load_env import hermes_cli.auth as auth_mod from hermes_cli.auth import ( CODEX_ACCESS_TOKEN_REFRESH_SKEW_SECONDS, @@ -1380,6 +1381,16 @@ def _seed_from_singletons(provider: str, entries: List[PooledCredential]) -> Tup def _seed_from_env(provider: str, entries: List[PooledCredential]) -> Tuple[bool, Set[str]]: changed = False active_sources: Set[str] = set() + + # Prefer ~/.hermes/.env over os.environ — the user's config file is the + # authoritative source for Hermes credentials. Stale env vars from parent + # processes (Codex CLI, test scripts, etc.) should not override deliberate + # changes to the .env file. + def _get_env_prefer_dotenv(key: str) -> str: + env_file = load_env() + val = env_file.get(key) or os.environ.get(key) or "" + return val.strip() + # Honour user suppression — `hermes auth remove ` for an # env-seeded credential marks the env: source as suppressed so it # won't be re-seeded from the user's shell environment or ~/.hermes/.env. @@ -1391,8 +1402,8 @@ 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": - # Check both os.environ and ~/.hermes/.env file - token = (get_env_value("OPENROUTER_API_KEY") or "").strip() + # Prefer ~/.hermes/.env over os.environ + token = _get_env_prefer_dotenv("OPENROUTER_API_KEY") if token: source = "env:OPENROUTER_API_KEY" if _is_source_suppressed(provider, source): @@ -1418,7 +1429,7 @@ def _seed_from_env(provider: str, entries: List[PooledCredential]) -> Tuple[bool env_url = "" if pconfig.base_url_env_var: - env_url = (get_env_value(pconfig.base_url_env_var) or "").strip().rstrip("/") + env_url = _get_env_prefer_dotenv(pconfig.base_url_env_var).rstrip("/") env_vars = list(pconfig.api_key_env_vars) if provider == "anthropic": @@ -1429,8 +1440,8 @@ def _seed_from_env(provider: str, entries: List[PooledCredential]) -> Tuple[bool ] for env_var in env_vars: - # Check both os.environ and ~/.hermes/.env file - token = (get_env_value(env_var) or "").strip() + # Prefer ~/.hermes/.env over os.environ + token = _get_env_prefer_dotenv(env_var) if not token: continue source = f"env:{env_var}"