Merge pull request #4436 from nesquena/release/stage-4432

Release v0.51.498 — RH (profile runtime env no longer clobbers core shell identity, #4432)
This commit is contained in:
nesquena-hermes 2026-06-18 13:20:08 -07:00 committed by GitHub
commit dbc7b317e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 86 additions and 6 deletions

View file

@ -3,6 +3,12 @@
## [Unreleased]
## [v0.51.498] — 2026-06-18 — Release RH (profile runtime env no longer clobbers core shell identity)
### Fixed
- **A profile's runtime env no longer overrides the WebUI server's own core shell-identity variables during a turn.** When a profile-scoped turn projected the profile's `config.yaml` terminal settings and `.env` into the process environment, a profile that set `HOME`, `PATH`, `PWD`, `SHELL`, `USER`, `PYTHONPATH`, `VIRTUAL_ENV`, `LD_LIBRARY_PATH`, or an `XDG_*` var could clobber the running server's identity/import path for the duration of the run. WebUI now filters those core-identity keys out of the projected profile env (matching Hermes Agent gateway semantics) while still projecting credentials, `HERMES_HOME`, and `TERMINAL_*` settings. Thanks @lunarnexus.
## [v0.51.497] — 2026-06-18 — Release RG (rollback checkpoint diffs no longer follow symlinks)
### Security

View file

@ -696,6 +696,40 @@ def get_profile_runtime_env(home: Path) -> dict[str, str]:
return env
# Match Hermes Agent gateway behavior: profile-scoped WebUI runs should
# project intended runtime vars (credentials, HERMES_HOME, TERMINAL_*)
# without allowing profile env to override core shell identity variables
# like HOME or PATH.
_BLOCKED_RUNTIME_ENV_KEYS = {
'HOME',
'PATH',
'PWD',
'SHELL',
'USER',
'LOGNAME',
'SHLVL',
'OLDPWD',
'PYTHONPATH',
'VIRTUAL_ENV',
'LD_LIBRARY_PATH',
}
def filter_runtime_env_for_gateway_parity(env: dict[str, str]) -> dict[str, str]:
"""Return a profile runtime env filtered to mimic Hermes gateway semantics."""
filtered: dict[str, str] = {}
for key, value in (env or {}).items():
k = str(key).strip()
if not k:
continue
if k in _BLOCKED_RUNTIME_ENV_KEYS:
continue
if k.startswith('XDG_'):
continue
filtered[k] = value
return filtered
@contextmanager
def profile_env_for_background_worker(
session,
@ -724,6 +758,7 @@ def profile_env_for_background_worker(
profile_home_path = Path(get_hermes_home_for_profile(profile))
runtime_env = get_profile_runtime_env(profile_home_path)
safe_runtime_env = filter_runtime_env_for_gateway_parity(runtime_env)
except Exception:
log.debug(
"Failed to resolve profile env for %s profile %s; falling back to current env",
@ -734,7 +769,7 @@ def profile_env_for_background_worker(
yield
return
thread_env = dict(runtime_env)
thread_env = dict(safe_runtime_env)
thread_env["HERMES_HOME"] = str(profile_home_path)
# Hybrid profile routing: keep the broad runtime env in WebUI's thread-local
# channel for WebUI helpers, and also mirror it into process env for the
@ -749,11 +784,11 @@ def profile_env_for_background_worker(
try:
_set_thread_env(**thread_env)
with _ENV_LOCK:
old_runtime_env = {key: os.environ.get(key) for key in runtime_env}
old_runtime_env = {key: os.environ.get(key) for key in safe_runtime_env}
had_hermes_home = "HERMES_HOME" in os.environ
old_hermes_home = os.environ.get("HERMES_HOME")
skill_home_snapshot = snapshot_skill_home_modules()
os.environ.update(runtime_env)
os.environ.update(safe_runtime_env)
os.environ["HERMES_HOME"] = str(profile_home_path)
try:
patch_skill_home_modules(profile_home_path)

View file

@ -5856,6 +5856,7 @@ def _run_agent_streaming(
# process-level active-profile global. Falls back gracefully.
try:
from api.profiles import (
filter_runtime_env_for_gateway_parity,
patch_skill_home_modules,
get_hermes_home_for_profile,
get_profile_runtime_env,
@ -5863,9 +5864,11 @@ def _run_agent_streaming(
_profile_home_path = get_hermes_home_for_profile(getattr(s, 'profile', None))
_profile_home = str(_profile_home_path)
_profile_runtime_env = get_profile_runtime_env(_profile_home_path)
_safe_profile_runtime_env = filter_runtime_env_for_gateway_parity(_profile_runtime_env)
except ImportError:
_profile_home = os.environ.get('HERMES_HOME', '')
_profile_runtime_env = {}
_safe_profile_runtime_env = {}
patch_skill_home_modules = None
# Profile-aware provider/model enrichment: when the session belongs
@ -5922,7 +5925,7 @@ def _run_agent_streaming(
# The finally block re-acquires to restore — keeping critical sections short
# and preventing a deadlock where the restore would re-enter the same lock.
with _ENV_LOCK:
old_profile_env = {key: os.environ.get(key) for key in _profile_runtime_env}
old_profile_env = {key: os.environ.get(key) for key in _safe_profile_runtime_env}
old_cwd = os.environ.get('TERMINAL_CWD')
old_exec_ask = os.environ.get('HERMES_EXEC_ASK')
old_session_key = os.environ.get('HERMES_SESSION_KEY')
@ -5930,7 +5933,7 @@ def _run_agent_streaming(
old_session_platform = os.environ.get('HERMES_SESSION_PLATFORM')
old_session_chat_id = os.environ.get('HERMES_SESSION_CHAT_ID')
old_hermes_home = os.environ.get('HERMES_HOME')
os.environ.update(_profile_runtime_env)
os.environ.update(_safe_profile_runtime_env)
os.environ['TERMINAL_CWD'] = str(s.workspace)
os.environ['HERMES_EXEC_ASK'] = '1'
os.environ['HERMES_SESSION_KEY'] = session_id

View file

@ -52,8 +52,44 @@ def test_streaming_applies_profile_runtime_env_to_agent_run():
assert "get_profile_runtime_env" in src
assert "_profile_runtime_env" in src
assert "_safe_profile_runtime_env" in src
assert "old_profile_env" in src
assert "os.environ.update(_profile_runtime_env)" in src
assert "filter_runtime_env_for_gateway_parity" in src
assert "os.environ.update(_safe_profile_runtime_env)" in src
assert "os.environ.update(_profile_runtime_env)" not in src
def test_filter_runtime_env_for_gateway_parity_blocks_shell_identity_vars():
from api.profiles import filter_runtime_env_for_gateway_parity
env = {
"HOME": "/tmp/fake-home",
"PATH": "/tmp/fake-bin:/usr/bin",
"PWD": "/tmp/fake-pwd",
"SHELL": "/bin/zsh",
"OPENAI_API_KEY": "test-key",
"TERMINAL_ENV": "ssh",
"TERMINAL_CWD": "/workspace",
}
filtered = filter_runtime_env_for_gateway_parity(env)
assert "HOME" not in filtered
assert "PATH" not in filtered
assert "PWD" not in filtered
assert "SHELL" not in filtered
assert filtered["OPENAI_API_KEY"] == "test-key"
assert filtered["TERMINAL_ENV"] == "ssh"
assert filtered["TERMINAL_CWD"] == "/workspace"
def test_profile_background_worker_uses_gateway_parity_runtime_env_filter():
src = Path("api/profiles.py").read_text(encoding="utf-8")
assert "filter_runtime_env_for_gateway_parity" in src
assert "safe_runtime_env" in src
assert "os.environ.update(safe_runtime_env)" in src
assert "os.environ.update(runtime_env)" not in src
def test_streaming_thread_env_allows_profile_terminal_cwd_override():