fix: isolate profile .env secrets on switch (#351)
* fix: isolate profile .env secrets on switch * fix: move direct os.environ set after _reload_dotenv to survive profile isolation The profile env isolation in _reload_dotenv now clears previously tracked env keys before re-reading .env. When apply_onboarding_setup set os.environ BEFORE _reload_dotenv, the key was immediately cleared. Move the belt-and-braces os.environ set to AFTER _reload_dotenv so the API key survives regardless of profile tracking state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -479,9 +479,6 @@ def apply_onboarding_setup(body: dict) -> dict:
|
||||
|
||||
if api_key:
|
||||
_write_env_file(env_path, {provider_meta["env_var"]: api_key})
|
||||
# Belt-and-braces: set directly on os.environ so the value is visible to
|
||||
# any code in the same process that reads it before the next request cycle.
|
||||
os.environ[provider_meta["env_var"]] = api_key
|
||||
|
||||
# Reload the hermes_cli provider/config cache so the next streaming call
|
||||
# picks up the new key without requiring a server restart.
|
||||
@@ -491,6 +488,12 @@ def apply_onboarding_setup(body: dict) -> dict:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Belt-and-braces: set directly on os.environ AFTER _reload_dotenv so the
|
||||
# value survives even if _reload_dotenv cleared it (e.g. when _write_env_file
|
||||
# wrote to disk but the profile isolation tracking hasn't seen it yet).
|
||||
if api_key:
|
||||
os.environ[provider_meta["env_var"]] = api_key
|
||||
|
||||
try:
|
||||
# hermes_cli may cache config at import time; ask it to reload if possible.
|
||||
from hermes_cli.config import reload as _cli_reload
|
||||
|
||||
@@ -26,6 +26,7 @@ _CLONE_CONFIG_FILES = ['config.yaml', '.env', 'SOUL.md']
|
||||
# ── Module state ────────────────────────────────────────────────────────────
|
||||
_active_profile = 'default'
|
||||
_profile_lock = threading.Lock()
|
||||
_loaded_profile_env_keys: set[str] = set()
|
||||
|
||||
def _resolve_base_hermes_home() -> Path:
|
||||
"""Return the BASE ~/.hermes directory — the root that contains profiles/.
|
||||
@@ -120,11 +121,24 @@ def _set_hermes_home(home: Path):
|
||||
|
||||
|
||||
def _reload_dotenv(home: Path):
|
||||
"""Load .env from the profile dir into os.environ (additive)."""
|
||||
"""Load .env from the profile dir into os.environ with profile isolation.
|
||||
|
||||
Clears env vars that were loaded from the previously active profile before
|
||||
applying the current profile's .env. This prevents API keys and other
|
||||
profile-scoped secrets from leaking across profile switches.
|
||||
"""
|
||||
global _loaded_profile_env_keys
|
||||
|
||||
# Remove keys loaded from the previous profile first.
|
||||
for key in list(_loaded_profile_env_keys):
|
||||
os.environ.pop(key, None)
|
||||
_loaded_profile_env_keys = set()
|
||||
|
||||
env_path = home / '.env'
|
||||
if not env_path.exists():
|
||||
return
|
||||
try:
|
||||
loaded_keys: set[str] = set()
|
||||
for line in env_path.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#') and '=' in line:
|
||||
@@ -133,8 +147,10 @@ def _reload_dotenv(home: Path):
|
||||
v = v.strip().strip('"').strip("'")
|
||||
if k and v:
|
||||
os.environ[k] = v
|
||||
loaded_keys.add(k)
|
||||
_loaded_profile_env_keys = loaded_keys
|
||||
except Exception:
|
||||
pass
|
||||
_loaded_profile_env_keys = set()
|
||||
|
||||
|
||||
def init_profile_state() -> None:
|
||||
|
||||
Reference in New Issue
Block a user