From 036035dae0584296c09e8806140385ae3a977f51 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 02:18:35 +0000 Subject: [PATCH] fix(sandbox): preserve PermissionError messages and allow /mnt/user-data root in resolve_local_tool_path Co-authored-by: WillemJiang <219644+WillemJiang@users.noreply.github.com> --- backend/src/sandbox/tools.py | 21 ++++++++++++------- backend/tests/test_sandbox_tools_security.py | 22 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/backend/src/sandbox/tools.py b/backend/src/sandbox/tools.py index cd6a79a..bad9a19 100644 --- a/backend/src/sandbox/tools.py +++ b/backend/src/sandbox/tools.py @@ -144,6 +144,11 @@ def resolve_local_tool_path(path: str, thread_data: ThreadDataState | None) -> s if not allowed_roots: raise SandboxRuntimeError("No allowed local sandbox directories configured") + # Also allow the virtual root itself (/mnt/user-data) to map to the common parent dir. + common_parent = _thread_virtual_to_actual_mappings(thread_data).get(VIRTUAL_PATH_PREFIX) + if common_parent is not None: + allowed_roots.append(Path(common_parent).resolve()) + for root in allowed_roots: try: resolved.relative_to(root) @@ -405,8 +410,8 @@ def ls_tool(runtime: ToolRuntime[ContextT, ThreadState], description: str, path: return f"Error: {e}" except FileNotFoundError: return f"Error: Directory not found: {requested_path}" - except PermissionError: - return f"Error: Permission denied: {requested_path}" + except PermissionError as e: + return f"Error: {e}" except Exception as e: return f"Error: Unexpected error listing directory: {type(e).__name__}: {e}" @@ -444,8 +449,8 @@ def read_file_tool( return f"Error: {e}" except FileNotFoundError: return f"Error: File not found: {requested_path}" - except PermissionError: - return f"Error: Permission denied reading file: {requested_path}" + except PermissionError as e: + return f"Error: {e}" except IsADirectoryError: return f"Error: Path is a directory, not a file: {requested_path}" except Exception as e: @@ -478,8 +483,8 @@ def write_file_tool( return "OK" except SandboxError as e: return f"Error: {e}" - except PermissionError: - return f"Error: Permission denied writing to file: {requested_path}" + except PermissionError as e: + return f"Error: {e}" except IsADirectoryError: return f"Error: Path is a directory, not a file: {requested_path}" except OSError as e: @@ -529,7 +534,7 @@ def str_replace_tool( return f"Error: {e}" except FileNotFoundError: return f"Error: File not found: {requested_path}" - except PermissionError: - return f"Error: Permission denied accessing file: {requested_path}" + except PermissionError as e: + return f"Error: {e}" except Exception as e: return f"Error: Unexpected error replacing string: {type(e).__name__}: {e}" diff --git a/backend/tests/test_sandbox_tools_security.py b/backend/tests/test_sandbox_tools_security.py index 651701d..145a173 100644 --- a/backend/tests/test_sandbox_tools_security.py +++ b/backend/tests/test_sandbox_tools_security.py @@ -36,6 +36,28 @@ def test_mask_local_paths_in_output_hides_host_paths() -> None: assert "/mnt/user-data/workspace/result.txt" in masked +def test_resolve_local_tool_path_resolves_valid_virtual_path() -> None: + thread_data = { + "workspace_path": "/tmp/deer-flow/threads/t1/user-data/workspace", + "uploads_path": "/tmp/deer-flow/threads/t1/user-data/uploads", + "outputs_path": "/tmp/deer-flow/threads/t1/user-data/outputs", + } + + result = resolve_local_tool_path("/mnt/user-data/workspace/report.txt", thread_data) + assert result == "/tmp/deer-flow/threads/t1/user-data/workspace/report.txt" + + +def test_resolve_local_tool_path_allows_virtual_root() -> None: + thread_data = { + "workspace_path": "/tmp/deer-flow/threads/t1/user-data/workspace", + "uploads_path": "/tmp/deer-flow/threads/t1/user-data/uploads", + "outputs_path": "/tmp/deer-flow/threads/t1/user-data/outputs", + } + + result = resolve_local_tool_path("/mnt/user-data", thread_data) + assert result == "/tmp/deer-flow/threads/t1/user-data" + + def test_resolve_local_tool_path_rejects_non_virtual_path() -> None: thread_data = { "workspace_path": "/tmp/deer-flow/threads/t1/user-data/workspace",