fix: align webui runtime env handling with gateway behavior
This commit is contained in:
1 parent
bc11cfe6d2
commit
dfcc06acdb
3 files changed
+80
-6
No files matched your search
+38
-3
@@ -696,6 +696,40 @@ def get_profile_runtime_env(home: Path) -> dict[str, str]:
|
|||||||
return env
|
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
|
@contextmanager
|
||||||
def profile_env_for_background_worker(
|
def profile_env_for_background_worker(
|
||||||
session,
|
session,
|
||||||
@@ -724,6 +758,7 @@ def profile_env_for_background_worker(
|
|||||||
|
|
||||||
profile_home_path = Path(get_hermes_home_for_profile(profile))
|
profile_home_path = Path(get_hermes_home_for_profile(profile))
|
||||||
runtime_env = get_profile_runtime_env(profile_home_path)
|
runtime_env = get_profile_runtime_env(profile_home_path)
|
||||||
|
safe_runtime_env = filter_runtime_env_for_gateway_parity(runtime_env)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.debug(
|
log.debug(
|
||||||
"Failed to resolve profile env for %s profile %s; falling back to current env",
|
"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
|
yield
|
||||||
return
|
return
|
||||||
|
|
||||||
thread_env = dict(runtime_env)
|
thread_env = dict(safe_runtime_env)
|
||||||
thread_env["HERMES_HOME"] = str(profile_home_path)
|
thread_env["HERMES_HOME"] = str(profile_home_path)
|
||||||
# Hybrid profile routing: keep the broad runtime env in WebUI's thread-local
|
# 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
|
# channel for WebUI helpers, and also mirror it into process env for the
|
||||||
@@ -749,11 +784,11 @@ def profile_env_for_background_worker(
|
|||||||
try:
|
try:
|
||||||
_set_thread_env(**thread_env)
|
_set_thread_env(**thread_env)
|
||||||
with _ENV_LOCK:
|
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
|
had_hermes_home = "HERMES_HOME" in os.environ
|
||||||
old_hermes_home = os.environ.get("HERMES_HOME")
|
old_hermes_home = os.environ.get("HERMES_HOME")
|
||||||
skill_home_snapshot = snapshot_skill_home_modules()
|
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)
|
os.environ["HERMES_HOME"] = str(profile_home_path)
|
||||||
try:
|
try:
|
||||||
patch_skill_home_modules(profile_home_path)
|
patch_skill_home_modules(profile_home_path)
|
||||||
|
|||||||
+5
-2
@@ -5856,6 +5856,7 @@ def _run_agent_streaming(
|
|||||||
# process-level active-profile global. Falls back gracefully.
|
# process-level active-profile global. Falls back gracefully.
|
||||||
try:
|
try:
|
||||||
from api.profiles import (
|
from api.profiles import (
|
||||||
|
filter_runtime_env_for_gateway_parity,
|
||||||
patch_skill_home_modules,
|
patch_skill_home_modules,
|
||||||
get_hermes_home_for_profile,
|
get_hermes_home_for_profile,
|
||||||
get_profile_runtime_env,
|
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_path = get_hermes_home_for_profile(getattr(s, 'profile', None))
|
||||||
_profile_home = str(_profile_home_path)
|
_profile_home = str(_profile_home_path)
|
||||||
_profile_runtime_env = get_profile_runtime_env(_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:
|
except ImportError:
|
||||||
_profile_home = os.environ.get('HERMES_HOME', '')
|
_profile_home = os.environ.get('HERMES_HOME', '')
|
||||||
_profile_runtime_env = {}
|
_profile_runtime_env = {}
|
||||||
|
_safe_profile_runtime_env = {}
|
||||||
patch_skill_home_modules = None
|
patch_skill_home_modules = None
|
||||||
|
|
||||||
# Profile-aware provider/model enrichment: when the session belongs
|
# 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
|
# The finally block re-acquires to restore — keeping critical sections short
|
||||||
# and preventing a deadlock where the restore would re-enter the same lock.
|
# and preventing a deadlock where the restore would re-enter the same lock.
|
||||||
with _ENV_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_cwd = os.environ.get('TERMINAL_CWD')
|
||||||
old_exec_ask = os.environ.get('HERMES_EXEC_ASK')
|
old_exec_ask = os.environ.get('HERMES_EXEC_ASK')
|
||||||
old_session_key = os.environ.get('HERMES_SESSION_KEY')
|
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_platform = os.environ.get('HERMES_SESSION_PLATFORM')
|
||||||
old_session_chat_id = os.environ.get('HERMES_SESSION_CHAT_ID')
|
old_session_chat_id = os.environ.get('HERMES_SESSION_CHAT_ID')
|
||||||
old_hermes_home = os.environ.get('HERMES_HOME')
|
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['TERMINAL_CWD'] = str(s.workspace)
|
||||||
os.environ['HERMES_EXEC_ASK'] = '1'
|
os.environ['HERMES_EXEC_ASK'] = '1'
|
||||||
os.environ['HERMES_SESSION_KEY'] = session_id
|
os.environ['HERMES_SESSION_KEY'] = session_id
|
||||||
|
|||||||
@@ -52,8 +52,44 @@ def test_streaming_applies_profile_runtime_env_to_agent_run():
|
|||||||
|
|
||||||
assert "get_profile_runtime_env" in src
|
assert "get_profile_runtime_env" in src
|
||||||
assert "_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 "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():
|
def test_streaming_thread_env_allows_profile_terminal_cwd_override():
|
||||||
|
|||||||
Reference in new issue
Block a user