mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-09 13:39:26 +00:00
fix: improve local environment detection guidance (#5111)
Co-authored-by: angus-guo <217034332+angus-guo@users.noreply.github.com>
This commit is contained in:
parent
9e0fbd60fa
commit
47f43f79f4
@ -478,6 +478,8 @@ DeerFlow supports multiple sandbox execution modes:
|
||||
- **Docker Execution** (runs sandbox code in isolated Docker containers)
|
||||
- **Docker Execution with Kubernetes** (runs sandbox code in Kubernetes pods via provisioner service)
|
||||
|
||||
When host Bash is enabled for Local Execution, DeerFlow starts OS detection with `uname -s`, then uses `sw_vers` on Darwin. On Linux, it reads host system files such as `/etc/os-release` only when the active sandbox policy permits it. Host filesystem path checks still apply; after a blocked path, the agent is directed to use a permitted command-only probe or virtual path instead of repeating the rejected command.
|
||||
|
||||
For Docker development, service startup follows `config.yaml` sandbox mode. In Local/Docker modes, `provisioner` is not started.
|
||||
|
||||
See the [Sandbox Configuration Guide](backend/docs/CONFIGURATION.md#sandbox) to configure your preferred mode.
|
||||
|
||||
@ -95,7 +95,7 @@
|
||||
|
||||
**Sandbox Tools** (in `packages/harness/deerflow/sandbox/tools.py`):
|
||||
- Every sandbox tool keeps a model-visible `description` field for a human-readable progress label, but the field is optional and defaults to an empty string. Tool execution must depend only on its operational arguments; the frontend supplies localized fallback labels when a provider omits `description`.
|
||||
- `bash` - Execute commands with path translation and error handling. For `LocalSandbox` (host bash), output on POSIX and Windows is captured through bounded pipe-drain threads and stdin is `/dev/null`; Windows capture decodes with the platform text encoding and applies universal-newline translation, matching the former `subprocess.run(..., text=True)` behavior for locale-code-page output, Python UTF-8 Mode, CRLF, and bare CR. That translation is Windows-only so the pre-existing POSIX output contract remains byte-decoded without newline rewriting. On POSIX, a backgrounded long-lived process (`server &`) returns immediately instead of blocking the turn on an inherited pipe, while unredirected background output is drained without growing anonymous temp files. Commands that read stdin get immediate EOF. The command runs in its own process group with a wall-clock timeout (`sandbox.bash_command_timeout`, default 600s); on timeout the whole POSIX process group or Windows process tree is killed and the agent gets a notice telling it to background long-lived processes. The bash tool description itself also instructs the model to background long-lived processes (e.g. servers) up front so it doesn't waste the turn waiting on a foreground server. See `LocalSandbox.execute_command`, its platform runners, and `bash_tool`'s docstring.
|
||||
- `bash` - Execute commands with path translation and error handling. For `LocalSandbox` (host bash), output on POSIX and Windows is captured through bounded pipe-drain threads and stdin is `/dev/null`; Windows capture decodes with the platform text encoding and applies universal-newline translation, matching the former `subprocess.run(..., text=True)` behavior for locale-code-page output, Python UTF-8 Mode, CRLF, and bare CR. That translation is Windows-only so the pre-existing POSIX output contract remains byte-decoded without newline rewriting. On POSIX, a backgrounded long-lived process (`server &`) returns immediately instead of blocking the turn on an inherited pipe, while unredirected background output is drained without growing anonymous temp files. Commands that read stdin get immediate EOF. The command runs in its own process group with a wall-clock timeout (`sandbox.bash_command_timeout`, default 600s); on timeout the whole POSIX process group or Windows process tree is killed and the agent gets a notice telling it to background long-lived processes. The shared bash tool description scopes host environment detection to LocalSandbox: start with `uname -s`, follow with `sw_vers` on Darwin, and read Linux host system files only when the active policy permits them. Local path and `file://` rejections provide the same conditional recovery guidance: command-only probes for environment questions, allowed virtual paths otherwise, and no repetition of the rejected path. The description also instructs the model to background long-lived processes (e.g. servers) up front so it doesn't waste the turn waiting on a foreground server. See `LocalSandbox.execute_command`, its platform runners, and `bash_tool`'s docstring.
|
||||
- `ls` - Directory listing (tree format, max 2 levels)
|
||||
- `glob` - Find files or directories below a root directory with bounded results
|
||||
- `grep` - Search one text file or recursively search a directory, with optional glob filtering and bounded line-level results
|
||||
|
||||
@ -67,6 +67,7 @@ _FILE_URL_PATTERN = re.compile(r"\bfile://\S+", re.IGNORECASE)
|
||||
_URL_WITH_SCHEME_PATTERN = re.compile(r"^[a-z][a-z0-9+.-]*://", re.IGNORECASE)
|
||||
_URL_IN_COMMAND_PATTERN = re.compile(r"\b[a-z][a-z0-9+.-]*://[^\s\"'`;&|<>()]+", re.IGNORECASE)
|
||||
_DOTDOT_PATH_SEGMENT_PATTERN = re.compile(r"(?:^|[/\\=])\.\.(?:$|[/\\])")
|
||||
_LOCAL_BASH_PATH_RECOVERY_GUIDANCE = "For environment questions, use command-only probes such as uname; otherwise use an allowed virtual path. Do not repeat the rejected path."
|
||||
_LOCAL_BASH_SYSTEM_PATH_PREFIXES = (
|
||||
"/bin/",
|
||||
"/usr/bin/",
|
||||
@ -1261,7 +1262,7 @@ def validate_local_bash_command_paths(command: str, thread_data: ThreadDataState
|
||||
# Block file:// URLs which bypass the absolute-path regex but allow local file exfiltration
|
||||
file_url_match = _FILE_URL_PATTERN.search(command)
|
||||
if file_url_match:
|
||||
raise PermissionError(f"Unsafe file:// URL in command: {file_url_match.group()}. Use paths under {VIRTUAL_PATH_PREFIX}")
|
||||
raise PermissionError(f"Unsafe file:// URL in command: {file_url_match.group()}. Use paths under {VIRTUAL_PATH_PREFIX}. {_LOCAL_BASH_PATH_RECOVERY_GUIDANCE}")
|
||||
|
||||
unsafe_paths: list[str] = []
|
||||
allowed_paths = _get_mcp_allowed_paths()
|
||||
@ -1281,7 +1282,7 @@ def validate_local_bash_command_paths(command: str, thread_data: ThreadDataState
|
||||
|
||||
if unsafe_paths:
|
||||
unsafe = ", ".join(sorted(dict.fromkeys(unsafe_paths)))
|
||||
raise PermissionError(f"Unsafe absolute paths in command: {unsafe}. Use paths under {VIRTUAL_PATH_PREFIX}")
|
||||
raise PermissionError(f"Unsafe absolute paths in command: {unsafe}. Use paths under {VIRTUAL_PATH_PREFIX}. {_LOCAL_BASH_PATH_RECOVERY_GUIDANCE}")
|
||||
|
||||
|
||||
def replace_virtual_paths_in_command(command: str, thread_data: ThreadDataState | None) -> str:
|
||||
@ -2004,12 +2005,18 @@ def _lark_cli_env_from_runtime(runtime: Runtime, command: str, *, sandbox_paths:
|
||||
|
||||
@tool("bash", parse_docstring=True)
|
||||
def bash_tool(runtime: Runtime, command: str, description: str = "") -> str:
|
||||
"""Execute a bash command in a Linux environment.
|
||||
"""Execute a bash command in the configured execution environment.
|
||||
|
||||
|
||||
- Use `python` to run Python code.
|
||||
- Prefer a thread-local virtual environment in `/mnt/user-data/workspace/.venv`.
|
||||
- Use `python -m pip` (inside the virtual environment) to install Python packages.
|
||||
- When running against the local host via host bash, inspect the current environment instead of
|
||||
guessing. For OS detection, start with `uname -s`; on Darwin follow with `sw_vers`. On Linux,
|
||||
start with `uname -a` and read host system files such as `/etc/os-release` only when the active
|
||||
sandbox policy permits it.
|
||||
- If local host bash rejects a path, do not repeat the rejected command. For environment questions,
|
||||
retry with command-only probes; otherwise use allowed virtual paths or explain the restriction.
|
||||
- To start a long-lived process such as a web server, ALWAYS run it in the background with its
|
||||
output redirected, e.g. `your-command > /mnt/user-data/workspace/server.log 2>&1 &`, then check
|
||||
the log file or poll the port. A long-lived process run in the foreground blocks the turn until
|
||||
|
||||
@ -613,6 +613,17 @@ def test_validate_local_bash_command_paths_blocks_host_paths() -> None:
|
||||
validate_local_bash_command_paths("cat /etc/passwd", _THREAD_DATA)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("command", ["cat /etc/os-release", "curl file:///etc/os-release"])
|
||||
def test_validate_local_bash_command_paths_guides_environment_probe_recovery(command: str) -> None:
|
||||
with pytest.raises(PermissionError) as exc_info:
|
||||
validate_local_bash_command_paths(command, _THREAD_DATA)
|
||||
|
||||
message = str(exc_info.value)
|
||||
assert "For environment questions, use command-only probes" in message
|
||||
assert "otherwise use an allowed virtual path" in message
|
||||
assert "Do not repeat the rejected path" in message
|
||||
|
||||
|
||||
def test_validate_local_bash_command_paths_allows_https_urls() -> None:
|
||||
"""URLs like https://github.com/... must not be flagged as unsafe absolute paths."""
|
||||
validate_local_bash_command_paths(
|
||||
@ -822,6 +833,38 @@ def test_bash_tool_rejects_host_bash_when_local_sandbox_default(monkeypatch) ->
|
||||
assert "Host bash execution is disabled" in result
|
||||
|
||||
|
||||
def test_bash_tool_description_scopes_environment_detection_to_local_host_bash() -> None:
|
||||
description = bash_tool.description
|
||||
|
||||
assert "local host via host bash" in description
|
||||
assert "inspect the current environment instead of" in description
|
||||
assert "`uname -s`" in description
|
||||
assert "`sw_vers`" in description
|
||||
assert "`uname -a`" in description
|
||||
assert "`/etc/os-release` only when the active" in description
|
||||
assert "sandbox policy permits it" in description
|
||||
assert "do not repeat the rejected command" in description
|
||||
assert "otherwise use allowed virtual paths or explain the restriction" in description
|
||||
|
||||
|
||||
def test_bash_tool_guides_recovery_after_host_path_rejection(monkeypatch) -> None:
|
||||
runtime = SimpleNamespace(
|
||||
state={"sandbox": {"sandbox_id": "local"}, "thread_data": _THREAD_DATA.copy()},
|
||||
context={"thread_id": "thread-1"},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"deerflow.sandbox.tools.ensure_sandbox_initialized",
|
||||
lambda runtime: SimpleNamespace(execute_command=lambda command: pytest.fail("unsafe command should not execute")),
|
||||
)
|
||||
monkeypatch.setattr("deerflow.sandbox.tools.ensure_thread_directories_exist", lambda runtime: None)
|
||||
monkeypatch.setattr("deerflow.sandbox.tools.is_host_bash_allowed", lambda: True)
|
||||
|
||||
result = bash_tool.func(runtime=runtime, description="detect the OS", command="cat /etc/os-release")
|
||||
|
||||
assert "For environment questions, use command-only probes" in result
|
||||
assert "Do not repeat the rejected path" in result
|
||||
|
||||
|
||||
def test_bash_tool_blocks_relative_traversal_before_host_execution(monkeypatch) -> None:
|
||||
runtime = SimpleNamespace(
|
||||
state={"sandbox": {"sandbox_id": "local"}, "thread_data": _THREAD_DATA.copy()},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user