mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-14 16:08:41 +00:00
* Preserve Windows CLI compatibility for local sandbox commands MSYS path conversion must remain disabled for DeerFlow virtual paths, but applying a blanket environment override to every POSIX command breaks host-native CLI shims on Windows. Limit MSYS argument-conversion exclusions to safe non-root virtual path prefixes, omit values that would broaden the exclusion pattern, and document the contract. Constraint: Preserve the virtual-path protection introduced by #2765/#2766 Rejected: Disable MSYS conversion for every command | breaks Windows CLI shims Rejected: Toggle blanket conversion only for commands containing virtual paths | host CLIs can receive virtual-path arguments and still need normal conversion for their own paths Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep regression coverage for virtual-path arguments, root mounts, and host-native CLI launchers Tested: test_local_sandbox_encoding.py (12 passed); related sandbox suite (197 passed, 8 skipped, 7 failures matching origin/main); ruff check; ruff format --check; git diff --check; direct LocalSandbox CLI and virtual-path smoke tests Not-tested: Full offline suite completion; stopped at 6% after unrelated Windows and optional-runtime failures Related: #2765 Related: #2766 * Keep MSYS regression tests portable across CI operating systems The Windows-shell environment tests patched os.name to nt while mounting Windows-specific paths. On Linux and macOS, pathlib then attempted to construct WindowsPath during command resolution or output masking, so the backend merge gate failed before exercising the environment contract. Stub the exclusion boundary in execute-command tests and retain mapping-specific filtering coverage in the helper test. Constraint: Backend unit tests run on Linux, while the behavior under test is Windows-only Rejected: Skip the tests outside Windows | would remove CI coverage of the environment contract Rejected: Patch pathlib internals | couples tests to implementation details and hides the platform boundary Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep OS-specific subprocess assertions independent from host-path resolution Tested: test_local_sandbox_encoding.py (12 passed); ruff check; ruff format --check; git diff --check Not-tested: Linux runner execution locally because Docker Desktop is unavailable and WSL cannot access this linked worktree Related: #5003 Related: https://github.com/bytedance/deer-flow/pullrequestreview-5013380238
256 lines
9.9 KiB
Python
256 lines
9.9 KiB
Python
import builtins
|
|
from types import SimpleNamespace
|
|
|
|
import deerflow.sandbox.local.local_sandbox as local_sandbox
|
|
from deerflow.sandbox.local.local_sandbox import LocalSandbox, PathMapping
|
|
|
|
|
|
def _open(base, file, mode="r", *args, **kwargs):
|
|
if "b" in mode:
|
|
return base(file, mode, *args, **kwargs)
|
|
return base(file, mode, *args, encoding=kwargs.pop("encoding", "gbk"), **kwargs)
|
|
|
|
|
|
def test_read_file_uses_utf8_on_windows_locale(tmp_path, monkeypatch):
|
|
path = tmp_path / "utf8.txt"
|
|
text = "\u201cutf8\u201d"
|
|
path.write_text(text, encoding="utf-8")
|
|
base = builtins.open
|
|
|
|
monkeypatch.setattr(local_sandbox, "open", lambda file, mode="r", *args, **kwargs: _open(base, file, mode, *args, **kwargs), raising=False)
|
|
|
|
assert LocalSandbox("t").read_file(str(path)) == text
|
|
|
|
|
|
def test_write_file_uses_utf8_on_windows_locale(tmp_path, monkeypatch):
|
|
path = tmp_path / "utf8.txt"
|
|
text = "emoji \U0001f600"
|
|
base = builtins.open
|
|
|
|
monkeypatch.setattr(local_sandbox, "open", lambda file, mode="r", *args, **kwargs: _open(base, file, mode, *args, **kwargs), raising=False)
|
|
|
|
LocalSandbox("t").write_file(str(path), text)
|
|
|
|
assert path.read_text(encoding="utf-8") == text
|
|
|
|
|
|
def test_get_shell_prefers_posix_shell_from_path_before_windows_fallback(monkeypatch):
|
|
monkeypatch.setattr(local_sandbox.os, "name", "nt")
|
|
monkeypatch.setattr(LocalSandbox, "_find_first_available_shell", lambda candidates: r"C:\Program Files\Git\bin\sh.exe" if candidates == ("/bin/zsh", "/bin/bash", "/bin/sh", "sh") else None)
|
|
|
|
assert LocalSandbox._get_shell() == r"C:\Program Files\Git\bin\sh.exe"
|
|
|
|
|
|
def test_get_shell_uses_powershell_fallback_on_windows(monkeypatch):
|
|
calls: list[tuple[str, ...]] = []
|
|
|
|
def fake_find(candidates: tuple[str, ...]) -> str | None:
|
|
calls.append(candidates)
|
|
if candidates == ("/bin/zsh", "/bin/bash", "/bin/sh", "sh"):
|
|
return None
|
|
return r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
|
|
|
|
monkeypatch.setattr(local_sandbox.os, "name", "nt")
|
|
monkeypatch.setattr(local_sandbox.os, "environ", {"SystemRoot": r"C:\Windows"})
|
|
monkeypatch.setattr(LocalSandbox, "_find_first_available_shell", fake_find)
|
|
|
|
assert LocalSandbox._get_shell() == r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
|
|
assert calls[1] == (
|
|
"pwsh",
|
|
"pwsh.exe",
|
|
"powershell",
|
|
"powershell.exe",
|
|
r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe",
|
|
"cmd.exe",
|
|
)
|
|
|
|
|
|
def test_get_shell_uses_cmd_as_last_windows_fallback(monkeypatch):
|
|
def fake_find(candidates: tuple[str, ...]) -> str | None:
|
|
if candidates == ("/bin/zsh", "/bin/bash", "/bin/sh", "sh"):
|
|
return None
|
|
return r"C:\Windows\System32\cmd.exe"
|
|
|
|
monkeypatch.setattr(local_sandbox.os, "name", "nt")
|
|
monkeypatch.setattr(local_sandbox.os, "environ", {"SystemRoot": r"C:\Windows"})
|
|
monkeypatch.setattr(LocalSandbox, "_find_first_available_shell", fake_find)
|
|
|
|
assert LocalSandbox._get_shell() == r"C:\Windows\System32\cmd.exe"
|
|
|
|
|
|
def test_execute_command_uses_powershell_command_mode_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(local_sandbox.os, "environ", {"PATH": r"C:\Windows", "OPENAI_API_KEY": "should-not-leak"})
|
|
monkeypatch.setattr(LocalSandbox, "_get_shell", staticmethod(lambda: r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"))
|
|
monkeypatch.setattr(local_sandbox.subprocess, "run", fake_run)
|
|
|
|
output = LocalSandbox("t").execute_command("Write-Output hello")
|
|
|
|
assert output == "ok"
|
|
# Platform secrets are scrubbed from the inherited environment even on the
|
|
# Windows PowerShell path (#3861); benign PATH is preserved and the env is an
|
|
# explicit scrubbed dict, no longer None.
|
|
assert calls == [
|
|
(
|
|
[
|
|
r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe",
|
|
"-NoProfile",
|
|
"-Command",
|
|
"Write-Output hello",
|
|
],
|
|
{
|
|
"shell": False,
|
|
"capture_output": True,
|
|
"text": True,
|
|
"timeout": 600,
|
|
"env": {"PATH": r"C:\Windows"},
|
|
},
|
|
)
|
|
]
|
|
|
|
|
|
def test_execute_command_keeps_msys_path_conversion_for_host_commands_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(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(LocalSandbox, "_msys_path_conversion_exclusions", lambda self: "/mnt/user-data")
|
|
monkeypatch.setattr(local_sandbox.subprocess, "run", fake_run)
|
|
|
|
output = LocalSandbox("t").execute_command("echo hello")
|
|
|
|
assert output == "ok"
|
|
assert calls == [
|
|
(
|
|
[r"C:\Program Files\Git\bin\sh.exe", "-c", "echo hello"],
|
|
{
|
|
"shell": False,
|
|
"capture_output": True,
|
|
"text": True,
|
|
"timeout": 600,
|
|
"env": {
|
|
"PATH": r"C:\Program Files\Git\bin",
|
|
"MSYS2_ARG_CONV_EXCL": "/mnt/user-data",
|
|
},
|
|
},
|
|
)
|
|
]
|
|
|
|
|
|
def test_execute_command_scopes_msys_path_conversion_exclusions_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(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(LocalSandbox, "_msys_path_conversion_exclusions", lambda self: "/mnt/user-data")
|
|
monkeypatch.setattr(local_sandbox.subprocess, "run", fake_run)
|
|
|
|
output = LocalSandbox("t").execute_command("cat /mnt/user-data/workspace/input.txt")
|
|
|
|
assert output == "ok"
|
|
assert calls[0][1]["env"] == {
|
|
"PATH": r"C:\Program Files\Git\bin",
|
|
"MSYS2_ARG_CONV_EXCL": "/mnt/user-data",
|
|
}
|
|
|
|
|
|
def test_execute_command_ignores_root_msys_mapping_for_host_commands_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(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(LocalSandbox, "_msys_path_conversion_exclusions", lambda self: "")
|
|
monkeypatch.setattr(local_sandbox.subprocess, "run", fake_run)
|
|
|
|
output = LocalSandbox("t").execute_command("echo hello")
|
|
|
|
assert output == "ok"
|
|
assert calls[0][1]["env"] == {"PATH": r"C:\Program Files\Git\bin"}
|
|
|
|
|
|
def test_msys_path_conversion_exclusions_omit_blanket_patterns():
|
|
sandbox = LocalSandbox(
|
|
"t",
|
|
[
|
|
PathMapping(container_path="/", local_path="C:\\"),
|
|
PathMapping(container_path="/mnt/data;*", local_path=r"C:\data"),
|
|
PathMapping(container_path="/mnt/user-data/", local_path=r"C:\user-data"),
|
|
PathMapping(container_path="/mnt/user-data", local_path=r"C:\user-data"),
|
|
],
|
|
)
|
|
|
|
assert sandbox._msys_path_conversion_exclusions() == "/mnt/user-data"
|
|
|
|
|
|
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(local_sandbox.os, "environ", {"PATH": r"C:\tools"})
|
|
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"
|
|
# Non-MSYS posix shell adds no MSYS_* vars; the env is the scrubbed inherited
|
|
# environment, not None (#3861).
|
|
assert calls[0][1]["env"] == {"PATH": r"C:\tools"}
|
|
assert "MSYS_NO_PATHCONV" not in calls[0][1]["env"]
|
|
|
|
|
|
def test_execute_command_uses_cmd_command_mode_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(local_sandbox.os, "environ", {"PATH": r"C:\Windows", "GITHUB_TOKEN": "should-not-leak"})
|
|
monkeypatch.setattr(LocalSandbox, "_get_shell", staticmethod(lambda: r"C:\Windows\System32\cmd.exe"))
|
|
monkeypatch.setattr(local_sandbox.subprocess, "run", fake_run)
|
|
|
|
output = LocalSandbox("t").execute_command("echo hello")
|
|
|
|
assert output == "ok"
|
|
# Platform secrets are scrubbed even on the Windows cmd path (#3861); the env
|
|
# is an explicit scrubbed dict, no longer None.
|
|
assert calls == [
|
|
(
|
|
[r"C:\Windows\System32\cmd.exe", "/c", "echo hello"],
|
|
{
|
|
"shell": False,
|
|
"capture_output": True,
|
|
"text": True,
|
|
"timeout": 600,
|
|
"env": {"PATH": r"C:\Windows"},
|
|
},
|
|
)
|
|
]
|