Files
aiturk-hermes-ide/tests/tools/test_terminal_scope_multiplex.py

185 lines
7.5 KiB
Python

"""Per-turn terminal scope isolation under profile multiplexing (#68559 class).
One multiplexed process serves several profiles, but terminal.* used to
resolve through the process-global ``TERMINAL_*`` env vars bridged once at
startup — so every routed profile inherited the launch profile's backend,
cwd, docker mounts and shared-container key (#68559, #94200, #101132,
#95470). ``tools.terminal_scope`` installs the routed profile's COMPLETE
terminal policy as a ContextVar at each profile boundary; readers resolve
ONLY from it (omitted key → defined default, never ``os.environ``) and an
unresolvable policy fails closed.
"""
import json
import os
import pytest
from tools.terminal_scope import (
TerminalPolicyRefusal,
TerminalPolicyUnavailable,
get_terminal_scope,
install_profile_terminal_scope,
reset_terminal_scope,
set_terminal_scope,
terminal_env,
)
_LAUNCH_CWD = "/home/launch-user/private"
_LAUNCH_VOLUMES = '["/host/secret:/data:rw"]'
@pytest.fixture(autouse=True)
def _polluted_launch_env(monkeypatch, tmp_path):
"""Launch profile A bridged a docker backend with sensitive policy into
the process env; every test proves a routed profile observes none of it."""
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
monkeypatch.setenv("TERMINAL_ENV", "docker")
monkeypatch.setenv("TERMINAL_CWD", _LAUNCH_CWD)
monkeypatch.setenv("TERMINAL_DOCKER_VOLUMES", _LAUNCH_VOLUMES)
monkeypatch.setenv("TERMINAL_DOCKER_SHARED_CONTAINER_KEY", "alpha-shared")
monkeypatch.setenv("TERMINAL_SSH_HOST", "10.10.0.103")
monkeypatch.setattr("agent.secret_scope.build_profile_secret_scope", lambda _h: {})
monkeypatch.setattr("hermes_cli.env_loader.hydrate_profile_secret_sources", lambda _h: None)
import tools.terminal_tool as tt
monkeypatch.setattr(tt, "_terminal_config_bridge_attempted", True)
yield
def _profile(tmp_path, name, config_yaml="", dotenv=""):
home = tmp_path / "profiles" / name
home.mkdir(parents=True)
if config_yaml:
(home / "config.yaml").write_text(config_yaml, encoding="utf-8")
if dotenv:
(home / ".env").write_text(dotenv, encoding="utf-8")
return home
def test_no_scope_keeps_process_env_behavior():
"""Single-process CLI/TUI (no scope bound) is byte-identical to before."""
assert terminal_env("TERMINAL_ENV") == "docker"
assert terminal_env("TERMINAL_SSH_HOST") == "10.10.0.103"
def test_scoped_read_never_falls_through_to_process_env():
"""Omitted key under a scope → defined default, NOT the ambient value."""
token = set_terminal_scope({"TERMINAL_ENV": "local"})
try:
assert terminal_env("TERMINAL_ENV") == "local"
assert terminal_env("TERMINAL_SSH_HOST") == ""
assert terminal_env("TERMINAL_DOCKER_VOLUMES", "[]") == "[]"
assert os.environ["TERMINAL_ENV"] == "docker" # never mutated
finally:
reset_terminal_scope(token)
@pytest.mark.parametrize(
"config_yaml,dotenv",
[
pytest.param("terminal:\n backend: local\n cwd: {cwd}\n", "", id="config-yaml"),
pytest.param("", "TERMINAL_ENV=local\nTERMINAL_CWD={cwd}\n", id="dotenv-only"),
],
)
def test_routed_turn_reads_every_terminal_consumer_from_profile(
tmp_path, config_yaml, dotenv
):
"""Leak matrix through the REAL gateway boundary: a routed local profile
with its own cwd must be seen as such by every terminal.* consumer —
terminal_tool config, container key resolution, docker media translation,
file_tools/runtime_cwd cwd anchors, and the browser/env_probe backend
checks — with none of launch profile A's docker policy showing through."""
import gateway.run as gw
import tools.terminal_tool as tt
from agent import runtime_cwd
from gateway.platforms import base as gbase
from tools import browser_tool, env_probe, file_tools
b_cwd = tmp_path / "b-work"
b_cwd.mkdir()
home = _profile(
tmp_path, "bee",
config_yaml.format(cwd=b_cwd), dotenv.format(cwd=b_cwd),
)
with gw._profile_runtime_scope(home):
cfg = tt._get_env_config()
assert cfg["env_type"] == "local"
assert cfg["cwd"] == str(b_cwd)
assert cfg["docker_volumes"] == []
assert cfg["docker_shared_container_key"] == ""
assert tt._resolve_container_task_id(None) == "default"
assert gbase._parse_docker_volume_mounts() == []
assert not any(
"alpha-shared" in c for c in gbase._docker_sandbox_dir_candidates("agent:bee:x")
)
assert file_tools._configured_terminal_cwd() == str(b_cwd)
assert runtime_cwd.resolve_agent_cwd() == b_cwd
assert browser_tool._is_local_backend() is True
# env_probe bails out with "" for remote backends; a local profile
# must not be treated as remote just because the launch env is docker.
assert env_probe._resolve_terminal_backend() == "local"
assert get_terminal_scope() is None
# Process env untouched — the launch profile's own turns are unchanged.
assert os.environ["TERMINAL_DOCKER_VOLUMES"] == _LAUNCH_VOLUMES
def test_profile_omitting_keys_gets_defaults_not_launch_values(tmp_path):
"""#101132/#95470: a docker profile that does NOT set docker_volumes or
docker_shared_container_key must not inherit the launch profile's."""
import gateway.run as gw
import tools.terminal_tool as tt
home = _profile(tmp_path, "bee", "terminal:\n backend: docker\n")
with gw._profile_runtime_scope(home):
cfg = tt._get_env_config()
assert cfg["env_type"] == "docker"
assert cfg["docker_volumes"] == []
assert cfg["docker_shared_container_key"] == ""
assert cfg["ssh_host"] == ""
assert cfg["cwd"] != _LAUNCH_CWD
assert json.loads(os.environ["TERMINAL_DOCKER_VOLUMES"]) # A unchanged
def test_malformed_profile_config_refuses_execution(tmp_path):
"""Unresolvable policy → refusal scope; terminal_tool refuses instead of
running under the launch process's ambient policy (fail closed)."""
from tools.terminal_tool import terminal_tool
home = _profile(tmp_path, "broken", "terminal: [unclosed\n")
token = install_profile_terminal_scope(home)
try:
assert isinstance(get_terminal_scope(), TerminalPolicyRefusal)
with pytest.raises(TerminalPolicyUnavailable):
terminal_env("TERMINAL_ENV")
result = terminal_tool(command="whoami")
assert "terminal policy unavailable" in result
finally:
reset_terminal_scope(token)
def test_gateway_runtime_scope_resets_on_error(tmp_path):
import gateway.run as gw
home = _profile(tmp_path, "qa", "terminal:\n backend: local\n")
with pytest.raises(RuntimeError):
with gw._profile_runtime_scope(home):
assert terminal_env("TERMINAL_ENV") == "local"
raise RuntimeError("turn blew up")
assert get_terminal_scope() is None
def test_tui_and_cron_boundaries_bind_and_reset(tmp_path):
import tui_gateway.server as server
from tools.terminal_scope import install_and_reset_profile_terminal_scope
home = _profile(tmp_path, "dash", "terminal:\n backend: local\n")
with server._session_profile_runtime_scope({"profile_home": str(home)}):
assert terminal_env("TERMINAL_ENV") == "local"
assert terminal_env("TERMINAL_SSH_HOST") == ""
assert get_terminal_scope() is None
with install_and_reset_profile_terminal_scope(home): # cron fire helper
assert terminal_env("TERMINAL_ENV") == "local"
assert get_terminal_scope() is None