wutongyuonce d8ed8160c9
fix(sandbox): stop list_dir from reporting failures as empty (#5264)
* fix(sandbox): stop list_dir from reporting failures as empty

Remote providers swallowed find/client errors as [] and 2>/dev/null
missing paths as empty stdout. ls_tool then told the agent the
directory was (empty). Raise OSError/FileNotFoundError instead so
the tool returns Error.

* fix(sandbox): list_dir raises on missing local paths and uses find -H

Empty stdout is not a missing path when find's start point is a
symlink (E2B /mnt/acp-workspace). Dereference only the start point
with find -H. LocalSandbox now raises FileNotFoundError for a
non-directory root, matching remote providers. AIO maps a missing
result.data to OSError rather than FileNotFoundError.

* fix(sandbox): group AIO list_dir find type predicates

Without parentheses, find PATH -maxdepth N -type f -o -type d applies
-type d without maxdepth and can drop files from the listing.

* fix(sandbox): distinguish list_dir command failure from missing path

Tenki, Boxlite, and OpenSandbox treated any empty find stdout as
FileNotFoundError, so a missing find binary (exit 127) or SDK error
looked like a missing directory. Raise OSError when find status is
outside (0, 1); keep FileNotFoundError for the find-ran-but-empty case.

* fix(sandbox): apply list_dir exit-status contract to AIO and E2B

Same gap as Tenki/Boxlite/OpenSandbox: empty find stdout with exit 127
was FileNotFoundError. Raise OSError when the status is outside (0, 1).

* fix(sandbox): classify list_dir by find status not head status

find | head under sh -lc reports head's exit code, so a missing find
binary (127) became FileNotFoundError. Record find's own status after
the bounded listing, treat SIGPIPE 141 as truncation success, and add
a shell-level regression test.

* test(auth): include projects permissions in /me contract pins

#5265 added projects:read/write/delete to the registered route set.
The /auth/me tests still pinned the pre-projects list, so CI failed
after merging main.

* fix(sandbox): do not treat missing list_dir marker as success

The generated script ended on `rm -f`, so process status was 0/1 even
when find's marker never landed. Both codes are in _FIND_OK, and the
parser fallback then classified an empty listing as FileNotFoundError —
the 127 misclassification this helper was meant to close.

Exit with find's status (126 if unknown). A missing marker is now
OSError unless the process status is already a non-OK failure.

* test(sandbox): emit list_dir status marker in provider fixtures

Parser now requires __DF_FIND_STATUS__ and refuses marker-less stdout.
Update AIO/Boxlite/E2B stubs and OpenSandbox/Tenki find fakes so listings
carry :0 and missing paths carry :1 with matching exit codes.

* style(sandbox): format list dir test fixture

* style(sandbox): format remote list dir helper

* docs(sandbox): keep guidance within the tested size budget

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-09 10:12:45 +08:00
..

BoxLite backend

Runs each DeerFlow sandbox as a BoxLite micro-VM — a daemonless, OCI-native VM with its own kernel (libkrun/KVM on Linux, Hypervisor.framework on macOS). Motivated by the resource/cold-start pain with the default AIO Docker sandbox in #3439 and #3213; discussion in #3936.

Configuration

sandbox:
  use: deerflow.community.boxlite:BoxliteProvider
  image: python:3.12-slim         # any OCI image (default: python:3.12-slim)
  memory_mib: 1024                # per-box memory cap (optional)
  cpus: 2                         # per-box vCPUs (optional)
  replicas: 3                     # active + warm VM cap per gateway process (default: 3)
  idle_timeout: 600               # warm VM idle seconds before stop; 0 disables
  health_check_skip_seconds: 0.0  # optional low-latency mode: skip reclaim
                                  #   health checks for recent releases; 0 keeps
                                  #   reliability-first validation (default: 0.0)
  environment:                    # injected into every command
    PYTHONUNBUFFERED: "1"

Install the optional runtime before selecting this provider:

pip install "deerflow-harness[boxlite]"

The boxlite package is an optional DeerFlow harness extra, not part of the default install. It is also limited to the host platforms and architectures where BoxLite publishes wheels and can boot micro-VMs. Unsupported development hosts, such as Windows, should use another sandbox provider or run DeerFlow from a supported Linux/macOS environment.

Host requirement: BoxLite boots micro-VMs, so a Linux host needs KVM — i.e. nested virtualization when DeerFlow runs inside a cloud VM. macOS uses Hypervisor.framework. This is the main deployment constraint to weigh vs. the container-based providers.

Design

DeerFlow's Sandbox contract is synchronous; BoxLite's SDK is async-native and its box handles are event-loop-affine. The provider owns one private asyncio loop on a daemon thread and marshals every coroutine onto it via run_coroutine_threadsafe. BoxLite boxes are named deterministically from user_id:thread_id, released into an in-process warm pool after each agent turn, and reclaimed by the same thread on the next acquire.

File Role
provider.py SandboxProvider lifecycle + the private-loop bridge
box.py Sandbox adapter; execute_command + file ops

Contract coverage

The full Sandbox surface is implemented. File operations run as shell commands inside the box and reuse deerflow.sandbox.search, mirroring e2b_sandbox:

  • execute_commandsh -lc, with per-call env and timeout.
  • read_file / write_file / update_filecat and chunked base64 (binary-safe, no arg-size limit).
  • download_file — 100 MB cap, restricted to the /mnt/user-data prefix.
  • list_dir / glob / grepfind / grep with busybox-portable flags; results filtered/capped in Python.

The provider creates /mnt/user-data/{workspace,uploads,outputs} and /mnt/skills on box start so those virtual paths resolve natively.

Warm-pool capacity is governed by sandbox.replicas across active + warm VMs. sandbox.idle_timeout controls how long released warm VMs stay running; 0 disables idle reaping. Active boxes are never evicted to satisfy the cap.

Status

Verified end-to-end against a live box (provider resolution → execute_command → file ops) on macOS/HVF. Linux/KVM validation and benchmarks vs. the AIO sandbox are tracked in #3936.