Appearance
Sandbox — Shared Workspace
Multiple agents can attach to one Sandbox and share a single filesystem — planner, coder, and reviewer working on the same repo inside one VM or workspace directory.
Each agent keeps its own conversation (messages, LLM context). They collaborate through files and commands in the shared workspace.
| Per-agent (never shared) | Shared workspace |
|---|---|
| Conversation history | Files on disk in sandbox |
| LLM context window | Build artifacts, test output |
| Tool call history (per agent) | Shell side effects in workspace |
Quick start — planner and coder
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
async def main() -> None:
async with Sandbox(
workspace_id=workspace_id_for_run(str(uuid4())),
backend=LocalSandboxBackend(Path("/tmp/shared-ws")),
ttl_seconds=7200,
seed_from=Path("./my-repo"),
) as sandbox:
await sandbox.add_files({"TASK.md": "Add input validation to src/auth.py\n"})
planner = Agent(
name="planner",
config=AgentConfig(
sandbox=sandbox,
plugins=[SandboxPlugin()],
),
)
coder = Agent(
name="coder",
config=AgentConfig(
sandbox=sandbox,
plugins=[SandboxPlugin()],
),
)
await planner.invoke_async("Write a step-by-step plan to notes/plan.md")
await coder.invoke_async("Read notes/plan.md and implement step 1")
asyncio.run(main())What this shows:
| Piece | Concept |
|---|---|
Same sandbox on both agents | One backend, one filesystem |
Sequential await agent(...) | Planner writes; coder reads the same files |
add_files before agents run | Orchestrator injects task context |
Attach runs inside Agent.__init__ when sandbox= is set — you do not call attach() manually in normal use.
Attach and detach
How attach works
When AgentConfig(sandbox=sandbox) is set:
Sandbox.attach(agent)wiresexecution_backend,sandbox_mode, and workspace sync policyHostExecutionGuardblocks host execution tools in production modeSandboxPluginis registered if not already presentSandboxAttachHooksadd turn locking and read-only enforcement
What detach(agent) does
python
sandbox.detach(coder) # coder loses sandbox access; VM keeps running| Effect | Detail |
|---|---|
| Sandbox VM | Keeps running — other attached agents unaffected |
| Detached agent | Cannot use sandbox tools |
| Host tools | Not restored — create a new Agent() for host execution, or attach() again |
| Idempotent | No-op if agent is not attached |
stop() vs detach()
detach(agent) | stop() | |
|---|---|---|
| Sandbox VM | Keeps running | Closed after optional flush |
| Scope | One agent | All agents (detaches each first) |
| Typical use | Revoke one agent mid-run | End of job |
stop(sync=True) (default) flushes artifacts to the host before closing.
Example — detach one agent, reviewer continues
python
async with Sandbox(...) as sandbox:
coder = Agent(config=AgentConfig(sandbox=sandbox, plugins=[SandboxPlugin()]))
reviewer = Agent(
config=AgentConfig(
sandbox=sandbox,
sandbox_attach_mode=SandboxAttachMode.READ_ONLY,
plugins=[SandboxPlugin()],
),
)
await coder.invoke_async("Implement the feature in src/")
sandbox.detach(coder)
await reviewer.invoke_async("Review src/ for security issues")What this shows:
| Piece | Concept |
|---|---|
detach(coder) | Coder done; workspace stays alive for reviewer |
READ_ONLY reviewer | Can read and list — cannot execute or write_file |
Import attach mode:
python
from elsai.agent import SandboxAttachModeRe-attach after detach
python
sandbox.detach(agent)
# ... later, same started sandbox ...
sandbox.attach(agent, mode=SandboxAttachMode.READ_WRITE)Re-attach runs the full attach checklist including host execution policy.
Detached agent errors
If a detached agent invokes a tool, the call is cancelled with a message directing you to create a new Agent() or attach again. Mutating helpers raise AgentSandboxDetachedError.
Attach modes
| Mode | Agent tools | Use for |
|---|---|---|
read_write (default) | execute, read_file, write_file, list_dir | Planner, coder |
read_only | read_file, list_dir | Reviewer, untrusted-input nodes |
Read-only agents are exempt from the turn lock so they can read while another agent holds the lock.
Orchestrator APIs (add_files, delete_files) are not restricted by agent attach mode.
Workspace identity
Derive workspace_id from your run or tenant — never hard-code a generic string like "shared".
| Use case | Recommended ID | Helper |
|---|---|---|
| Graph / workflow run | graph-{run_id} | workspace_id_for_run(run_id) |
| A2A isolated (default) | context_id | workspace_id_for_context(id, shared=False) |
| A2A team (shared in one context) | {context_id}:shared-ws | workspace_id_for_context(id, shared=True) |
| Local test | local-{uuid} | ad hoc |
python
from elsai.execution import workspace_id_for_run, workspace_id_for_contextNot cross-process
workspace_id is a registry key within one process. The same string in two processes creates two separate workspaces with no error. Share the same Sandbox Python object in one application.
Lifecycle
| Phase | API | Who |
|---|---|---|
| Bootstrap | start(), seed_from | Application |
| Inject / cleanup | add_files, delete_files, move_file | Application |
| Agent work | SandboxPlugin tools | Agents |
| Pull artifacts | flush, download_files | Application |
| Teardown | stop or exit async with | Application |
Prefer async with Sandbox(...) as sandbox: so stop runs on success and on exceptions.
Idle TTL: ttl_seconds on Sandbox(...) is required. It defines how long the workspace may stay idle before SandboxManager.cleanup_expired() can close it — call that on a schedule in long-running services. Normal teardown is stop() at the end of the flow. See Timeouts and TTL.
Default seed excludes: .git, node_modules, __pycache__, .venv, .env, secrets patterns — see DEFAULT_SANDBOX_SEED_EXCLUDES.
With graphs
Wrap the graph run in a sandbox context; give each node agent the same sandbox=:
python
async with Sandbox(
workspace_id=workspace_id_for_run(run_id),
backend=template,
ttl_seconds=7200,
seed_from=repo_path,
) as sandbox:
def make_agent(name: str) -> Agent:
return Agent(
config=AgentConfig(
name=name,
sandbox=sandbox,
plugins=[SandboxPlugin()],
),
)
# Register make_agent("planner"), make_agent("coder"), … with GraphBuilder
await graph.invoke_async("Implement the feature")See Graph. Run dependent nodes sequentially in v1.
With A2A
A2A session isolation (conversation per context_id) and workspace sharing are orthogonal:
- Conversation →
SessionManager/context_id - Execution sharing → explicit
workspace_idonSandbox
python
def workspace_for_context(context_id: str, *, team: bool = False) -> str:
return workspace_id_for_context(context_id, shared=team)
# In A2A agent factory — one Sandbox per context, reused across specialist agentsNever reuse a workspace ID across unrelated A2A context_id values. See A2A and Sessions.
Custom sandbox-aware tools
python
from elsai import tool
from elsai.plugins.sandbox import run_command
@tool
async def run_pytest(agent, path: str = "tests/") -> str:
"""Run pytest in the sandbox workspace."""
return await run_command(agent, f"python -m pytest {path} -q")Custom tool code stays on the host; only command effects run in the workspace. Read-only attach mode raises SandboxMutationDeniedError on mutating helpers.
Output offloading
Large sandbox tool outputs can exhaust the context window. Set sandbox_output_offload on AgentConfig to auto-register a ContextOffloader:
python
from elsai.agent import SandboxOutputOffloadPolicy
from elsai.plugins.context_offloader import InMemoryStorage
AgentConfig(
sandbox=sandbox,
sandbox_output_offload=SandboxOutputOffloadPolicy(
storage=InMemoryStorage(),
max_result_tokens=2500,
),
plugins=[SandboxPlugin()],
)Configuration validation
These combinations raise ValueError at agent construction:
| Invalid | Reason |
|---|---|
sandbox= + execution_backend | Use Sandbox only |
sandbox= + workspace_root | Use Sandbox(seed_from=...) |
sandbox= + sandbox_manager | Manager owned by Sandbox |
sandbox= + sandbox_mode=local_dev | Shared attach always uses production sandbox_mode='sandbox' and blocks host tools — see note below |
Agent attached to two different Sandbox instances | SandboxAttachError |
local_dev is not supported with shared Sandbox
AgentConfig(sandbox_mode='local_dev') cannot be combined with AgentConfig(sandbox=...). Attach always wires sandbox_mode='sandbox' and applies HostExecutionGuard so host prebuilt tools (shell, file_read, file_write, …) stay blocked.
That applies even when the backend is LocalSandboxBackend — the workspace directory is on the host, but agents still use SandboxPlugin tools, not host prebuilt tools.
For local development with host tools, use the per-agent advanced path instead of a shared Sandbox facade:
python
from elsai.agent import AgentConfig, SandboxMode
from elsai.execution import LocalSandboxBackend, SandboxManager
from elsai.plugins.sandbox import SandboxPlugin
Agent(
config=AgentConfig(
execution_backend=LocalSandboxBackend("/tmp/ws"),
sandbox_mode=SandboxMode.LOCAL_DEV,
workspace_root="./my-repo",
sandbox_manager=SandboxManager(),
plugins=[SandboxPlugin()],
),
)Attempting AgentConfig(sandbox=sandbox, sandbox_mode=SandboxMode.LOCAL_DEV) raises:
text
ValueError: AgentConfig.sandbox_mode='local_dev' is not supported when using
a shared Sandbox (AgentConfig.sandbox). Attach always uses sandbox_mode='sandbox'
and blocks host execution tools. For local dev with host tools, use
execution_backend= directly instead of AgentConfig(sandbox=...).Limitations
- Parallel mutating graph branches on one sandbox — unsupported in v1
- Turn lock serializes
read_writeinvocations but does not enforce workflow order (you orchestrate planner → coder) detach()does not restore host prebuilt tools- No cross-process workspace sharing