Sandbox Guarantees
What each isolation surface actually guarantees — and what it does not
Each isolation surface in PraisonAI guarantees something different — this page states exactly what, so you never mistake a restriction flag for a real boundary.
Quick Start
- Isolate an explicit execute_code() call
Agent(sandbox=…)gives you the caller-invokedexecute_code()API. It adds no tools and does not isolatetools=callables.python from praisonaiagents import Agent agent = Agent(name="Coder", instructions="Run Python.", sandbox=True) result = agent.execute_code_sync("print(2 + 2)") print(result.stdout) - Isolate everything the model runs
AgentFlow(run_on="docker")puts every step’s shell and file tools inside one shared container boundary, so model-driven tools route through the shared sandbox.python from praisonaiagents import Agent, AgentFlow flow = AgentFlow( agents=[Agent(name="Builder", instructions="Build and run code.")], run_on="docker", # docker | e2b | modal | daytona | flyio | tenki | local ) flow.start("Write a script that prints the first 10 primes, then run it")
Guarantee Matrix
Each surface below isolates a different thing — read the row before you rely on it.
| Surface | Isolates model-visible tools? | Isolates tools= callables? | Real container boundary? | Enforces SecurityPolicy? | ||
|---|---|---|---|---|---|---|
Agent(sandbox=True) (default subprocess backend) | No — no tools auto-added | No | No (subprocess only) | Partial — blocked_imports now enforced on both run_command() and execute() (PR #4875, Python code, AST-based); other clauses still run_command()-only | ||
Agent(sandbox=SandboxConfig(backend=...)) with real container backend | No — no tools auto-added | No | Yes (via configured backend) | Depends on backend | ||
Agent(sandbox=SandboxConfig.docker(...)) | No — no tools auto-added | No | Yes (container) | Command-level clauses (blocked_commands, allowed_commands, blocked_paths, allowed_paths, max_output_size) enforced on the host before dispatch (PR #4303). allow_subprocess is deliberately not enforced — the container is the boundary that clause approximates. | ||
Agent(sandbox=SandboxConfig.native()) (sandlock) | Isolates only explicit execute_code() | No | Yes — Landlock + seccomp-bpf, the only real local boundary on Linux | Kernel-enforced | ||
| `AgentFlow(run_on=”docker” \ | “e2b” \ | …)` | Yes — every step’s shell/file tools route through the shared sandbox | No (callables stay in-process) | Yes | Depends on provider |
| `LocalAgent(compute=”docker” \ | “e2b” \ | …)` | Yes for that agent’s shell/file tools | No | Yes | Depends on provider |
SandboxedAgent(compute=...) | Yes | No | Yes | Depends on provider | ||
praisonai code with PRAISON_SHELL=sandboxed | Yes — every execute_command (and ACP acp_execute_command), with or without shell operators | No | Yes — Seatbelt (macOS) / bwrap (Linux); writable only in PRAISONAI_WORKSPACE + $TMPDIR, secret-named env vars scrubbed, ~/.ssh ~/.aws ~/.gnupg ~/.config/gcloud ~/.azure masked | No — refuses to run if the kernel jail isn’t proven enforcing | ||
Docker write_file / read_file | — | — | Yes — symlink-race safe via open_in_sandbox() (O_NOFOLLOW per component) | Mount cannot be redirected by a container-side symlink |
File persistence per backend
A True return from write_file() means the bytes are on disk; Modal is honest about writing nothing and returns False.
| Backend | File persistence | write_file() return |
|---|---|---|
subprocess / sandlock | Yes | True when the bytes land on disk |
docker | Yes — shared /sandbox mount | True when the bytes land on disk |
ssh | Yes — on the remote host | True when the bytes land on disk |
e2b / daytona / novita | Yes — on the cloud sandbox | True when the bytes land on disk |
modal | No — stateless, no file storage | False — nothing is written; use execute(code=...) |
Why sandbox= is not a capability grant
Agent(sandbox=…) is a restriction flag, not a way to hand the model an execution tool.
Reliability: no leaked containers
Two guarantees close the container-leak story end to end — a timed-out execution and an exited run_on= script both leave zero running containers.
| Guarantee | How it is achieved |
|---|---|
| A timed-out docker execution leaves no running container | Every docker run is named praisonai-<execution_id>; on timeout the sandbox issues docker kill <name> before killing the client (PR #4109). |
A run_on= instance is reclaimed at process exit | ComputeManagedAgent registers a weakref.finalize at provision that runs on GC and at interpreter exit (PR #4109); explicit ashutdown() detaches the finalizer to avoid double-release. |
Verified before/after:
| Before | After | |
|---|---|---|
| Containers after a timeout | 1 | 0 |
Containers after a run_on= script exits | 1 | 0 |
| Ordinary run | 42 | 42 |
See Reclaim Stray Sandboxes for the full mechanism and Placement for how the leaks fit the placement story.
Common Patterns
Explicit code execution
from praisonaiagents import Agent
agent = Agent(name="Analyst", instructions="Analyze data.", sandbox=True)
result = agent.execute_code_sync("import statistics; print(statistics.mean([1, 2, 3]))")
print(result.stdout)Give the model a sandbox tool
from praisonaiagents import Agent
agent = Agent(name="Coder", instructions="Use run_code to answer.", sandbox=True)
def run_code(code: str) -> str:
"""Run Python in the agent's configured sandbox."""
result = agent.execute_code_sync(code)
return result.stdout or result.error or ""
agent.tools = [run_code] # explicit, the way MCP() is
agent.start("Print the Python version")Whole-workflow container
from praisonaiagents import Agent, AgentFlow
flow = AgentFlow(
agents=[Agent(name="Builder", instructions="Build and run code.")],
run_on="e2b",
)
flow.start("Create and run a script that lists installed packages")Best Practices
Treat sandbox= as a restriction, not a grant
Agent(sandbox=…) configures the explicit execute_code() API. It never hands the model a tool — add one yourself only when you intend the model to run code.
Use a real container for untrusted or model-driven code
The default subprocess backend is for trusted development only. For untrusted or model-driven execution, use AgentFlow(run_on="docker"), LocalAgent(compute="docker"), or sandlock for a kernel-enforced local boundary.
Remember autonomy= injects a host tool
With autonomy=True, the agent still carries the host execute_command tool. sandbox= does not protect that path — isolate the whole workflow with AgentFlow(run_on=…) instead.
Install a sandbox backend first
pip install praisonaiagents cannot execute any sandbox. Install praisonai-sandbox with the backend you need before relying on isolation.