* feat(sandbox): share sandbox identity derivation and acquire serialization (#4741) Remote providers (AIO, E2B, BoxLite, Tenki, OpenSandbox) each inlined the same sha256(user:thread)[:16] sandbox-id expression and kept per-scope lock dicts that grew unboundedly until shutdown. This extracts both mechanisms into shared components without changing provider lifecycle, ids, capacity semantics, or public tool behavior: - sandbox/identity.py: keyword-only derive_sandbox_scope_token (byte-pinned compatibility contract) + is_sandbox_scope_token; per-provider golden vectors pin current behavior including BoxLite's raw-None quirk and each provider's private user_id resolution. - sandbox/acquire_serialization.py: AcquireSerializer — per-key lock table with holder/waiter refcount reclamation, bounded dedicated executor (async waits off both the event loop and the default executor), worker-owned cancellation cleanup (no event-loop callback dependency), idempotent close(). - Each provider adopts both components; AIO/E2B key by (user_id, thread_id) with acquire and (E2B) release serialized; BoxLite/Tenki/OpenSandbox key by derived sandbox id and offload the whole sync acquire to the serializer's executor so a cancelled awaiter cannot overlap a retried same-scope body (leaked-remote-VM regression caught in review). - thread_id=None acquires stay unserialized; provider shutdown()/reset() close the serializer; E2B capacity/ledger/reconciliation and AIO ownership/flock machinery untouched. - blocking-IO anchor proves contended OpenSandbox acquire_async stays off the event loop (teeth verified red/green); AGENTS.md documents the shared components. * refactor(sandbox): address review on acquire serialization (#5089) - Replace unreachable checkin branch with an assertion: run() returns False only after abandon(), which the except handler always re-raises; the old _checkin would have double-decremented the refcount. - Document the task.cancelling() == 0 assumption in hold_async. - Drop unused thread_id/user_id kwargs from BoxLite and Tenki _acquire_scope_locked (OpenSandbox still forwards them). * fix(sandbox): preserve request ContextVars in acquire executor bridge (#5089) loop.run_in_executor() does not copy contextvars, unlike the inherited SandboxProvider.acquire_async() which used asyncio.to_thread(). The BoxLite/OpenSandbox/Tenki acquire_async bridges introduced in this PR therefore dropped the request trace id (logged as trace_id=-). Add AcquireSerializer.run_on_executor(), which copies the calling context and runs the callable through ctx.run, and route all three providers through it. Add regression tests binding request_trace_context and verifying the worker thread observes it.
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_command—sh -lc, with per-call env and timeout.read_file/write_file/update_file—catand chunkedbase64(binary-safe, no arg-size limit).download_file— 100 MB cap, restricted to the/mnt/user-dataprefix.list_dir/glob/grep—find/grepwith 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.