From 82132f7911ecf71f27ee5657870bf4105cecf8e2 Mon Sep 17 00:00:00 2001 From: Tranquil-Flow <66773372+Tranquil-Flow@users.noreply.github.com> Date: Mon, 29 Jun 2026 00:07:40 +0200 Subject: [PATCH] fix(file-tools): sanitize host/relative cwd override before it reaches container sandbox (#54447) --- tests/tools/test_container_cwd_sanitize.py | 111 +++++++++++++++++++++ tools/file_tools.py | 22 ++++ 2 files changed, 133 insertions(+) diff --git a/tests/tools/test_container_cwd_sanitize.py b/tests/tools/test_container_cwd_sanitize.py index 5de5bc2011b..00a155e8bb8 100644 --- a/tests/tools/test_container_cwd_sanitize.py +++ b/tests/tools/test_container_cwd_sanitize.py @@ -144,3 +144,114 @@ class TestOverrideCwdSanitizedAtCallSite: # RL/benchmark envs set an in-container path; it must pass through. cwd = self._run_and_capture_cwd(monkeypatch, "/workspace/task42") assert cwd == "/workspace/task42" + + +class TestFileOpsCwdSanitizedAtCallSite: + """E2E pin: file tools (_get_file_ops) must sanitize a host/relative cwd + override before it reaches _create_environment on a container backend — + the same guard the terminal tool got in #50636. Without it, a Desktop/TUI + host cwd (e.g. ``/Users/me/workspace``) leaks straight into + ``docker run -w`` and ``search_files`` returns an empty workspace (#54447). + """ + + def _run_and_capture_cwd(self, monkeypatch, override_cwd, env_type="docker", + config_cwd="/workspace"): + """Drive ``_get_file_ops()`` on a container backend with a host-path cwd + override registered, and return the cwd that reached + ``_create_environment`` (i.e. the cwd passed to ``docker run -w``). + """ + import tools.terminal_tool as tt + import tools.file_tools as ft + + captured = {} + + config = { + "env_type": env_type, + "docker_image": "pytorch/pytorch:latest", + "singularity_image": "docker://pytorch/pytorch:latest", + "modal_image": "pytorch/pytorch:latest", + "daytona_image": "pytorch/pytorch:latest", + "cwd": config_cwd, + "host_cwd": None, + "timeout": 180, + "lifetime_seconds": 300, + "container_cpu": 1, + "container_memory": 5120, + "container_disk": 51200, + "container_persistent": True, + "docker_volumes": [], + "docker_env": {}, + "docker_extra_args": [], + "docker_mount_cwd_to_workspace": False, + "docker_run_as_host_user": False, + "docker_forward_env": [], + "modal_mode": "auto", + "ssh_host": "", + "ssh_user": "", + "ssh_port": 22, + "ssh_key": "", + "ssh_persistent": False, + "local_persistent": False, + } + + class _DummyEnv: + cwd = config_cwd + + def execute(self, *a, **k): + return {"output": "", "exit_code": 0} + + def fake_create_environment(env_type, image, cwd, timeout, **kwargs): + captured["cwd"] = cwd + return _DummyEnv() + + monkeypatch.setattr(tt, "_get_env_config", lambda: config) + monkeypatch.setattr(tt, "_start_cleanup_thread", lambda: None) + monkeypatch.setattr(tt, "_create_environment", fake_create_environment) + # Force a fresh environment build. + monkeypatch.setattr(tt, "_active_environments", {}) + monkeypatch.setattr(tt, "_last_activity", {}) + monkeypatch.setattr(ft, "_file_ops_cache", {}) + monkeypatch.setattr(ft, "_last_known_cwd", {}) + + task_id = "sess-fileops-host-cwd" + tt.register_task_env_overrides(task_id, {"cwd": override_cwd}) + try: + ft._get_file_ops(task_id) + finally: + tt.clear_task_env_overrides(task_id) + return captured.get("cwd") + + def test_macos_host_override_does_not_reach_container(self, monkeypatch): + # Desktop/TUI registers /Users//workspace as the session cwd. + cwd = self._run_and_capture_cwd(monkeypatch, "/Users/me/workspace") + assert cwd == "/workspace", ( + f"Host-path cwd override leaked to the container builder: {cwd!r}. " + "It must be sanitized back to config['cwd']." + ) + + def test_posix_home_host_override_does_not_reach_container(self, monkeypatch): + cwd = self._run_and_capture_cwd(monkeypatch, "/home/someuser/project") + assert cwd == "/workspace" + + def test_windows_host_override_does_not_reach_container(self, monkeypatch): + cwd = self._run_and_capture_cwd(monkeypatch, r"C:\Users\someuser") + assert cwd == "/workspace" + + def test_relative_cwd_override_does_not_reach_container(self, monkeypatch): + cwd = self._run_and_capture_cwd(monkeypatch, "src/app") + assert cwd == "/workspace" + + def test_valid_container_override_is_preserved(self, monkeypatch): + # RL/benchmark envs set an in-container path; it must pass through. + cwd = self._run_and_capture_cwd(monkeypatch, "/workspace/task42") + assert cwd == "/workspace/task42" + + def test_host_override_sanitized_on_singularity(self, monkeypatch): + cwd = self._run_and_capture_cwd( + monkeypatch, "/Users/me/workspace", env_type="singularity") + assert cwd == "/workspace" + + def test_host_override_sanitized_on_modal(self, monkeypatch): + cwd = self._run_and_capture_cwd( + monkeypatch, "/Users/me/workspace", env_type="modal") + assert cwd == "/workspace" diff --git a/tools/file_tools.py b/tools/file_tools.py index bc268c37d85..4d2b3340afb 100644 --- a/tools/file_tools.py +++ b/tools/file_tools.py @@ -832,6 +832,8 @@ def _get_file_ops(task_id: str = "default") -> ShellFileOperations: _creation_locks, _creation_locks_lock, _resolve_container_task_id, + _is_unusable_container_cwd, + _CONTAINER_BACKENDS, ) import time @@ -893,6 +895,26 @@ def _get_file_ops(task_id: str = "default") -> ShellFileOperations: image = "" cwd = overrides.get("cwd") or _last_known_cwd.get(task_id) or config["cwd"] + # Re-apply the container cwd guard that _get_env_config() already + # ran on config["cwd"] (see #50636). A per-task cwd override + # registered by the gateway/TUI/ACP for workspace tracking is a + # raw host path (e.g. a Desktop session's /Users//workspace or + # C:\\Users\\). On a container backend that reaches + # ``docker run -w `` and the container starts in a + # directory that doesn't exist inside the sandbox, so search_files + # and friends silently return empty results (#54447). Sanitize it + # back to the already-validated config["cwd"] so the override can't + # bypass the guard. Valid in-container override paths (RL/benchmark + # sandboxes that set cwd to /workspace, /root, etc.) are absolute + # non-host paths and pass through untouched. + if env_type in _CONTAINER_BACKENDS and _is_unusable_container_cwd(cwd): + if cwd != config["cwd"]: + logger.info( + "Ignoring host/relative cwd override %r for %s backend " + "(won't exist in sandbox). Using %r instead.", + cwd, env_type, config["cwd"], + ) + cwd = config["cwd"] logger.info("Creating new %s environment for task %s...", env_type, task_id[:8]) container_config = None