Skip to content
Open
3 changes: 2 additions & 1 deletion backend/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,8 @@ Proxied through nginx: `/api/langgraph/*` → Gateway LangGraph-compatible runti
**Environment policy** (`sandbox/env_policy.py`): `execute_command` no longer inherits the full `os.environ`. `build_sandbox_env()` scrubs secret-looking names (`*KEY*`/`*SECRET*`/`*TOKEN*`/`*PASS*`/`*CREDENTIAL*`) from the inherited environment before layering injected request secrets on top, so platform credentials (e.g. `OPENAI_API_KEY`) never leak into skill subprocesses. Benign vars (`PATH`, `HOME`, `LANG`, `VIRTUAL_ENV`, ...) are preserved.
**Implementations**:
- `LocalSandboxProvider` - Local filesystem execution. `acquire(thread_id)` returns a per-thread `LocalSandbox` (id `local:{thread_id}`) whose `path_mappings` resolve `/mnt/user-data/{workspace,uploads,outputs}` and `/mnt/acp-workspace` to that thread's host directories, so the public `Sandbox` API honours the `/mnt/user-data` contract uniformly with AIO. `acquire()` / `acquire(None)` keeps the legacy generic singleton (id `local`) for callers without a thread context. Per-thread sandboxes are held in an LRU cache (default 256 entries) guarded by a `threading.Lock`. Legacy global-custom mounts are gated by the same user-scoped skill discovery rule used for prompt/list visibility; providers must not infer visibility from raw directory presence alone.
- `AioSandboxProvider` (`packages/harness/deerflow/community/`) - Docker-based isolation. Active-cache and warm-pool entries are checked with the backend during acquire/reuse; definitively dead containers are dropped from all in-process maps so the thread can discover or create a fresh sandbox instead of reusing a stale client. Backend health-check failures are treated as unknown, not dead; local discovery likewise treats an unverifiable container as not adoptable and falls through to create rather than failing acquire. `get()` remains an in-memory lookup for event-loop-safe tool paths. Legacy global-custom mounts follow the same shared visibility helper as local and remote providers.
- `AioSandboxProvider` (`packages/harness/deerflow/community/`) - Docker-based isolation. Active-cache and warm-pool entries are checked with the backend during acquire/reuse; definitively dead containers are dropped from all in-process maps so the thread can discover or create a fresh sandbox instead of reusing a stale client. Backend health-check failures are treated as unknown, not dead; local discovery likewise treats an unverifiable container as not adoptable and falls through to create rather than failing acquire. `get()` remains an in-memory lookup for event-loop-safe tool paths — it never renews the lease (that would be blocking IO on the event loop); active leases are refreshed off the event loop from the background idle checker (`_renew_active_leases`) and on acquire/reclaim. Legacy global-custom mounts follow the same shared visibility helper as local and remote providers.
- **Multi-worker ownership lease** (`sandbox_lease.py`, #4206): workers sharing one Docker daemon coordinate container ownership through file leases under `{base_dir}/sandbox-leases/`. Startup/idle reconciliation only adopts a container with no live foreign lease, and idle/replica/shutdown destroy paths refuse a peer-owned container. `read_lease` returns `None` only for a genuinely-absent lease and **raises** (`CorruptLeaseError`/`OSError`) when a lease exists but is unreadable or corrupt, so ownership checks fail closed (an unprovable lease counts as foreign-owned) rather than mistaking it for a free container. The ownership check and the container stop run under a per-sandbox `flock` guard (`lease_ownership_guard`, keyed by `sandbox_id` under the lease dir), and every lease write takes the same guard, so a peer's `touch_lease` cannot land between a destroy's check and the stop. Scope is **same-host** (multi-worker gateway processes on one Docker daemon); it is deliberately not a multi-pod distributed lock — `flock` over a shared network volume is unreliable.
- `BoxliteProvider` (`packages/harness/deerflow/community/boxlite/`) - BoxLite micro-VM isolation. The `boxlite` runtime is optional (`deerflow-harness[boxlite]`) and lazy-imported only when this provider is selected. The provider owns one private asyncio event loop on a daemon thread because BoxLite handles are loop-affine; sync `Sandbox` calls marshal onto that loop with `run_coroutine_threadsafe`.
Boxes are named deterministically from `user_id:thread_id`, released into an in-process warm pool after each agent turn, and reclaimed only by the same user/thread. Warm-pool health checks use a short explicit timeout and forward that timeout through both BoxLite `exec(timeout=...)` and the private-loop `.result(timeout)` bridge so a hung VM cannot pin the per-thread acquire lock indefinitely.
`sandbox.replicas` caps active + warm VMs per gateway process; if capacity is exhausted, only warm-pool VMs are evicted. `sandbox.idle_timeout` stops idle warm VMs after the configured seconds. `reset()` is intentionally a lightweight registry clear for `reset_sandbox_provider()` and does not close boxes, stop the idle reaper, or close the private loop; full teardown remains `shutdown()`.
Expand Down
Loading
Loading