From 4d6686c18abd255c64de4598e2de1ebaf33bc06a Mon Sep 17 00:00:00 2001 From: Gaurav Saxena Date: Thu, 2 Jul 2026 04:42:26 +0000 Subject: [PATCH] fix(execute-code): honor session cwd overrides --- tests/tools/test_code_execution_modes.py | 33 +++++++++++++++++++ tools/code_execution_tool.py | 42 ++++++++++++++++-------- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/tests/tools/test_code_execution_modes.py b/tests/tools/test_code_execution_modes.py index e5e2d2262ffa..2b05d2d3f2df 100644 --- a/tests/tools/test_code_execution_modes.py +++ b/tests/tools/test_code_execution_modes.py @@ -220,6 +220,17 @@ class TestResolveChildCwd(unittest.TestCase): with patch.dict(os.environ, {"TERMINAL_CWD": "~"}): self.assertEqual(_resolve_child_cwd("project", "/tmp/staging"), home) + def test_project_prefers_registered_task_cwd_override(self): + import tempfile + import tools.terminal_tool as terminal_tool + + with tempfile.TemporaryDirectory() as td: + task_id = "session-cwd-test" + with patch.dict(os.environ, {"TERMINAL_CWD": "/does/not/exist"}): + with patch.object(terminal_tool, "_task_env_overrides", {}, create=False): + terminal_tool.register_task_env_overrides(task_id, {"cwd": td}) + self.assertEqual(_resolve_child_cwd("project", "/tmp/staging", task_id=task_id), td) + # --------------------------------------------------------------------------- # Schema description @@ -316,6 +327,28 @@ class TestExecuteCodeModeIntegration(unittest.TestCase): os.path.realpath(td), ) + def test_project_mode_uses_registered_session_cwd_override(self): + """Project mode must honor session.cwd.set-style overrides even when + TERMINAL_CWD is absent or points elsewhere.""" + import tempfile + import tools.terminal_tool as terminal_tool + + with tempfile.TemporaryDirectory() as td: + task_id = "session-cwd-test" + with patch.dict(os.environ, {"TERMINAL_CWD": "/does/not/exist"}): + with patch.object(terminal_tool, "_task_env_overrides", {}, create=False): + terminal_tool.register_task_env_overrides(task_id, {"cwd": td}) + with _mock_mode("project"): + with patch("model_tools.handle_function_call", side_effect=_mock_handle_function_call): + raw = execute_code( + code="import os; print(os.getcwd())", + task_id=task_id, + enabled_tools=list(SANDBOX_ALLOWED_TOOLS), + ) + result = json.loads(raw) + self.assertEqual(result["status"], "success") + self.assertEqual(os.path.realpath(result["output"].strip()), os.path.realpath(td)) + def test_project_mode_interpreter_is_venv_python(self): """Project mode: sys.executable inside the child is the venv's python when VIRTUAL_ENV is set to a real venv.""" diff --git a/tools/code_execution_tool.py b/tools/code_execution_tool.py index 5915e97ba944..7c3a302c7c0a 100644 --- a/tools/code_execution_tool.py +++ b/tools/code_execution_tool.py @@ -1339,7 +1339,7 @@ def execute_code( # Env scrubbing and tool whitelist apply identically in both modes. _mode = _get_execution_mode() _child_python = _resolve_child_python(_mode) - _child_cwd = _resolve_child_cwd(_mode, tmpdir, task_id=task_id) + _child_cwd = _resolve_child_cwd(_mode, tmpdir, task_id=task_id or "") _script_path = os.path.join(tmpdir, "script.py") proc = subprocess.Popen( @@ -1745,29 +1745,43 @@ def _resolve_child_python(mode: str) -> str: return sys.executable -def _resolve_child_cwd(mode: str, staging_dir: str, task_id: Optional[str] = None) -> str: +def _resolve_child_cwd(mode: str, staging_dir: str, task_id: str = "") -> str: """Resolve the working directory for the execute_code subprocess. - ``strict``: the staging tmpdir (today's behavior). - - ``project``: the session's registered cwd override (from - ``session.cwd.set``), then ``TERMINAL_CWD``, then ``os.getcwd()``. - Falls back to the staging tmpdir as a last resort so we never invoke - Popen with a nonexistent cwd. + - ``project``: the session's own cwd — its per-session cwd record + (written after every completed terminal command), then the raw + per-session cwd override registered via ``session.cwd.set`` / + ``register_task_env_overrides``, then the session's TERMINAL_CWD + (same as the terminal tool), or ``os.getcwd()`` if none points at a + real dir. Falls back to the staging tmpdir as a last resort so we + never invoke Popen with a nonexistent cwd. + + This mirrors the resolution ladder file tools and the terminal use + (record → registered override → TERMINAL_CWD), so all file-writing + paths within a session agree on the working directory. (#56047) """ if mode != "project": return staging_dir - # Check per-session cwd override first (registered via session.cwd.set - # → register_task_env_overrides). This is the same lookup used by - # write_file/read_file/patch/terminal so all tool paths agree on the - # working directory within a session. (#56047) if task_id: + # 1. The session's cwd record — IS the session's `cd` state. + try: + from tools.terminal_tool import get_session_cwd + + recorded = get_session_cwd(task_id) + except Exception: + recorded = None + if recorded and os.path.isdir(recorded): + return recorded + # 2. Registered workspace override (session.cwd.set → gateway/TUI/ACP). try: from tools.file_tools import _registered_task_cwd_override - override = _registered_task_cwd_override(task_id) - if override and os.path.isdir(override): - return override + + session_cwd = _registered_task_cwd_override(task_id) except Exception: - pass + session_cwd = None + if session_cwd and os.path.isdir(session_cwd): + return session_cwd raw = os.environ.get("TERMINAL_CWD", "").strip() if raw: expanded = os.path.expanduser(raw)