fix(terminal): scope terminal config per turn under profile multiplexing
A multiplexed Hermes process (gateway.multiplex_profiles, unified dashboard/TUI, or cron) serves several profiles at once, but terminal.* resolved through process-global TERMINAL_* env vars bridged ONCE at startup from the launch profile (gateway/run.py ~2700-2760) plus the one-shot _ensure_terminal_env_bridged() guard. Every routed profile therefore inherited the launch profile's backend, cwd, docker volumes, SSH target and shared-container key: a local profile ran inside another profile's docker sandbox (or a docker profile escaped to the host), and a container labeled profile A carried profile B's RW bind mounts. Fix: an authoritative per-profile terminal policy seam, mirroring agent/secret_scope.py: - tools/terminal_scope.py: ContextVar holding the routed profile's COMPLETE effective TERMINAL_* policy (defined defaults <- profile .env TERMINAL_* <- config.yaml terminal:). While bound, terminal_env() resolves ONLY from it - an omitted key yields the defined default, never os.environ. Unreadable/malformed policy installs a refusal scope; terminal_tool / execute_code refuse instead of running under ambient launch-process policy (fail closed). - Installed at every in-process profile boundary: gateway _profile_runtime_scope, tui_gateway session/build/turn scopes, cron per-job fire. The unscoped single-process path is byte-identical. - Every terminal.* consumer reads through the scope: terminal_tool (_get_env_config, _resolve_container_task_id shared key, orphan reaper lifetime, degraded mode), gateway/platforms/base.py docker media translation (volumes, shared key, persistence), runtime_cwd / agent_init / skill_utils / code_execution_tool / file_tools cwd anchors, prompt_builder / browser_tool / env_probe backend checks, gateway footer, @-refs and slash-command cwd. env_probe resolves the backend in the caller's context, since the probe worker thread does not inherit the ContextVar. Salvage of #99225 onto current main: adds the three ambient reads the PR missed (tools/file_tools.py TERMINAL_CWD, tools/browser_tool.py and tools/env_probe.py TERMINAL_ENV; shape from #79117) and trims the test module to the leak matrix driven through the real gateway boundary, omitted-key defaults, refusal, and boundary reset. Fixes #68559 Fixes #94200 Fixes #101132 Fixes #95470 Co-authored-by: x7peeps <9640837+x7peeps@users.noreply.github.com> Co-authored-by: Eva <239388517+100yenadmin@users.noreply.github.com> Co-authored-by: ExitMaster <292490062+ExitMaster@users.noreply.github.com>
This commit is contained in:
@@ -1553,6 +1553,21 @@ def execute_code(
|
||||
"Use normal tool calls (terminal, read_file, write_file, ...) instead."
|
||||
)
|
||||
|
||||
# Fail closed under a terminal-policy refusal scope (#68559): the routed
|
||||
# profile's terminal policy could not be resolved and execute_code runs on
|
||||
# the configured terminal backend — refuse rather than inheriting the
|
||||
# launch process's ambient policy.
|
||||
try:
|
||||
from tools.terminal_scope import enforce_no_refusal
|
||||
|
||||
enforce_no_refusal()
|
||||
except Exception as refusal:
|
||||
return tool_error(
|
||||
f"execute_code refused: {refusal} "
|
||||
"(profile terminal policy unresolved; fix the profile's "
|
||||
"config.yaml / .env and retry)"
|
||||
)
|
||||
|
||||
if not code or not code.strip():
|
||||
return tool_error(
|
||||
"No code provided. execute_code requires a non-empty 'code' "
|
||||
@@ -2282,7 +2297,9 @@ def _resolve_child_cwd(mode: str, staging_dir: str, task_id: str = "") -> str:
|
||||
session_cwd = None
|
||||
if session_cwd and os.path.isdir(session_cwd):
|
||||
return session_cwd
|
||||
raw = os.environ.get("TERMINAL_CWD", "").strip()
|
||||
from agent.runtime_cwd import scope_terminal_cwd
|
||||
|
||||
raw = scope_terminal_cwd().strip()
|
||||
if raw:
|
||||
expanded = os.path.expanduser(raw)
|
||||
if os.path.isdir(expanded):
|
||||
|
||||
Reference in New Issue
Block a user