Skip to content

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 historyFiles on disk in sandbox
LLM context windowBuild 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:

PieceConcept
Same sandbox on both agentsOne backend, one filesystem
Sequential await agent(...)Planner writes; coder reads the same files
add_files before agents runOrchestrator 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:

  1. Sandbox.attach(agent) wires execution_backend, sandbox_mode, and workspace sync policy
  2. HostExecutionGuard blocks host execution tools in production mode
  3. SandboxPlugin is registered if not already present
  4. SandboxAttachHooks add turn locking and read-only enforcement

What detach(agent) does

python
sandbox.detach(coder)  # coder loses sandbox access; VM keeps running
EffectDetail
Sandbox VMKeeps running — other attached agents unaffected
Detached agentCannot use sandbox tools
Host toolsNot restored — create a new Agent() for host execution, or attach() again
IdempotentNo-op if agent is not attached

stop() vs detach()

detach(agent)stop()
Sandbox VMKeeps runningClosed after optional flush
ScopeOne agentAll agents (detaches each first)
Typical useRevoke one agent mid-runEnd 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:

PieceConcept
detach(coder)Coder done; workspace stays alive for reviewer
READ_ONLY reviewerCan read and list — cannot execute or write_file

Import attach mode:

python
from elsai.agent import SandboxAttachMode

Re-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

ModeAgent toolsUse for
read_write (default)execute, read_file, write_file, list_dirPlanner, coder
read_onlyread_file, list_dirReviewer, 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 caseRecommended IDHelper
Graph / workflow rungraph-{run_id}workspace_id_for_run(run_id)
A2A isolated (default)context_idworkspace_id_for_context(id, shared=False)
A2A team (shared in one context){context_id}:shared-wsworkspace_id_for_context(id, shared=True)
Local testlocal-{uuid}ad hoc
python
from elsai.execution import workspace_id_for_run, workspace_id_for_context

Not 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

PhaseAPIWho
Bootstrapstart(), seed_fromApplication
Inject / cleanupadd_files, delete_files, move_fileApplication
Agent workSandboxPlugin toolsAgents
Pull artifactsflush, download_filesApplication
Teardownstop or exit async withApplication

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_id on Sandbox
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 agents

Never 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:

InvalidReason
sandbox= + execution_backendUse Sandbox only
sandbox= + workspace_rootUse Sandbox(seed_from=...)
sandbox= + sandbox_managerManager owned by Sandbox
sandbox= + sandbox_mode=local_devShared attach always uses production sandbox_mode='sandbox' and blocks host tools — see note below
Agent attached to two different Sandbox instancesSandboxAttachError

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_write invocations but does not enforce workflow order (you orchestrate planner → coder)
  • detach() does not restore host prebuilt tools
  • No cross-process workspace sharing

Copyright © 2026 elsai foundry.