diff --git a/backend/packages/harness/deerflow/sandbox/local/local_sandbox.py b/backend/packages/harness/deerflow/sandbox/local/local_sandbox.py index 116a62159..62577abb9 100644 --- a/backend/packages/harness/deerflow/sandbox/local/local_sandbox.py +++ b/backend/packages/harness/deerflow/sandbox/local/local_sandbox.py @@ -42,6 +42,13 @@ class LocalSandbox(Sandbox): """Return whether the selected shell is cmd.exe.""" return LocalSandbox._shell_name(shell) in {"cmd", "cmd.exe"} + @staticmethod + def _is_msys_shell(shell: str) -> bool: + """Return whether the selected shell is a Git Bash/MSYS shell.""" + normalized = shell.replace("\\", "/").lower() + shell_name = LocalSandbox._shell_name(shell) + return shell_name in {"sh.exe", "bash.exe"} and any(part in normalized for part in ("/git/", "/mingw", "/msys")) + @staticmethod def _find_first_available_shell(candidates: tuple[str, ...]) -> str | None: """Return the first executable shell path or command found from candidates.""" @@ -303,12 +310,19 @@ class LocalSandbox(Sandbox): shell = self._get_shell() if os.name == "nt": + env = None if self._is_powershell(shell): args = [shell, "-NoProfile", "-Command", resolved_command] elif self._is_cmd_shell(shell): args = [shell, "/c", resolved_command] else: args = [shell, "-c", resolved_command] + if self._is_msys_shell(shell): + env = { + **os.environ, + "MSYS_NO_PATHCONV": "1", + "MSYS2_ARG_CONV_EXCL": "*", + } result = subprocess.run( args, @@ -316,6 +330,7 @@ class LocalSandbox(Sandbox): capture_output=True, text=True, timeout=600, + env=env, ) else: args = [shell, "-c", resolved_command] diff --git a/backend/tests/test_local_sandbox_encoding.py b/backend/tests/test_local_sandbox_encoding.py index f1d237379..76a5f187c 100644 --- a/backend/tests/test_local_sandbox_encoding.py +++ b/backend/tests/test_local_sandbox_encoding.py @@ -105,6 +105,7 @@ def test_execute_command_uses_powershell_command_mode_on_windows(monkeypatch): "capture_output": True, "text": True, "timeout": 600, + "env": None, }, ) ] @@ -118,6 +119,7 @@ def test_execute_command_uses_posix_shell_command_mode_on_windows(monkeypatch): return SimpleNamespace(stdout="ok", stderr="", returncode=0) monkeypatch.setattr(local_sandbox.os, "name", "nt") + monkeypatch.setattr(local_sandbox.os, "environ", {"PATH": r"C:\Program Files\Git\bin"}) monkeypatch.setattr(LocalSandbox, "_get_shell", staticmethod(lambda: r"C:\Program Files\Git\bin\sh.exe")) monkeypatch.setattr(local_sandbox.subprocess, "run", fake_run) @@ -132,11 +134,33 @@ def test_execute_command_uses_posix_shell_command_mode_on_windows(monkeypatch): "capture_output": True, "text": True, "timeout": 600, + "env": { + "PATH": r"C:\Program Files\Git\bin", + "MSYS_NO_PATHCONV": "1", + "MSYS2_ARG_CONV_EXCL": "*", + }, }, ) ] +def test_execute_command_does_not_set_msys_env_for_non_msys_posix_shell_on_windows(monkeypatch): + calls: list[tuple[object, dict]] = [] + + def fake_run(*args, **kwargs): + calls.append((args[0], kwargs)) + return SimpleNamespace(stdout="ok", stderr="", returncode=0) + + monkeypatch.setattr(local_sandbox.os, "name", "nt") + monkeypatch.setattr(LocalSandbox, "_get_shell", staticmethod(lambda: r"C:\tools\busybox\sh.exe")) + monkeypatch.setattr(local_sandbox.subprocess, "run", fake_run) + + output = LocalSandbox("t").execute_command("echo /mnt/skills/demo") + + assert output == "ok" + assert calls[0][1]["env"] is None + + def test_execute_command_uses_cmd_command_mode_on_windows(monkeypatch): calls: list[tuple[object, dict]] = [] @@ -159,6 +183,7 @@ def test_execute_command_uses_cmd_command_mode_on_windows(monkeypatch): "capture_output": True, "text": True, "timeout": 600, + "env": None, }, ) ]