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>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-06 02:18:35 +00:00
parent cfad26b684
commit 036035dae0
2 changed files with 35 additions and 8 deletions

View File

@@ -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}"

View File

@@ -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",