diff --git a/src/agent_loop.py b/src/agent_loop.py index efc6b4e05..f3609b41b 100644 --- a/src/agent_loop.py +++ b/src/agent_loop.py @@ -16711,16 +16711,15 @@ def _existing_workspace_files(paths: list[str], workspace: Optional[str]) -> lis """ if not workspace: return [] - root = Path(str(workspace)).expanduser() + from src.tool_execution import _resolve_tool_path_in_workspace + existing: list[str] = [] for path in paths: - candidate = Path(path).expanduser() - if not candidate.is_absolute(): - candidate = root / candidate try: - if candidate.is_file(): + resolved = _resolve_tool_path_in_workspace(str(workspace), str(path)) + if os.path.isfile(resolved): existing.append(path) - except OSError: + except (OSError, ValueError): continue return existing diff --git a/tests/test_existing_workspace_files_confinement.py b/tests/test_existing_workspace_files_confinement.py new file mode 100644 index 000000000..5b528a665 --- /dev/null +++ b/tests/test_existing_workspace_files_confinement.py @@ -0,0 +1,76 @@ +from src.agent_loop import _existing_workspace_files + + +def test_existing_workspace_files_accepts_relative_file_inside_workspace(tmp_path): + workspace = tmp_path / "workspace" + workspace.mkdir() + target = workspace / "inside.py" + target.write_text("pass\n") + + assert _existing_workspace_files( + ["inside.py"], + str(workspace), + ) == ["inside.py"] + + +def test_existing_workspace_files_accepts_absolute_file_inside_workspace(tmp_path): + workspace = tmp_path / "workspace" + workspace.mkdir() + target = workspace / "inside.py" + target.write_text("pass\n") + + assert _existing_workspace_files( + [str(target)], + str(workspace), + ) == [str(target)] + + +def test_existing_workspace_files_rejects_absolute_file_outside_workspace(tmp_path): + workspace = tmp_path / "workspace" + workspace.mkdir() + outside = tmp_path / "outside.py" + outside.write_text("pass\n") + + assert _existing_workspace_files( + [str(outside)], + str(workspace), + ) == [] + + +def test_existing_workspace_files_rejects_parent_traversal(tmp_path): + workspace = tmp_path / "workspace" + workspace.mkdir() + outside = tmp_path / "outside.py" + outside.write_text("pass\n") + + assert _existing_workspace_files( + ["../outside.py"], + str(workspace), + ) == [] + + +def test_existing_workspace_files_rejects_symlink_escape(tmp_path): + workspace = tmp_path / "workspace" + workspace.mkdir() + outside = tmp_path / "outside.py" + outside.write_text("pass\n") + + escape = workspace / "escape.py" + escape.symlink_to(outside) + + assert _existing_workspace_files( + ["escape.py"], + str(workspace), + ) == [] + + +def test_existing_workspace_files_supports_workspace_alias(tmp_path): + workspace = tmp_path / "workspace" + workspace.mkdir() + target = workspace / "inside.py" + target.write_text("pass\n") + + assert _existing_workspace_files( + ["/workspace/inside.py"], + str(workspace), + ) == ["/workspace/inside.py"]