> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 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.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Choosing Isolation"
        A[🤖 Agent] --> Q{🔍 What needs isolation?}
        Q -->|Explicit execute_code calls| E[⚙️ Agent sandbox=]
        Q -->|Model-driven execution| F[🐳 AgentFlow run_on=]
        Q -->|One agent's tools| L[📦 LocalAgent compute=]
        E --> R[✅ Isolated]
        F --> R
        L --> R
    end

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef question fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef surface fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff

    class A agent
    class Q question
    class E,F,L surface
    class R result
```

## Quick Start

**1. Isolate an explicit execute\_code() call**

`Agent(sandbox=…)` gives you the caller-invoked `execute_code()` API. It adds no tools and does not isolate `tools=` callables.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

agent = Agent(name="Coder", instructions="Run Python.", sandbox=True)
result = agent.execute_code_sync("print(2 + 2)")
print(result.stdout)
```

**2. 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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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](https://github.com/MervinPraison/PraisonAI/pull/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](https://github.com/MervinPraison/PraisonAI/pull/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 |

> [!NOTE]
>
> No row above provides **macOS** local containment: `SandboxConfig.native()` is Linux-only (`sandlock`) and raises `ImportError` on macOS. macOS Seatbelt (`sandbox-exec`) and Linux `bwrap` live in the `praisonai code` CLI, reached with [`PRAISON_SHELL=sandboxed`](https://praison.ai/docs/features/os-sandbox) — a separate path from `SandboxConfig.native()`.

> [!NOTE]
>
> With the docker backend, cross-command filesystem state **is** provided: `write_file()`, `run_command()`, and `execute()` share a `/sandbox` bind mount, so a file written by one is visible to the next. That mount does **not** expose the host filesystem (`ls /Users` fails), and code inside the container cannot swap a symlink into the mount to redirect the host's `write_file` / `read_file` — both open descriptor-relative with `O_NOFOLLOW` at every path component. `list_files()` never returns host paths — even on platforms where `TMPDIR` sits behind a symlink (macOS `/var → /private/var`).

### 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=...)` |

> [!WARNING]
>
> **Modal has no file persistence.** `write_file()` returns `False` and writes nothing, `read_file()` returns `None`, and `execute_file()` fails with a "no file storage" error. Pass code inline via `execute(code=...)`, or use `docker` / `subprocess` / `ssh` when the workflow needs files. See [Modal caveats](https://praison.ai/docs/docs/features/sandbox-backends#modal-caveats).

***

## Why `sandbox=` is not a capability grant

`Agent(sandbox=…)` is a **restriction flag**, not a way to hand the model an execution tool.

> [!WARNING]
>
> `Agent(sandbox=…)` does **not** add `execute_python_code` / `execute_shell_command` to `agent.tools` — that auto-injection was reverted in [PR #3976](https://github.com/MervinPraison/PraisonAI/pull/3976). Giving the model a sandboxed execution tool must be a deliberate act by the caller, the way `MCP()` is. No peer framework grants execution capability from a config flag.

> [!NOTE]
>
> As of [PR #4875](https://github.com/MervinPraison/PraisonAI/pull/4875), the subprocess backend's `execute()` path enforces **`blocked_imports`** on Python code. The check parses the source with `ast` (no substring false positives from strings, comments, or longer identifiers) and resolves aliases back to their real dotted names. See the [`sandbox` warning](https://praison.ai/docs/features/sandbox#what-securitypolicy-actually-controls-in-subprocess) for the end-to-end example.

> [!WARNING]
>
> `blocked_imports` is now enforced on the subprocess **`execute()`** path. The rest of `SecurityPolicy` is still **not** enforced there (this warning is scoped to that backend and that path):
>
> * `allow_network=False` does not block outbound HTTPS — code importing a module that isn't on the blocked list can still open a socket.
> * `blocked_paths=['~/.ssh', ...]` does not stop reading an SSH private key.
> * Command-level clauses (`blocked_commands`, `allowed_commands`, `blocked_paths`, `allowed_paths`) apply on `run_command()` only, not `execute()`.
> * A separate process is not a security boundary.
>
> For real containment, use a real container backend (`docker` / `e2b`), `sandlock` for a kernel-enforced local boundary (Landlock + seccomp-bpf), or `AgentFlow(run_on=…)`.

> [!NOTE]
>
> `DockerSandbox` is different — as of [PR #4303](https://github.com/MervinPraison/PraisonAI/pull/4303) it **does** enforce the command-level parts of `SecurityPolicy` on the host before Docker is invoked.
>
> * **Enforced on docker:** `blocked_commands`, `allowed_commands`, `blocked_paths`, `allowed_paths` on every `run_command()`; `max_output_size` on both `run_command()` and `execute()`.
> * **Not enforced on docker:** `allow_subprocess` — the container itself is the boundary that clause approximates on the host, so blocking `sh` inside the container would refuse every command rather than harden it.
> * **Not a substitute for the container:** the container still bounds *where* code runs; policy enforcement is additional, not a replacement for the isolation boundary.
>
> See [Docker SecurityPolicy enforcement](https://praison.ai/docs/features/sandbox-backends.md#docker-securitypolicy-enforcement).

> [!NOTE]
>
> `praisonai-sandbox` must be installed to run any sandbox backend — `pip install praisonaiagents` alone is not enough. Install a backend, e.g. `pip install "praisonai-sandbox[docker]"`.

***

## 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](https://github.com/MervinPraison/PraisonAI/pull/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](https://github.com/MervinPraison/PraisonAI/pull/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](https://praison.ai/docs/features/reclaim-stray-sandboxes.md) for the full mechanism and [Placement](https://praison.ai/docs/features/placement.md) for how the leaks fit the placement story.

***

## Common Patterns

> [tab] Explicit code execution
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> 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)
> ```

> [tab] Give the model a sandbox tool
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> 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")
> ```

> [tab] Whole-workflow container
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> 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

> [accordion] 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.

> [accordion] 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.

> [accordion] 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.

> [accordion] Install a sandbox backend first
>
> `pip install praisonaiagents` cannot execute any sandbox. Install `praisonai-sandbox` with the backend you need before relying on isolation.

***

## Related

> [card] [Sandbox](https://praison.ai/docs/features/sandbox.md)
>
> Configure the explicit `execute_code()` API and choose a backend

> [card] [Shared Sandbox](https://praison.ai/docs/features/shared-sandbox.md)
>
> Share one container across every agent with `run_on=`


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.