diff --git a/tests/tools/test_docker_environment.py b/tests/tools/test_docker_environment.py index 54bd51f600..3684f237a8 100644 --- a/tests/tools/test_docker_environment.py +++ b/tests/tools/test_docker_environment.py @@ -1,4 +1,5 @@ import logging +import os from io import StringIO import subprocess @@ -294,6 +295,68 @@ def test_runtime_exec_tracks_scope_and_clears_missing_value(monkeypatch): assert "unset SERVICE_TOKEN" in second_cmd[-1] +def test_wrapped_exec_scopes_explicit_forward_env_across_profiles(monkeypatch, tmp_path): + """The shared snapshot must not resurrect an explicit forward-only value.""" + from agent import secret_scope as ss + + env = _make_execute_only_env(forward_env=["EXPLICIT_TOKEN"]) + env.cwd = str(tmp_path) + env._snapshot_path = str(tmp_path / "snapshot.sh") + env._cwd_file = str(tmp_path / "cwd.txt") + env._snapshot_passthrough_names = set() + (tmp_path / "snapshot.sh").write_text( + "export EXPLICIT_TOKEN=stale-from-previous-profile\n", + encoding="utf-8", + ) + monkeypatch.setenv("EXPLICIT_TOKEN", "token-for-default") + monkeypatch.setattr(docker_env, "_load_hermes_env_vars", lambda: {}) + + def _run_fake_docker_exec(cmd, stdin_data=None): + """Execute the generated docker exec command in a real local bash.""" + container_index = cmd.index(env._container_id) + child_env = os.environ.copy() + index = 2 + while index < container_index: + assert cmd[index] == "-e" + key, value = cmd[index + 1].split("=", 1) + child_env[key] = value + index += 2 + assert cmd[container_index + 1 : container_index + 3] == ["bash", "-c"] + return subprocess.Popen( + ["bash", "-c", cmd[container_index + 3]], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + stdin=subprocess.PIPE if stdin_data is not None else subprocess.DEVNULL, + text=True, + encoding="utf-8", + errors="replace", + env=child_env, + ) + + monkeypatch.setattr(docker_env, "_popen_bash", _run_fake_docker_exec) + ss.set_multiplex_active(True) + + try: + for scope, expected in ( + ({"EXPLICIT_TOKEN": "token-for-profile-a"}, "token-for-profile-a"), + ({"EXPLICIT_TOKEN": "token-for-profile-b"}, "token-for-profile-b"), + ({}, "unset"), + ): + scope_token = ss.set_secret_scope(scope) + try: + result = env.execute("printf '%s' \"${EXPLICIT_TOKEN-unset}\"") + finally: + ss.reset_secret_scope(scope_token) + + assert result["returncode"] == 0 + assert result["output"] == expected + assert "EXPLICIT_TOKEN=" not in ( + tmp_path / "snapshot.sh" + ).read_text(encoding="utf-8") + finally: + ss.set_multiplex_active(False) + + # ── docker_env tests ────────────────────────────────────────────── diff --git a/tools/environments/base.py b/tools/environments/base.py index dfeda1421a..c541e52509 100644 --- a/tools/environments/base.py +++ b/tools/environments/base.py @@ -531,6 +531,10 @@ class BaseEnvironment(ABC): # Session snapshot (init_session) # ------------------------------------------------------------------ + def _additional_profile_scoped_passthrough_names(self) -> Iterable[str]: + """Return backend-specific names that must not persist in snapshots.""" + return () + def _snapshot_excluded_passthrough_names(self) -> tuple[str, ...]: """Return profile-scoped names that must not persist in the snapshot. @@ -545,9 +549,13 @@ class BaseEnvironment(ABC): from agent.secret_scope import is_multiplex_active if is_multiplex_active(): from tools.env_passthrough import get_all_passthrough + names = ( + *get_all_passthrough(), + *self._additional_profile_scoped_passthrough_names(), + ) self._snapshot_passthrough_names.update( name - for name in get_all_passthrough() + for name in names if isinstance(name, str) and _SHELL_ENV_NAME_RE.fullmatch(name) ) except Exception: diff --git a/tools/environments/docker.py b/tools/environments/docker.py index 54981eaad5..137d80627c 100644 --- a/tools/environments/docker.py +++ b/tools/environments/docker.py @@ -846,6 +846,10 @@ class DockerEnvironment(BaseEnvironment): _profile_scoped_passthrough = True + def _additional_profile_scoped_passthrough_names(self) -> tuple[str, ...]: + """Keep explicit docker_forward_env values out of shared snapshots.""" + return tuple(self._forward_env) + def __init__( self, image: str,