diff --git a/backend/packages/harness/deerflow/sandbox/local/local_sandbox.py b/backend/packages/harness/deerflow/sandbox/local/local_sandbox.py index eaf1ed0ad..ebadbf7b8 100644 --- a/backend/packages/harness/deerflow/sandbox/local/local_sandbox.py +++ b/backend/packages/harness/deerflow/sandbox/local/local_sandbox.py @@ -376,7 +376,9 @@ class LocalSandbox(Sandbox): def replace_match(match: re.Match) -> str: matched_path = match.group(0) - return self._resolve_path(matched_path) + # Normalize to forward slashes so bash doesn't interpret Windows + # backslash sequences (\\U, \\a, \\d, \\s, \\n, \\t) as escapes. + return self._resolve_path(matched_path).replace("\\", "/") return pattern.sub(replace_match, command) diff --git a/backend/packages/harness/deerflow/sandbox/tools.py b/backend/packages/harness/deerflow/sandbox/tools.py index 1440bb8e3..5217d80d7 100644 --- a/backend/packages/harness/deerflow/sandbox/tools.py +++ b/backend/packages/harness/deerflow/sandbox/tools.py @@ -1049,7 +1049,7 @@ def replace_virtual_paths_in_command(command: str, thread_data: ThreadDataState skills_pattern = re.compile(rf"{re.escape(skills_container)}(/[^\s\"';&|<>()]*)?") def replace_skills_match(match: re.Match) -> str: - return _resolve_skills_path(match.group(0)) + return _resolve_skills_path(match.group(0)).replace("\\", "/") result = skills_pattern.sub(replace_skills_match, result) @@ -1060,7 +1060,7 @@ def replace_virtual_paths_in_command(command: str, thread_data: ThreadDataState acp_pattern = re.compile(rf"{re.escape(_ACP_WORKSPACE_VIRTUAL_PATH)}(/[^\s\"';&|<>()]*)?") def replace_acp_match(match: re.Match, _tid: str | None = _thread_id) -> str: - return _resolve_acp_workspace_path(match.group(0), _tid) + return _resolve_acp_workspace_path(match.group(0), _tid).replace("\\", "/") result = acp_pattern.sub(replace_acp_match, result) @@ -1071,7 +1071,7 @@ def replace_virtual_paths_in_command(command: str, thread_data: ThreadDataState pattern = re.compile(rf"{re.escape(VIRTUAL_PATH_PREFIX)}(/[^\s\"';&|<>()]*)?") def replace_user_data_match(match: re.Match) -> str: - return replace_virtual_path(match.group(0), thread_data) + return replace_virtual_path(match.group(0), thread_data).replace("\\", "/") result = pattern.sub(replace_user_data_match, result) diff --git a/backend/tests/test_sandbox_windows_path_normalization.py b/backend/tests/test_sandbox_windows_path_normalization.py new file mode 100644 index 000000000..211537cd6 --- /dev/null +++ b/backend/tests/test_sandbox_windows_path_normalization.py @@ -0,0 +1,79 @@ +"""Regression tests for Windows backslash path normalization. + +Ensures that replace_virtual_paths_in_command and LocalSandbox._resolve_paths_in_command +return forward-slash paths when the host paths use backslashes (Windows). +""" + +from unittest.mock import patch + +from deerflow.sandbox.local.local_sandbox import LocalSandbox, PathMapping +from deerflow.sandbox.tools import replace_virtual_paths_in_command + +# Windows-style thread data with backslash paths +_WIN_THREAD_DATA = { + "workspace_path": r"C:\Users\admin\deer-flow\backend\.deer-flow\users\user1\threads\t1\user-data\workspace", + "uploads_path": r"C:\Users\admin\deer-flow\backend\.deer-flow\users\user1\threads\t1\user-data\uploads", + "outputs_path": r"C:\Users\admin\deer-flow\backend\.deer-flow\users\user1\threads\t1\user-data\outputs", +} + + +class TestReplaceVirtualPathsWindows: + """replace_virtual_paths_in_command must normalize backslashes to forward slashes.""" + + def test_user_data_workspace_no_backslash(self) -> None: + cmd = "cat /mnt/user-data/workspace/data.json" + result = replace_virtual_paths_in_command(cmd, _WIN_THREAD_DATA) + assert "\\" not in result, f"Backslash in: {result}" + + def test_user_data_outputs_no_backslash(self) -> None: + cmd = "ls /mnt/user-data/outputs/report.html" + result = replace_virtual_paths_in_command(cmd, _WIN_THREAD_DATA) + assert "\\" not in result, f"Backslash in: {result}" + + def test_user_data_subdir_no_backslash(self) -> None: + cmd = "cat /mnt/user-data/workspace/subdir/file.txt" + result = replace_virtual_paths_in_command(cmd, _WIN_THREAD_DATA) + assert "\\" not in result, f"Backslash in: {result}" + + @patch("deerflow.sandbox.tools._get_skills_host_path", return_value=r"C:\Users\admin\deer-flow\skills") + @patch("deerflow.sandbox.tools._get_skills_container_path", return_value="/mnt/skills") + def test_skills_path_no_backslash(self, _mock_container, _mock_host) -> None: + cmd = "python /mnt/skills/custom/skill/scripts/run.py" + result = replace_virtual_paths_in_command(cmd, _WIN_THREAD_DATA) + assert "\\" not in result, f"Backslash in: {result}" + + @patch("deerflow.sandbox.tools._resolve_acp_workspace_path", return_value=r"C:\Users\admin\deer-flow\acp-workspace\data.json") + @patch("deerflow.sandbox.tools._get_acp_workspace_host_path", return_value=r"C:\Users\admin\deer-flow\acp-workspace") + def test_acp_workspace_no_backslash(self, _mock_acp_host, _mock_resolve_acp) -> None: + cmd = "cat /mnt/acp-workspace/data.json" + result = replace_virtual_paths_in_command(cmd, _WIN_THREAD_DATA) + assert "\\" not in result, f"Backslash in: {result}" + assert "C:/Users/admin/deer-flow/acp-workspace/data.json" in result + + +class TestLocalSandboxResolvePathsInCommandWindows: + """LocalSandbox._resolve_paths_in_command must normalize backslashes.""" + + def test_custom_mount_no_backslash(self) -> None: + sandbox = LocalSandbox( + "test", + path_mappings=[ + PathMapping(container_path="/mnt/models", local_path=r"C:\Users\admin\models", read_only=True), + ], + ) + cmd = "cat /mnt/models/weights.bin" + result = sandbox._resolve_paths_in_command(cmd) + assert "\\" not in result, f"Backslash in: {result}" + assert "C:/Users/admin/models/weights.bin" in result + + def test_user_data_no_backslash(self) -> None: + sandbox = LocalSandbox( + "test", + path_mappings=[ + PathMapping(container_path="/mnt/user-data", local_path=r"C:\Users\admin\data"), + ], + ) + cmd = "ls /mnt/user-data/workspace/file.txt" + result = sandbox._resolve_paths_in_command(cmd) + assert "\\" not in result, f"Backslash in: {result}" + assert "C:/Users/admin/data/workspace/file.txt" in result