From 12397d21421f8ed8f29462314591627e096a5753 Mon Sep 17 00:00:00 2001 From: OUAO-FRANK <152768228+OUAO-FRANK@users.noreply.github.com> Date: Wed, 14 Oct 2026 06:47:38 +0800 Subject: [PATCH] fix(sandbox): forward bash command timeout to remote providers (#6782) --- .../packages/harness/deerflow/config/sandbox_config.py | 8 ++++---- backend/packages/harness/deerflow/sandbox/AGENTS.md | 2 +- backend/packages/harness/deerflow/sandbox/tools.py | 3 +++ backend/tests/test_skill_request_scoped_secrets.py | 4 ++-- config.example.yaml | 6 +++--- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/backend/packages/harness/deerflow/config/sandbox_config.py b/backend/packages/harness/deerflow/config/sandbox_config.py index 0bc9feb13c..80d0f0a2b7 100644 --- a/backend/packages/harness/deerflow/config/sandbox_config.py +++ b/backend/packages/harness/deerflow/config/sandbox_config.py @@ -327,10 +327,10 @@ class SandboxConfig(BaseModel): "(1.9.3+, recommended 1.11.0) enforce it server-side through " "`hard_timeout`; the frozen legacy `all-in-one-sandbox:latest` image " "only gets the bounded host-side request. `bash_command_timeout` is " - "used by providers that explicitly wire this setting (currently " - "LocalSandbox, AioSandbox, and OpenSandbox). Other providers retain " - "their provider-specific command defaults unless a caller supplies an " - "explicit timeout." + "forwarded to each provider's command API. LocalSandbox, AioSandbox, " + "OpenSandbox, E2B, BoxLite, and Tenki apply it to bash commands; " + "providers without per-command timeout support retain their own " + "defaults." ), ) diff --git a/backend/packages/harness/deerflow/sandbox/AGENTS.md b/backend/packages/harness/deerflow/sandbox/AGENTS.md index 71d7d96459..b0c51b14bc 100644 --- a/backend/packages/harness/deerflow/sandbox/AGENTS.md +++ b/backend/packages/harness/deerflow/sandbox/AGENTS.md @@ -116,7 +116,7 @@ subshell. Keep parser filtering as a backstop; test ignored roots, metacharacter symlinks, and visible depth/size bounds. - 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`, POSIX/Windows output uses bounded pipe-drain threads with `/dev/null` stdin; Windows decodes locale-code-page/UTF-8/CRLF/bare-CR output with universal-newline translation, while POSIX stays byte-decoded. POSIX background commands return without blocking on inherited pipes; unredirected output is drained without unbounded temp files. Commands that read stdin get immediate EOF. `bash_command_timeout` sets T (600s) for Local/AIO/OpenSandbox; others keep defaults unless explicit. Local: T is a wall-clock process-group deadline. Supported-semver AIO: server-side `hard_timeout`; frozen legacy `all-in-one-sandbox:latest`: command requests get a bounded host `T+5s` wait (`max_retries=0`), so a wedged command request cannot hold the sandbox lock for the SDK's full 600s budget; file/list RPCs are not bounded by it. Explicit AIO shell/bash session creation uses a separate 5s/no-retry host bound; an ambiguous create is never replayed and forces container recycle before warm reuse. Never replay ambiguous outcomes: transport timeout, terminated, no_change_timeout, unknown statuses. `hard_timeout` => terminated + Exit Code: 124, keep session; `no_change_timeout` => may still be running, fence session generation. The description scopes host-environment probes to LocalSandbox (`uname -s`, then `sw_vers` on Darwin; Linux files only when policy permits), gives conditional recovery for local path/`file://` rejections, and tells the model to background long-lived processes. See `LocalSandbox.execute_command`, its platform runners, and `bash_tool`'s docstring. +- `bash` - Execute commands with path translation and error handling. For `LocalSandbox`, POSIX/Windows output uses bounded pipe-drain threads with `/dev/null` stdin; Windows decodes locale-code-page/UTF-8/CRLF/bare-CR output with universal-newline translation, while POSIX stays byte-decoded. POSIX background commands return without blocking on inherited pipes; unredirected output is drained without unbounded temp files. Commands that read stdin get immediate EOF. `bash_command_timeout` is forwarded to providers with a per-command timeout API (Local/AIO/OpenSandbox/E2B/BoxLite/Tenki); providers without one keep their own defaults. Local: T is a wall-clock process-group deadline. Supported-semver AIO: server-side `hard_timeout`; frozen legacy `all-in-one-sandbox:latest`: command requests get a bounded host `T+5s` wait (`max_retries=0`), so a wedged command request cannot hold the sandbox lock for the SDK's full 600s budget; file/list RPCs are not bounded by it. Explicit AIO shell/bash session creation uses a separate 5s/no-retry host bound; an ambiguous create is never replayed and forces container recycle before warm reuse. Never replay ambiguous outcomes: transport timeout, terminated, no_change_timeout, unknown statuses. `hard_timeout` => terminated + Exit Code: 124, keep session; `no_change_timeout` => may still be running, fence session generation. The description scopes host-environment probes to LocalSandbox (`uname -s`, then `sw_vers` on Darwin; Linux files only when policy permits), gives conditional recovery for local path/`file://` rejections, and tells the model to background long-lived processes. See `LocalSandbox.execute_command`, its platform runners, and `bash_tool`'s docstring. - `ls` - Directory listing (tree format, max 2 levels). Remote commands precheck root existence and emit `__DF_FIND_STATUS__:missing`; `find` status 1 is always an incomplete-traversal `OSError`. `AioSandbox.list_dir` parses only `completed`/`None` on #5634's selected generation: `hard_timeout` raises `TimeoutError` and keeps that generation reusable; an ambiguous timeout or status raises `OSError`, replays nothing, and fences that generation. Missing-session 404s drop the stale id without replay. Persistent/scoped shells fence all `httpx.TransportError`s before cleanup. - `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 diff --git a/backend/packages/harness/deerflow/sandbox/tools.py b/backend/packages/harness/deerflow/sandbox/tools.py index 6e007a79b4..b963dcc3a7 100644 --- a/backend/packages/harness/deerflow/sandbox/tools.py +++ b/backend/packages/harness/deerflow/sandbox/tools.py @@ -2472,8 +2472,10 @@ def bash_tool(runtime: Runtime, command: str, description: str = "") -> str: sandbox_cfg = get_app_config().sandbox max_chars = sandbox_cfg.bash_output_max_chars if sandbox_cfg else 20000 + command_timeout = sandbox_cfg.bash_command_timeout if sandbox_cfg else None except Exception: max_chars = 20000 + command_timeout = None return _truncate_bash_output( mask_secret_values( _execute_bash_command( @@ -2481,6 +2483,7 @@ def bash_tool(runtime: Runtime, command: str, description: str = "") -> str: command, runtime=runtime, env=injected_env, + timeout=command_timeout, ), injected_env, ), diff --git a/backend/tests/test_skill_request_scoped_secrets.py b/backend/tests/test_skill_request_scoped_secrets.py index 822167d62d..bed0488d67 100644 --- a/backend/tests/test_skill_request_scoped_secrets.py +++ b/backend/tests/test_skill_request_scoped_secrets.py @@ -1553,7 +1553,7 @@ class TestBashToolInjectsActiveSecrets: assert captured["command"] == "echo hi" assert captured["env"] == {"ERP_TOKEN": "tok-789", USER_ID_ENV: "u-win"} - def test_remote_bash_does_not_forward_shared_timeout(self): + def test_remote_bash_forwards_shared_timeout(self): from deerflow.sandbox import tools as tools_mod captured = {} @@ -1600,7 +1600,7 @@ class TestBashToolInjectsActiveSecrets: ) assert out == "done" - assert captured["timeout"] is None + assert captured["timeout"] == 42 _SECRET = "sk-erp-9f3c-DO-NOT-LEAK" diff --git a/config.example.yaml b/config.example.yaml index 9f33104771..f0a6b0facc 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1728,9 +1728,9 @@ sandbox: # semver line (1.9.3+, recommended 1.11.0) enforce it server-side through # `hard_timeout`; the frozen legacy `all-in-one-sandbox:latest` image only # gets the bounded host-side request. `bash_command_timeout` is used by - # providers that explicitly wire this setting (currently LocalSandbox, - # AioSandbox, and OpenSandbox). Other providers retain their provider-specific - # command defaults unless a caller supplies an explicit timeout. For providers + # providers that expose a per-command timeout (LocalSandbox, AioSandbox, + # OpenSandbox, E2B, BoxLite, and Tenki). Providers without per-command timeout + # support retain their provider-specific command defaults. For providers # that enforce this deadline server- or process-side, blocking foreground # commands are terminated at the configured deadline. Long-lived processes # should be started in the background with output redirected; unredirected