Appearance
Sandbox — Overview
Sandbox execution gives agents a separate workspace for shell commands and file operations — without running the agent loop or API keys inside that environment.
Use it when agents need to run tests, edit code, install packages, or inspect project files safely. For multiple agents collaborating on one codebase, attach them to the same Sandbox so they share one filesystem while keeping separate conversations.
How it works
Sandbox-as-tool: The LLM and business tools (HTTP, retrieve, search, …) stay on the host. Only delegated workspace operations — execute, read_file, write_file, list_dir — run inside the backend.
On the first agent invocation, WorkspaceOrchestrator automatically prepares the workspace (create backend, seed project files if configured).
Core concepts
| Concept | Description |
|---|---|
Sandbox resource | Application-owned object with lifecycle (start, stop, flush). Agents attach via AgentConfig(sandbox=...). |
| Shared workspace | Files and command side effects in one environment; each agent keeps its own messages and LLM context. |
SandboxPlugin tools | execute, read_file, write_file, list_dir — registered when a backend is active. Auto-added on attach if missing. |
| Execution backend | Where commands run — local host dir (dev), AgentCore VM, or Daytona workspace. See Backends. |
| Host vs sandbox tools | In production SandboxMode.SANDBOX, host prebuilt tools (shell, file_read, …) are blocked; use sandbox plugin tools instead. |
Quick start — single agent
python
import asyncio
from pathlib import Path
from uuid import uuid4
from elsai import Agent
from elsai.agent import AgentConfig
from elsai.execution import LocalSandboxBackend, Sandbox, workspace_id_for_run
from elsai.plugins.sandbox import SandboxPlugin
HOST_REPO = Path("./my-repo")
SANDBOX_ROOT = Path("/tmp/elsai-sandbox-ws")
async def main() -> None:
async with Sandbox(
workspace_id=workspace_id_for_run(str(uuid4())),
backend=LocalSandboxBackend(SANDBOX_ROOT),
ttl_seconds=3600,
seed_from=HOST_REPO,
) as sandbox:
agent = Agent(
config=AgentConfig(
sandbox=sandbox,
plugins=[SandboxPlugin()],
),
)
result = await agent.invoke_async("List the project files and summarize the README")
print(result.message)
asyncio.run(main())What this shows:
| Piece | Concept |
|---|---|
async with Sandbox(...) | Lifecycle — start on enter, flush + stop on exit |
seed_from | Copy host project into workspace at start |
AgentConfig(sandbox=sandbox) | Auto-attach agent to shared workspace |
SandboxPlugin | Registers workspace tools on the agent |
Local backend
LocalSandboxBackend runs on the host with no isolation. Use AgentCore or Daytona for production.
Inject files before the agent runs
python
async with Sandbox(...) as sandbox:
await sandbox.add_files({
"TASK.md": "Fix the authentication bug described below.\n",
"notes/context.txt": "Priority: refresh token handling.\n",
})
agent = Agent(config=AgentConfig(sandbox=sandbox, plugins=[SandboxPlugin()]))
await agent.invoke_async("Read TASK.md and write a plan to notes/plan.md")What this shows:
| Piece | Concept |
|---|---|
add_files() | Orchestrator-side upload — no agent turn required |
| Agent reads/writes via plugin tools | Files live in the shared workspace |
Sandbox tools
When a backend is active, SandboxPlugin registers:
| Tool | Description |
|---|---|
execute | Run a shell command in the workspace |
read_file | Read a text file (optional line offset and limit) |
write_file | Write a text file |
list_dir | List a directory |
The execute tool is omitted when the backend does not support shell execution.
Production vs local development
SandboxMode controls host prebuilt tool access:
| Mode | Sandbox plugin tools | Host tools (shell, file_read, file_write, python_repl, environment) |
|---|---|---|
off | Not active | Allowed |
sandbox | Active | Blocked in production |
local_dev | Active | Allowed — development convenience |
HostExecutionGuard also blocks dynamically loaded host execution tools at runtime.
Other prebuilt tools such as editor, http_request, and retrieve are not in the blocked set unless you restrict them separately.
How this relates to prebuilt tools
| Mechanism | Package | Role |
|---|---|---|
Sandbox + SandboxPlugin | elsai-agents | Workspace execution (this guide) |
shell, file_read, … | elsai-agents-tools | Host tools — blocked when sandbox_mode=sandbox |
code_interpreter | elsai-agents-tools | Legacy AgentCore code sessions — prefer Sandbox + backend for full workspaces |
Advanced — per-agent backend
For single-agent flows or tests without a shared Sandbox facade:
python
from elsai.agent import AgentConfig, SandboxMode
from elsai.execution import LocalSandboxBackend, SandboxManager
from elsai.plugins.sandbox import SandboxPlugin
AgentConfig(
execution_backend=LocalSandboxBackend("/tmp/ws"),
sandbox_mode=SandboxMode.LOCAL_DEV,
workspace_root="./my-repo",
sandbox_manager=SandboxManager(),
plugins=[SandboxPlugin()],
)Do not combine this with AgentConfig(sandbox=...). Setting sandbox_mode=local_dev together with sandbox= raises ValueError at construction — shared attach always uses production host-tool blocking. See Shared workspace — Configuration validation.
Timeouts and TTL
Sandbox uses several timeout knobs — they are not interchangeable.
| Setting | Where | What it controls |
|---|---|---|
ttl_seconds | Sandbox(...) (required) | Workspace idle TTL — how long a managed session may sit unused before SandboxManager.cleanup_expired() can close it. |
session_timeout_seconds | AgentCoreSandboxTemplate | Remote AgentCore code-interpreter session lifetime (default 900). |
default_timeout | DaytonaSandboxTemplate | Per-command wall-clock limit in Daytona (default 900; 0 = poll until the command exits). |
timeout | LocalSandboxBackend | Per-command limit for host subprocesses (default 120). |
ttl_seconds (workspace idle)
- Required on every
Sandbox(...)— pick a value for your deployment (examples use3600–7200). - Primary teardown is
async with Sandbox(...)or explicitstop(). TTL is a backstop when a run crashes orstop()is never called. - Suggested formula:
longest_expected_run_duration + margin(for example a 90-minute graph →ttl_seconds=7200). - Activity on the workspace resets the idle clock (
last_used_aton the managed session). - Expiry is applied when your application calls
await sandbox.manager.cleanup_expired()(or the same on a sharedSandboxManager). There is no built-in background sweeper — long-running services should call it on a schedule if you rely on TTL cleanup. - Set
SandboxManager(ttl_seconds=None)only when constructing a custom manager to disable idle expiry checks.
session_timeout_seconds, default_timeout, and timeout are separate — they limit individual remote sessions or shell commands, not workspace idle time.
Limitations (v1)
- In-process sharing only — share the same Python
Sandboxobject; the sameworkspace_idstring in two processes creates two separate workspaces silently. - No agent delete/move tools — agents use
execute("rm …")or orchestratorSandbox.delete_files. - Sequential multi-agent turns recommended — parallel mutating graph nodes on one sandbox are unsupported.
- Cross-process workspace sharing is not supported.
Related
- Shared workspace — multi-agent patterns, attach, detach
- Backends — AgentCore, Daytona, local
- Sandbox API
- SandboxPlugin
- Sessions — conversation persistence (orthogonal to workspace)