dorianzheng 358bacad89
feat(sandbox): add BoxLite micro-VM sandbox provider (scaffold) (#3940)
* feat(sandbox): add BoxLite micro-VM sandbox provider (scaffold)

Community SandboxProvider backed by BoxLite, a daemonless OCI-native micro-VM
runtime. execute_command is wired end to end through a private asyncio loop that
bridges BoxLite's async SDK to DeerFlow's sync Sandbox contract; the remaining
Sandbox methods are stubbed pending approach review. Opt-in via sandbox.use
(pip install boxlite); a packaged [boxlite] extra will follow. Existing
providers are unchanged.

Refs #3936, #3439, #3213

* feat(sandbox): implement full Sandbox contract for BoxLite backend

Rename the community BoxLite integration to read as a compute backend rather
than "a sandbox": module boxlite_sandbox -> boxlite, BoxliteSandboxProvider ->
BoxliteProvider, BoxliteSandbox -> BoxliteBox.

Implement the file surface DeerFlow's default tool path assumes -- read_file,
write_file, update_file, download_file, list_dir, glob, grep -- as shell
commands inside the box (cat/find/grep/chunked base64), reusing
deerflow.sandbox.search and busybox-portable flags. Removes the reachable
NotImplementedError regression once the provider is selected. download_file
keeps the /mnt/user-data prefix + traversal guards; the provider materialises
those virtual dirs on box start.

Refs #3936, #3940

* fix(sandbox): resolve CI + review findings on BoxLite backend

- provider: import DEFAULT_SKILLS_CONTAINER_PATH instead of the "/mnt/skills"
  literal (backend-unit-tests guard), and drop the redundant env-var
  re-resolution -- AppConfig.resolve_env_variables already resolves $VARS and
  raises on missing.
- box: grep now passes the raw pattern to grep (-F/-E); it previously handed
  the re.escape'd pattern to grep -F, so a literal search of e.g. foo.bar
  looked for foo\.bar and never matched.
- box: execute_command now calls _validate_extra_env(env), matching the
  Sandbox POSIX env-key contract that the local/e2b/aio sandboxes enforce.
- add tests/test_boxlite_provider.py (CI-safe, no BoxLite): actionable
  ImportError on the lazy import, clean acquire-failure + shutdown, the
  traversal / download-prefix guards, and env-key rejection.

Refs #3936, #3940
2026-07-05 00:17:30 +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, run unchanged (default: python:3.12-slim)
  memory_mib: 1024          # per-box memory cap (optional)
  cpus: 2                   # per-box vCPUs (optional)
  environment:              # injected into every command
    PYTHONUNBUFFERED: "1"
pip install boxlite   # an optional `[boxlite]` extra + uv.lock update will follow once the approach lands

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. This keeps all operations on the loop the box was started on and is safe under DeerFlow's asyncio.to_thread worker pool — without using BoxLite's greenlet sync facade, which refuses to run inside an async context and is thread-affine.

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.

Out of scope for this pass (follow-ups): warm pooling, idle reaping, mount syncing, and remote/provisioner modes.

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.