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

> Secure isolated environment for executing untrusted code safely

Sandbox gives an agent the explicit `execute_code()` API for running code in an isolated environment — you invoke it directly, the model does not call it on its own.

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

agent = Agent(
    name="Coder",
    instructions="Analyze data with Python.",
    sandbox=True,
)

# You invoke the sandbox explicitly — the model is not given a tool:
result = agent.execute_code_sync("print(sum(range(11)))")
print(result.stdout)
```

<Warning>
  `Agent(sandbox=…)` configures the explicit `agent.execute_code()` API only. It does **NOT** add tools to `agent.tools` and does **NOT** give the model a new capability — the tool auto-injection was reverted in [PR #3976](https://github.com/MervinPraison/PraisonAI/pull/3976). It also does **not** contain the whole workflow. To let the model run code, hand it a tool explicitly (see [What sandbox= configures](#what-sandbox%3D-configures-and-what-it-does-not)), or run the whole workflow in a real container with `AgentFlow(run_on="docker")`.
</Warning>

<Tip>
  Not sure whether the model is calling out to a container? Run `print(agent.where_does_it_run())` — see [Where Your Agent Runs](/docs/features/where-does-it-run).
</Tip>

<Note>
  Sandbox backends ship in the standalone `praisonai-sandbox` package. **`pip install praisonaiagents` alone cannot execute any sandbox** — install a backend first, e.g. `pip install "praisonai-sandbox[docker]"`. Installing `praisonai` gives you every backend. See the [praisonai-sandbox Package](/docs/docs/features/praisonai-sandbox-package) for the `from praisonai_sandbox import …` import path.
</Note>

<Note>
  Need model-generated code to call your registered tools? See [Code Execution with Tools](./code-execution-with-tools). A sandbox can now service tool calls via a [`CodeToolBridge`](./code-execution-with-tools#isolated-code-with-tools-bridge) — the script runs isolated while tool calls are gated in the parent by the same allow-list and approval gate.
</Note>

The user asks the agent to execute generated code; work stays inside an isolated sandbox instead of the host shell.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Sandbox Code Execution"
        A[🤖 Agent] -->|sandbox=True| M[⚙️ SandboxManager]
        M --> S{🔍 Security Check}
        S --> B[📦 Backend]
        B -->|subprocess| L[🖥️ Local]
        B -->|docker| D[🐳 Docker]
        B -->|e2b| E[☁️ E2B Cloud]
        B -->|novita| NV[☁️ Novita Cloud]
        B -->|native| N[🛡️ OS-Native]
        L & D & E & NV & N --> R[✅ SandboxResult]
        R --> A
    end

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef manager fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef check fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef backend fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff

    class A agent
    class M manager
    class S check
    class B,L,D,E,NV,N backend
    class R result
```

## Quick Start

<Steps>
  <Step title="Simple Usage">
    Enable the sandbox with a single line, then invoke it explicitly with `execute_code_sync()`.

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

    agent = Agent(
        name="Coder",
        instructions="Analyze data with Python.",
        sandbox=True  # configures agent.execute_code(); adds NO tools
    )

    result = agent.execute_code_sync("print('Hello from the sandbox')")
    print(result.stdout)
    ```
  </Step>

  <Step title="With Configuration">
    Use factory methods for specific sandbox types.

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

    agent = Agent(
        name="DataAnalyst",
        instructions="Analyze data with Python.",
        sandbox=SandboxConfig.docker("python:3.12-slim")
    )

    result = agent.execute_code_sync("import pandas; print(pandas.__version__)")
    print(result.stdout)
    ```
  </Step>

  <Step title="Full Configuration">
    Complete control over sandbox settings.

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

    agent = Agent(
        name="SecureAgent",
        instructions="Execute code with strict security.",
        sandbox=SandboxConfig(
            sandbox_type="e2b",
            resource_limits=ResourceLimits(memory_mb=512, timeout_seconds=60),
            security_policy=SecurityPolicy.standard(),
        )
    )

    result = agent.execute_code_sync("print('processed securely')")
    print(result.stdout)
    ```
  </Step>
</Steps>

<Warning>
  Combining `Agent(sandbox=…, backend=…)` emits a `FutureWarning` and the `sandbox=` value is ignored — the managed backend handles the entire turn, so local execution never runs. Configure isolation on the backend instead (e.g. `LocalAgent(compute="docker")`).

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

  with warnings.catch_warnings(record=True) as caught:
      warnings.simplefilter("always")
      agent = Agent(
          name="Coder",
          sandbox=True,
          backend=SandboxedAgent(config=SandboxedAgentConfig(model="gpt-4o")),
      )

  assert any(issubclass(w.category, FutureWarning) for w in caught)
  ```
</Warning>

***

## Agent Sandbox API

When `sandbox` is configured, agents gain powerful execution capabilities:

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

agent = Agent(
    name="DataAnalyst",
    instructions="Analyze data with Python.",
    sandbox=True,
)

# Async pattern
async def main():
    result = await agent.execute_code("""
import statistics
data = [1, 2, 3, 4, 5, 100]
print(f"Mean: {statistics.mean(data)}")
print(f"Median: {statistics.median(data)}")
""")
    print(result.stdout)
    print(f"Status: {result.status}, Exit: {result.exit_code}")

asyncio.run(main())

# Sync pattern (for non-async code)
result = agent.execute_code_sync("print(2 + 2)")
print(result.stdout)
```

### SandboxMixin API

| Method                | Signature                                                                         | Description                                                                                                                                                                                                                |
| --------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `has_sandbox`         | `property -> bool`                                                                | Whether sandbox is configured                                                                                                                                                                                              |
| `execute_code`        | `async (code, language="python", check_security=True, **kwargs) -> SandboxResult` | Async code execution with optional security pre-check. Security warnings, if any, are attached to `result.metadata["security_warnings"]` — they are no longer passed as a `metadata` kwarg to `SandboxProtocol.execute()`. |
| `execute_code_sync`   | `(code, language="python", check_security=True, **kwargs) -> SandboxResult`       | Sync wrapper around `execute_code`                                                                                                                                                                                         |
| `run_shell_command`   | `async (command: str \| list, check_security=True, **kwargs) -> SandboxResult`    | Run shell command in sandbox                                                                                                                                                                                               |
| `get_sandbox_status`  | `() -> Dict[str, Any]`                                                            | Returns `{"configured", "config", "available_types", "current_type"}`                                                                                                                                                      |
| `sandbox_cleanup`     | `async () -> None`                                                                | Force cleanup of sandbox resources                                                                                                                                                                                         |
| `get_sandbox_manager` | `() -> SandboxManager \| None`                                                    | Get or lazily create the manager                                                                                                                                                                                           |

***

## What sandbox= configures (and what it does NOT)

`Agent(sandbox=…)` configures the **explicit, caller-invoked** execution API only — `agent.execute_code()`, `agent.execute_code_sync()`, and `agent.run_shell_command()`. It does **not** add any tools to `agent.tools`, so the model gains no new capability from the flag alone.

<Warning>
  `Agent(sandbox=…)` does **NOT** auto-attach `execute_python_code` / `execute_shell_command` to `agent.tools`. Earlier releases injected them; that injection was reverted in [PR #3976](https://github.com/MervinPraison/PraisonAI/pull/3976). `sandbox=` is a **restriction flag**, not a capability grant — the model cannot call the sandbox unless you explicitly hand it a tool.
</Warning>

Verify that no tools are attached — the model sees an empty tool list from `sandbox=` alone:

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

agent = Agent(
    name="Coder",
    instructions="Analyze data with Python.",
    sandbox=True,  # configures agent.execute_code(); adds NO tools
)

assert not any(getattr(t, "__name__", "") == "execute_python_code" for t in agent.tools)
assert not any(getattr(t, "__name__", "") == "execute_shell_command" for t in agent.tools)
```

**To let the model call the sandbox**, add a tool explicitly — the same way you opt into `MCP()`. Giving the model a sandboxed execution tool is a deliberate act by the caller, never a side effect of `sandbox=`:

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

agent = Agent(
    name="Coder",
    instructions="Use run_code to answer questions with Python.",
    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 ""

# Hand the wrapper to the model explicitly:
agent.tools = [run_code]
agent.start("Calculate fibonacci(10) and print the result")
```

<Note>
  There is no first-class `SandboxTool(...)` helper yet — wrap `agent.execute_code_sync()` in your own function as above. For a model-visible shell tool with a real container boundary, prefer `AgentFlow(run_on="docker")` (see [Shared Sandbox](/docs/features/shared-sandbox)).
</Note>

***

## Scope: what sandbox= isolates

`Agent(sandbox=…)` isolates only code you invoke through the explicit `agent.execute_code(...)` / `run_shell_command(...)` API.

It does **not** isolate:

* **Python callables passed via `tools=`** — ordinary in-process functions that run on the host.
* **Tools attached by other layers** — e.g. `autonomy=True` injects a host `execute_command` tool that runs unsandboxed regardless of `sandbox=`.

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

def read_file(path: str) -> str:
    # Runs in-process on the host, NOT inside the sandbox.
    return open(path).read()

agent = Agent(
    name="Analyst",
    instructions="Use tools to answer.",
    sandbox=True,      # only agent.execute_code(...) runs in the sandbox
    tools=[read_file], # this callable runs on the host, unchanged
)
```

<Warning>
  With `autonomy=True`, the agent still carries the host `execute_command` tool injected earlier in `__init__`. Even if you set `sandbox=`, the model can pick the unsandboxed `execute_command` path — `sandbox=` does not protect against it. For real containment, run the whole workflow remotely with `AgentFlow(run_on="docker")`.
</Warning>

***

## Sandbox Backends

### New import path

Backends now live in `praisonai_sandbox`. The old `praisonai.sandbox` path still works as a shim.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# New (recommended)
from praisonai_sandbox import DockerSandbox, SubprocessSandbox

# Backward-compatible shim (still works)
from praisonai.sandbox import SubprocessSandbox
```

Install just the backend you need with `pip install "praisonai-sandbox[docker]"` — see the [praisonai-sandbox Package](/docs/docs/features/praisonai-sandbox-package).

Choose the right backend for your security and performance needs:

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Start[Need code execution?] --> Q1{Untrusted code?}
    Q1 -->|No, trusted dev| Sub[subprocess<br/>fastest, no isolation]
    Q1 -->|Yes| Q2{Cloud or local?}
    Q2 -->|Local with Docker| Doc[docker<br/>full container isolation]
    Q2 -->|Cloud, scalable| E2B[e2b<br/>billed per second]
    Q2 -->|Local, no Docker| Nat[native<br/>OS sandboxing]

    classDef question fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef choice fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef start fill:#6366F1,stroke:#7C90A0,color:#fff

    class Start start
    class Q1,Q2 question
    class Sub,Doc,E2B,Nat choice
```

<Tabs>
  <Tab title="Subprocess (Local)">
    **Fastest, minimal isolation - development only**

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

    config = SandboxConfig.subprocess()
    agent = Agent(sandbox=config)
    ```

    | Requirements    | None (built-in)                     |
    | --------------- | ----------------------------------- |
    | **Use case**    | Development, trusted code only      |
    | **Isolation**   | ⚠️ Limited - NOT for untrusted code |
    | **Performance** | ✅ Fastest                           |

    <Warning>
      * **POSIX (Linux/macOS)**: Subprocess now blocks host env leakage, enforces `memory_mb`/`max_processes`/`max_open_files` via `setrlimit`, truncates output at `max_output_size`, and terminates the whole process group on timeout.
      * **Windows**: Still weaker — `setrlimit` and process-group kill are unavailable; the sandbox warns at runtime.
      * **Untrusted code**: Remains a Docker/E2B job, but the gap is now smaller.
    </Warning>
  </Tab>

  <Tab title="Docker">
    **Full container isolation - recommended for production**

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

    config = SandboxConfig.docker("python:3.12-slim")
    agent = Agent(sandbox=config)
    ```

    <Note>
      **Default Docker image:** the docker sandbox backend defaults to `python:3.12-slim`. Both `Agent(tools_run_on="docker")` and `Agent(sandbox=SandboxConfig.docker())` use the same image, so `run_in="docker"` and `tools_run_on="docker"` hand you the same Python runtime. Pass an explicit `image=` argument to override.
    </Note>

    | Requirements    | Docker daemon + `pip install "praisonai-sandbox[docker]"` (or `pip install praisonaiagents[sandbox-docker]`) |
    | --------------- | ------------------------------------------------------------------------------------------------------------ |
    | **Use case**    | Production, untrusted code, full isolation                                                                   |
    | **Isolation**   | ✅ Complete container isolation                                                                               |
    | **Performance** | Good                                                                                                         |

    Every command runs inside a container that mounts a per-sandbox host directory at `/sandbox` and sets it as the working directory. Files you write with `write_file()` are visible to the next `run_command()` and to `execute()`, and vice-versa. Each command still gets a fresh `--rm` container — persistence comes entirely from that mount, not from a long-lived container.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import SandboxConfig
    from praisonai_sandbox import DockerSandbox
    import asyncio

    async def demo():
        sandbox = DockerSandbox(config=SandboxConfig.docker("python:3.11-slim"))
        await sandbox.start()
        await sandbox.write_file("data.txt", "persisted")
        r = await sandbox.run_command("cat data.txt")
        print(r.stdout)   # "persisted"
        await sandbox.stop()

    asyncio.run(demo())
    ```

    <Note>
      `SandboxConfig.working_dir` defaults to `/workspace` in the config dataclass, but the docker backend uses `/sandbox` as its effective default working directory. Setting `working_dir` on `SandboxConfig` is still honoured when it differs from `/sandbox`.
    </Note>

    The mount does not widen the boundary — `ls /Users` from inside the container still fails.
  </Tab>

  <Tab title="E2B Cloud">
    **Scalable cloud VMs with full filesystem access**

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

    os.environ.setdefault("E2B_API_KEY", os.getenv("E2B_API_KEY", ""))
    config = SandboxConfig.e2b()
    agent = Agent(sandbox=config)
    ```

    | Requirements    | `E2B_API_KEY` + `pip install "praisonai-sandbox[e2b]"` (or `pip install praisonaiagents[sandbox] e2b-code-interpreter`) |
    | --------------- | ----------------------------------------------------------------------------------------------------------------------- |
    | **Use case**    | Cloud, scalable, full filesystem + shell                                                                                |
    | **Isolation**   | ✅ Cloud VM isolation                                                                                                    |
    | **Performance** | Good (network dependent)                                                                                                |
  </Tab>

  <Tab title="Native OS">
    **OS-level sandboxing without Docker**

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

    config = SandboxConfig.native(
        writable_paths=["./src", "./tests"],
        network=False,
    )
    agent = Agent(sandbox=config)
    ```

    | Requirements    | macOS (Seatbelt) or Linux (Landlock + bubblewrap) |
    | --------------- | ------------------------------------------------- |
    | **Use case**    | OS-native isolation without Docker                |
    | **Isolation**   | ✅ OS-level sandboxing                             |
    | **Performance** | Good                                              |

    **Native sandbox parameters:**

    | Param            | Type        | Default         | Description                        |
    | ---------------- | ----------- | --------------- | ---------------------------------- |
    | `writable_paths` | `List[str]` | `[os.getcwd()]` | Directories the agent may write to |
    | `network`        | `bool`      | `False`         | Whether to allow network access    |
  </Tab>

  <Tab title="Sandlock">
    **Kernel-level Landlock + seccomp isolation — strongest local sandbox**

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # New (recommended)
    from praisonai_sandbox import SandlockSandbox
    # Backward-compatible shim (still works): from praisonai.sandbox import SandlockSandbox
    from praisonaiagents import SandboxConfig

    config = SandboxConfig(sandbox_type="sandlock")
    ```

    | Requirements    | `pip install "praisonai-sandbox[sandlock]"` (or `pip install sandlock`) + **Linux ≥ 6.12**, `CONFIG_SECURITY_LANDLOCK=y`, seccomp not stripped |
    | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
    | **Use case**    | OS-native high-security isolation without Docker                                                                                               |
    | **Isolation**   | ✅ Kernel-enforced Landlock + seccomp-bpf                                                                                                       |
    | **Performance** | \~5ms overhead, no root required                                                                                                               |

    <Warning>
      **Breaking change (PR #1367):** `SandlockSandbox.__init__` now raises `RuntimeError` when the system's Landlock ABI is below the minimum required (currently ABI v6, Linux ≥ 6.12 with `CONFIG_SECURITY_LANDLOCK=y`). Previously it silently fell back to `SubprocessSandbox`. Containers that strip seccomp or run on older kernels will now fail loudly at startup.

      Use the graceful-degradation pattern if you need to support older kernels:

      ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
      # Backward-compatible shim still available: from praisonai.sandbox import ...
      from praisonai_sandbox import SandlockSandbox, SubprocessSandbox

      try:
          sb = SandlockSandbox(cfg)
      except (ImportError, RuntimeError):
          sb = SubprocessSandbox(cfg)
      ```
    </Warning>

    **Other behaviour changes in PR #1367:**

    | Change                              | Detail                                                           |
    | ----------------------------------- | ---------------------------------------------------------------- |
    | `clean_env=True` default            | Host environment variables are isolated from the child process   |
    | `stdout`/`stderr` type              | Decoded to `str` (previously returned raw `bytes`)               |
    | `max_cpu` from `limits.cpu_percent` | Now applied via `RLIMIT_CPU` (was accepted but ignored)          |
    | Timeout detection                   | `exit_code == -1` is sandlock's structural timeout sentinel      |
    | Network policy (enabled)            | `net_connect=["0-65535"]` — all ports allowed when network is on |
    | Network policy (disabled)           | `net_allow_hosts=[]` — no hosts allowed when network is off      |
    | `execute_file()`                    | Invokes script by path, not `-c <slurped source>`                |
  </Tab>

  <Tab title="SSH Remote">
    **Execute on remote servers via SSH**

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # Requires: pip install "praisonai-sandbox[ssh]"  (asyncssh)
    config = SandboxConfig(sandbox_type="ssh")
    ```

    | Requirements  | `pip install "praisonai-sandbox[ssh]"` (asyncssh) + SSH access |
    | ------------- | -------------------------------------------------------------- |
    | **Use case**  | Remote SSH execution                                           |
    | **Isolation** | Depends on remote system                                       |
  </Tab>

  <Tab title="Modal Cloud">
    **Modal cloud compute platform**

    <Warning>
      **Modal backend has no file storage.** `write_file()` returns `False` and logs
      a warning; `read_file()` returns `None`; `execute_file()` fails with a
      "no file storage" error. Pass code inline via `execute(code=...)` instead,
      or pick a backend with file storage — `docker`, `subprocess`, or `ssh`.

      Persisting files on Modal would require Modal Volumes, which the sandbox
      backend does not integrate yet. See [Modal caveats](/docs/docs/features/sandbox-backends#modal-caveats).
    </Warning>

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # Requires: pip install "praisonai-sandbox[modal]"
    config = SandboxConfig(sandbox_type="modal")
    ```

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # ❌ Won't work on Modal — write_file returns False, execute_file cannot read it
    await sandbox.write_file("script.py", "print('hi')")
    result = await sandbox.execute_file("script.py")

    # ✅ On Modal, pass the code directly
    result = await sandbox.execute(code="print('hi')")

    # ✅ Or use a backend that has file storage
    config = SandboxConfig(sandbox_type="docker")  # or "subprocess", "ssh"
    ```

    | Requirements  | `pip install "praisonai-sandbox[modal]"` + Modal account |
    | ------------- | -------------------------------------------------------- |
    | **Use case**  | Modal cloud compute                                      |
    | **Isolation** | ✅ Cloud isolation                                        |
  </Tab>

  <Tab title="Daytona">
    **Daytona development workspaces**

    <Warning>
      **Not yet implemented (PR #2122):** `DaytonaSandbox.is_available` returns `False` and `DaytonaSandbox.start()` raises `NotImplementedError`: *"Daytona backend not yet implemented. Use 'subprocess', 'docker', or 'e2b' sandbox instead."*
    </Warning>

    Use `subprocess`, `docker`, or `e2b` instead.
  </Tab>

  <Tab title="Novita Cloud">
    **Novita cloud sandboxes**

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # Requires: pip install "praisonai-sandbox[novita]"  (+ NOVITA_API_KEY)
    from praisonaiagents import Agent, SandboxConfig

    agent = Agent(
        name="Coder",
        instructions="Run code in a Novita cloud sandbox.",
        sandbox=SandboxConfig.novita(),
    )
    agent.start("Print the Python version")
    ```

    | Requirements    | `NOVITA_API_KEY` + `pip install "praisonai-sandbox[novita]"` |
    | --------------- | ------------------------------------------------------------ |
    | **Use case**    | Cloud, scalable, isolated Python / bash execution            |
    | **Isolation**   | ✅ Cloud VM isolation                                         |
    | **Performance** | Good (network dependent)                                     |
  </Tab>

  <Tab title="Capsule (Plugin)">
    **Plugin-provided secure sandbox — installed via praisonai-plugins**

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

    config = SandboxConfig.capsule()   # security_policy=strict() applied automatically
    agent = Agent(sandbox=config)
    ```

    | Requirements        | `pip install "praisonai-plugins[capsule]"`         |
    | ------------------- | -------------------------------------------------- |
    | **Use case**        | Plugin-registered secure sandbox backend           |
    | **Isolation**       | ✅ Provided by plugin backend                       |
    | **Security policy** | `SecurityPolicy.strict()` is applied automatically |

    <Note>
      `capsule` is registered by an external plugin under the `praisonai.sandbox` entry-point group. If the plugin is not installed, `SandboxManager` raises:
      `ValueError: Unknown sandbox type: 'capsule'. Available: [...]`
    </Note>
  </Tab>
</Tabs>

<Note>
  **Zero import cost when unused.** The sandbox subsystem — including `SandboxConfig`, `SandboxManager`, security checks, and all backends — is lazy-loaded. `from praisonaiagents import Agent` does not load any sandbox module. Sandbox modules are only imported the first time you set `sandbox=True` (or pass a `SandboxConfig`) on an `Agent`, or call a sandbox method. This keeps the default `import Agent` path light for agents that never execute code.
</Note>

### Which backend do I pick?

Pick a file-capable backend when your workflow writes, reads, or executes files; Modal runs code inline only.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Q[Need to run code<br/>in a sandbox] --> F{Need files<br/>on disk?}
    F -->|No — code only| M[modal<br/>docker / subprocess / e2b / ssh<br/>daytona / novita]
    F -->|Yes — write_file, read_file,<br/>execute_file| S[docker / subprocess / ssh<br/>daytona / novita / e2b]
    F -->|Yes and I want Modal| X[Not supported yet<br/>Modal Volumes integration TBD]

    classDef q fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff
    classDef warn fill:#F59E0B,stroke:#7C90A0,color:#fff

    class Q,F q
    class M,S ok
    class X warn
```

***

## What SecurityPolicy actually controls in subprocess

<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`, so a blocked name inside a string, a comment, or a longer identifier (`subprocess_count`) is not a false positive; aliases (`import os as x; x.system(...)`, `from os import system`) are resolved back to their real dotted names. Mirrors the [`sandbox-guarantees` warning](/docs/features/sandbox-guarantees#why-sandbox-is-not-a-capability-grant).
</Note>

<Warning>
  `blocked_imports` is now enforced on `execute()`. The rest of `SecurityPolicy` is still **not** enforced on the subprocess `execute()` 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 — a different PID is not evidence of isolation.

  For real containment, use a real container backend (`docker` / `e2b`), `sandlock` for a kernel-enforced local boundary, or `AgentFlow(run_on=…)`.
</Warning>

<Note>
  This warning is scoped to the **subprocess** backend. `DockerSandbox` **does** enforce the command-level clauses (`blocked_commands`, `allowed_commands`, `blocked_paths`, `allowed_paths`, `max_output_size`) on the host before dispatch, as of [PR #4303](https://github.com/MervinPraison/PraisonAI/pull/4303) — see [Docker SecurityPolicy enforcement](/docs/features/sandbox-backends#docker-securitypolicy-enforcement).
</Note>

Enforcement of `blocked_imports` refuses execution before the interpreter is spawned:

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

agent = Agent(
    name="Coder",
    instructions="Run Python.",
    sandbox=SandboxConfig(security_policy=SecurityPolicy.strict()),
)

result = agent.execute_code_sync("import subprocess\nprint('RAN')")
# result.status.name == "FAILED"
# result.stderr contains "Security policy violation: Blocked import: subprocess"
```

| Setting               | Effect on subprocess child                                                                                                                                                                                                                                                                                                     |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `allow_network=False` | Strips `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` from child env                                                                                                                                                                                                                                                                |
| `allow_network=True`  | Passes proxy vars through from host                                                                                                                                                                                                                                                                                            |
| `max_output_size`     | Truncates stdout/stderr; appends `[OUTPUT TRUNCATED]`                                                                                                                                                                                                                                                                          |
| `allow_file_write`    | Controls file write permissions (implementation varies by backend)                                                                                                                                                                                                                                                             |
| `allow_subprocess`    | Controls subprocess creation (implementation varies by backend)                                                                                                                                                                                                                                                                |
| `blocked_paths`       | Prevents access to specified paths                                                                                                                                                                                                                                                                                             |
| `blocked_commands`    | Blocks execution of specified shell commands                                                                                                                                                                                                                                                                                   |
| `blocked_imports`     | **Enforced on both `run_command()` and `execute()` for Python code** (AST-based, no substring false positives). Blocks direct/aliased/dotted/from imports and bare blocked builtins in call position ([PR #4875](https://github.com/MervinPraison/PraisonAI/pull/4875)). Non-Python languages are not parsed and pass through. |

***

## Resource limits in practice

<Tabs>
  <Tab title="Linux / macOS">
    POSIX `setrlimit` mapping enforces hard limits:

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

    limits = ResourceLimits(
        memory_mb=512,        # → RLIMIT_AS 
        max_processes=10,     # → RLIMIT_NPROC
        max_open_files=50,    # → RLIMIT_NOFILE
        timeout_seconds=30    # Wall clock timeout (separate)
    )
    ```

    When limits are exceeded:

    * Memory: Process is killed by the kernel
    * Processes: `fork()` fails with EAGAIN
    * Files: `open()` fails with EMFILE
    * Timeout: Whole process group killed with SIGKILL
  </Tab>

  <Tab title="Windows">
    Resource limits are not available on Windows. The subprocess warns at runtime:

    ```
    WARNING: Resource limits not supported on Windows - sandbox isolation is weaker
    ```

    Only timeout handling works:

    * Timeout: Process leader is killed (not process group)
    * Memory/processes/files: No enforcement

    For Windows users requiring resource limits, use Docker backend instead.

    | Platform capability                                 | Windows subprocess sandbox                                                                              |
    | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
    | `setrlimit` (memory / processes / file descriptors) | ❌ Not available — no enforcement                                                                        |
    | Process-group kill on timeout (`killpg`)            | ❌ Not available — only the leader is killed via `proc.kill()`                                           |
    | Symlink escape protection                           | ⚠️ Only reachable when symlink creation is permitted (Developer Mode / `SeCreateSymbolicLinkPrivilege`) |
    | `../` and absolute-path traversal protection        | ✅ Fully enforced                                                                                        |
    | Static code safety pre-checks (`check_code_safety`) | ✅ Fully enforced                                                                                        |

    For strong isolation on Windows, use the **Docker** or **E2B** backend — the subprocess backend on Windows is intended for development against trusted code only. See [issue #3214](https://github.com/MervinPraison/PraisonAI/issues/3214).
  </Tab>

  <Tab title="Docker">
    Docker enforces `max_processes` per **container** via `--pids-limit`, so unlike `RLIMIT_NPROC` — which is per user — the number means what it says:

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

    limits = ResourceLimits(
        memory_mb=512,        # → --memory 512m
        max_processes=128,    # → --pids-limit 128 (floored at MIN_CONTAINER_PIDS)
        timeout_seconds=30,   # Wall clock timeout
    )
    ```

    The pids cap has a **floor of `MIN_CONTAINER_PIDS = 128`** (importable from `praisonai_sandbox.docker`). The container applies `max(max_processes, 128)` on both `run_command()` and the `execute()` path (`_build_docker_command()`).

    The floor exists because `max_processes` defaults to `10` — a value chosen for the `setrlimit` world. A raw `--pids-limit 10` kills the container before the shell finishes starting, so `docker run` exits `125` with "No such container". The floor still bounds a fork bomb; it just stops the bound from being the bug.

    <Warning>
      Before PR #4107, `run_command()` accepted `max_processes` and **ignored it**, so fork bombs were unbounded on the bridged `execute_command` tool path. Both paths now honor it.
    </Warning>
  </Tab>
</Tabs>

***

## Timeout handling

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    subgraph "Timeout Response"
        A[⏰ Timeout Triggered] --> B{🖥️ OS?}
        B -->|POSIX| C[💀 killpg(SIGKILL)]
        B -->|Windows| D[💀 proc.kill()]
        C --> E[🧹 Process Group Dead]
        D --> F[⚠️ Leader Only Dead]
    end
    
    classDef timeout fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef decision fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef result fill:#8B0000,stroke:#7C90A0,color:#fff
    
    class A timeout
    class B decision
    class C,D,E,F result
```

On POSIX the whole process group is killed (`killpg(SIGKILL)`); on Windows only the leader is killed.

***

## Subprocess execution flow

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    subgraph "Subprocess Execution Flow"
        A[📨 Request] --> B[🔍 Policy Check]
        B --> C[🏠 Build Minimal Env]
        C --> D[⚖️ Apply setrlimit]
        D --> E[🚀 Exec Process]
        E --> F[📊 Monitor Output]
        F --> G[✂️ Truncate Output]
        G --> H[✅ Return Result]
    end
    
    classDef request fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef process fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff
    
    class A request
    class B,C,D,E,F,G process
    class H result
```

***

## Security Pre-checks

Static code analysis warns about potentially dangerous patterns before execution:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.sandbox import check_code_safety, format_warnings

code = """
import os
os.system("rm -rf /tmp/test")
"""

warnings = check_code_safety(code, language="python")
print(format_warnings(warnings))
# Security analysis found 1 potential issue(s):
# HIGH RISK:
#   - Direct system command execution (line 3)
#     Context: os.system("rm -rf /tmp/test")
# Note: These are warnings only. The sandbox provides real isolation.
```

### Security API

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.sandbox import (
    check_code_safety,
    format_warnings,
    get_security_summary,
    SecurityWarning,
)
```

| Function               | Signature                                                        | Description                                                                            |
| ---------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `check_code_safety`    | `(code: str, language: str = "python") -> List[SecurityWarning]` | Runs regex + AST analysis. Supports `python`, `bash`, generic fallback                 |
| `format_warnings`      | `(warnings: List[SecurityWarning]) -> str`                       | Pretty-print warnings grouped by severity                                              |
| `get_security_summary` | `(warnings) -> Dict`                                             | Returns `{"total_warnings", "by_severity", "max_severity", "is_safe", "has_critical"}` |

**SecurityWarning fields:**

| Field         | Type            | Description                                 |
| ------------- | --------------- | ------------------------------------------- |
| `pattern`     | `str`           | Regex pattern or AST node that triggered    |
| `message`     | `str`           | Human-readable warning                      |
| `severity`    | `str`           | `"low"`, `"medium"`, `"high"`, `"critical"` |
| `line_number` | `Optional[int]` | Line number in code                         |
| `context`     | `Optional[str]` | Source line text                            |

<Warning>
  Security pre-checks do not block execution - they provide warnings only. The sandbox provides the real isolation.
</Warning>

### Agent Integration

Agents automatically run security checks unless disabled:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
result = await agent.execute_code(
    code="import os; os.system('ls')",
    check_security=True,  # default
)
# Warnings are stored in result.metadata["security_warnings"]
```

<Note>
  Security warnings travel **only** through `result.metadata["security_warnings"]` — they are never forwarded as a `metadata` kwarg to the backend's `execute()`. A custom `SandboxProtocol.execute()` implementation does **not** need to accept a `metadata` argument.
</Note>

### Path Traversal Protection

`DockerSandbox` and `SubprocessSandbox` reject every path passed to
`write_file`, `read_file`, and `list_files` that resolves outside the
sandbox root — via `../`, absolute paths, or symlink escapes:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# Inside an agent's sandbox
sandbox.write_file("../../../etc/passwd", "x")   # returns False, logs warning
sandbox.read_file("/etc/shadow")                  # returns None
sandbox.list_files("../..")                       # returns []
```

Blocked attempts are logged as
`Path traversal attempt blocked: <path>`. This is in addition to the
container/OS isolation the sandbox already provides — defense in depth.

#### Symlink-safe file I/O on Docker

Your `write_file` / `read_file` are symlink-race safe on the Docker backend — no change to how you call them:

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

agent = Agent(
    name="Analyst",
    instructions="Write results to report.txt",
    sandbox=SandboxConfig.docker(image="python:3.12-slim"),
)
```

On Docker the sandbox directory is **bind-mounted into the container**, so any check that verifies a name string and *then* opens it can be raced from inside. A container running a loop of `ln -s /etc/passwd notes.txt` against repeated writes could swap the name between a regular file and a symlink and win the race — escaping to arbitrary host files and disclosing a host secret within a few hundred attempts.

No amount of stricter string checking fixes this: anything verified before the open can be invalidated after it.

The fix walks each path one component at a time relative to an open directory descriptor (`dir_fd=`) with `O_NOFOLLOW` at every step. A symlink substituted at any level fails the open instead of redirecting it. Two helpers in `praisonai_sandbox._compat` implement the walk:

| Helper                                               | What it does                                                                                                                                       |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `open_in_sandbox(temp_dir, path, flags, mode=0o600)` | Opens a path descriptor-relative with `O_NOFOLLOW` per component. Returns an open fd, or `None` if any component is a symlink or the path escapes. |
| `makedirs_in_sandbox(temp_dir, path)`                | Creates parent directories the same way, refusing symlinks at every level.                                                                         |

<Warning>
  Before this fix, the Docker check was a `safe_sandbox_path()` **string** validation followed by a name-based `open()` — a TOCTOU window a container could win repeatably. `safe_sandbox_path()` still exists but is **no longer load-bearing** for opens: it is used only for `list_files()` and the Windows fallback. Do not rely on it being symlink-safe on the Docker backend — the descriptor walk is what's safe.
</Warning>

<Note>
  **Windows note:** `os.O_NOFOLLOW` / `os.O_DIRECTORY` / `os.supports_dir_fd` don't exist on Windows, so `_HAS_OPENAT` is `False` and the resolved-string guard (`safe_sandbox_path()`) is used instead. That is safe here because Docker Desktop runs Linux containers inside a VM — there is no host-shared directory to race. Non-admin Windows users also cannot create symlinks unless **Developer Mode** is enabled or the process holds `SeCreateSymbolicLinkPrivilege`. See [PR #3224](https://github.com/MervinPraison/PraisonAI/pull/3224) / [issue #3214](https://github.com/MervinPraison/PraisonAI/issues/3214).
</Note>

On the docker backend, `list_files()` resolves symlinks on both sides — the sandbox root and every walked path — before comparing them, and silently drops any entry that would still resolve outside the sandbox root. This defends against the macOS `/var → /private/var` symlink and any similar case, so host paths are never returned:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sandbox.list_files("/")   # -> ['/a.txt', '/sub/b.txt']
                          # host paths like '../../private/var/folders/...' are never returned
```

***

## SandboxManager

Factory and async context manager for sandbox backends:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.sandbox import SandboxManager, SandboxConfig

# Convenience: one-shot run
config = SandboxConfig.docker("python:3.12-slim")
manager = SandboxManager(config)
result = await manager.run_code("print('Hello, World!')")

# Context manager: reuse one sandbox for multiple executions
async with SandboxManager(config) as sandbox:
    r1 = await sandbox.execute("x = 5")
    r2 = await sandbox.execute("print(x * 2)")  # persistence depends on backend
```

### SandboxManager API

| Method                                                         | Description                                      |
| -------------------------------------------------------------- | ------------------------------------------------ |
| `__init__(config: SandboxConfig \| None)`                      | Defaults to `SandboxConfig.subprocess()`         |
| `run_code(code, language="python", **kwargs) -> SandboxResult` | One-shot: open, run, cleanup                     |
| `__aenter__ / __aexit__`                                       | Async context manager yielding `SandboxProtocol` |
| `get_available_types() -> Dict[str, Dict]`                     | Returns availability info per backend            |

***

## Configuration Options

Progressive disclosure from simple to advanced. Every level below configures the explicit `agent.execute_code()` API; none adds a tool to `agent.tools`, and none isolates `tools=` callables.

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

# Level 1: Bool (simplest)
# Configures agent.execute_code(); gives the model NO tool; leaves tools= callables on the host.
agent = Agent(sandbox=True)

# Level 2: Factory shortcut
agent = Agent(sandbox=SandboxConfig.docker("python:3.12-slim"))

# Level 3: Full config
agent = Agent(
    sandbox=SandboxConfig(
        sandbox_type="e2b",
        resource_limits=ResourceLimits(memory_mb=512, timeout_seconds=60),
        security_policy=SecurityPolicy.standard(),
    )
)
```

### Factory Shortcuts

| Factory                                                    | What it does                                                                      |
| ---------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `SandboxConfig.subprocess()`                               | Local subprocess backend (default)                                                |
| `SandboxConfig.docker(image="python:3.12-slim")`           | Docker container backend                                                          |
| `SandboxConfig.e2b()`                                      | E2B cloud sandbox                                                                 |
| `SandboxConfig.novita()`                                   | Novita cloud sandbox — needs `NOVITA_API_KEY`                                     |
| `SandboxConfig.native(writable_paths=None, network=False)` | OS-native sandbox (Seatbelt / Landlock+bwrap)                                     |
| `SandboxConfig.capsule()`                                  | Plugin-provided Capsule sandbox — applies `SecurityPolicy.strict()` automatically |

### SandboxConfig Options

| Option            | Type             | Default              | Description                                                                                                                                                                                                          |
| ----------------- | ---------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sandbox_type`    | `str`            | `"subprocess"`       | Backend: subprocess, docker, e2b, native, sandlock, ssh, modal, daytona, novita. Additional names (e.g. `capsule`) can be provided by plugins registered under the `praisonai.sandbox` entry-point group.            |
| `image`           | `str`            | `"python:3.12-slim"` | Docker image (docker backend only)                                                                                                                                                                                   |
| `working_dir`     | `str`            | `"/workspace"`       | Working directory within sandbox. **The Docker backend uses `/sandbox` as its effective default** (the bind-mount target for `_temp_dir`); setting `working_dir` still overrides it when it differs from `/sandbox`. |
| `env`             | `Dict[str, str]` | `{}`                 | Environment variables                                                                                                                                                                                                |
| `resource_limits` | `ResourceLimits` | `ResourceLimits()`   | CPU, memory, timeout limits                                                                                                                                                                                          |
| `security_policy` | `SecurityPolicy` | `SecurityPolicy()`   | File and network access rules                                                                                                                                                                                        |
| `auto_cleanup`    | `bool`           | `True`               | Auto-cleanup after execution                                                                                                                                                                                         |
| `persist_files`   | `bool`           | `False`              | Keep files between runs                                                                                                                                                                                              |
| `mount_paths`     | `List[str]`      | `[]`                 | Paths to mount (host:container format)                                                                                                                                                                               |
| `metadata`        | `Dict[str, Any]` | `{}`                 | Additional configuration                                                                                                                                                                                             |

### ResourceLimits Presets

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

# Minimal limits for untrusted code
limits = ResourceLimits.minimal()  # 128MB, 30s, no network

# Standard limits  
limits = ResourceLimits.standard()  # 512MB, 60s

# Generous limits for trusted code
limits = ResourceLimits.generous()  # 2GB, 300s, network allowed
```

| Limit             | Minimal | Standard | Generous |
| ----------------- | ------- | -------- | -------- |
| `memory_mb`       | 128     | 512      | 2048     |
| `timeout_seconds` | 30      | 60       | 300      |
| `cpu_percent`     | 50      | 100      | 100      |
| `network_enabled` | ❌       | ❌        | ✅        |

### SecurityPolicy Presets

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

# Strict policy for untrusted code
policy = SecurityPolicy.strict()

# Standard policy
policy = SecurityPolicy.standard()

# Permissive policy (trusted code only)
policy = SecurityPolicy.permissive()

# Custom policy
policy = SecurityPolicy(
    allow_network=False,
    allow_file_write=True,
    allow_subprocess=False,
    blocked_paths=["/etc", "~/.ssh"],
    blocked_imports=["subprocess", "os.system"]
)
```

***

## Common Patterns

<Tabs>
  <Tab title="Data Science Agent">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent, SandboxConfig, ResourceLimits

    agent = Agent(
        name="DataScientist",
        instructions="Analyze data and create visualizations.",
        sandbox=SandboxConfig(
            sandbox_type="docker",
            image="python:3.12-slim",
            resource_limits=ResourceLimits.generous(),  # Need memory for data
            mount_paths=["./data:/workspace/data:ro"],   # Mount data read-only
        )
    )

    agent.start("Load the CSV file and show descriptive statistics")
    ```
  </Tab>

  <Tab title="Secure Code Review">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent, SandboxConfig, SecurityPolicy

    agent = Agent(
        name="SecurityReviewer", 
        instructions="Review code for security issues.",
        sandbox=SandboxConfig(
            sandbox_type="e2b",
            security_policy=SecurityPolicy.strict(),  # Maximum security
        )
    )

    agent.start("Analyze this code for potential vulnerabilities")
    ```
  </Tab>

  <Tab title="Batch Processing">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents.sandbox import SandboxManager, SandboxConfig

    config = SandboxConfig(
        sandbox_type="docker",
        persist_files=True,    # Keep files between runs
        auto_cleanup=False     # Manual cleanup
    )

    # Process multiple files using same sandbox
    async with SandboxManager(config) as sandbox:
        for file in files:
            result = await sandbox.execute(f"process_file('{file}')")
            print(result.stdout)
    ```
  </Tab>
</Tabs>

***

## Timeout Behavior & Resource Cleanup

Understanding how different backends handle timeouts and resource cleanup ensures your resource limits are properly enforced.

| Backend                                                   | Local cleanup on timeout                                                                                           | Remote cleanup on timeout                                                                                              |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `subprocess`                                              | Process killed                                                                                                     | N/A                                                                                                                    |
| `docker`                                                  | Client process killed **and** `docker kill <container>` issued on the named container (`praisonai-<execution_id>`) | Container stopped — resource limits enforced                                                                           |
| `ssh`                                                     | Local SSH client killed                                                                                            | Remote process terminated via `timeout N sh -c ...` wrapper; remote temp file cleaned up via `finally` (even on error) |
| `e2b`, `modal`, `native`, `sandlock`, `daytona`, `novita` | Backend-managed                                                                                                    | Backend-managed                                                                                                        |

### Docker Timeout Handling

Docker containers get deterministic names and are properly killed on timeout:

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Agent
    participant Sandbox
    participant Docker as docker run --name praisonai-X
    participant Container
    
    Agent->>Sandbox: execute(code, timeout=30)
    Sandbox->>Docker: start container
    Docker->>Container: run code
    
    alt Execution completes
        Container-->>Docker: exit 0
        Docker-->>Sandbox: success
    else Timeout occurs
        Note over Sandbox: asyncio.TimeoutError
        Sandbox->>Docker: docker kill praisonai-X
        Docker->>Container: SIGKILL
        Container-->>Docker: terminated
        Docker-->>Sandbox: timeout result
    end
    
    Sandbox-->>Agent: SandboxResult
```

**Why containers are no longer orphaned:** Every `docker run` is launched with `--name praisonai-<execution_id>`. On timeout, the sandbox issues `docker kill <name>` to stop the actual container — not just detach the client. Your `memory_mb` and `cpu_percent` limits are now enforced through the entire execution lifecycle.

<Note>
  This is the shipped behaviour as of [PR #4109](https://github.com/MervinPraison/PraisonAI/pull/4109). The fix has two moving parts:

  1. Every `docker run` from `execute()` gets `--name praisonai-<execution_id>` — previously the container was unnamed, so Docker assigned a random name that neither the timeout handler nor `praisonai managed ps` could find.
  2. On `asyncio.TimeoutError`, the sandbox spawns `docker kill <container_name>` and awaits it **before** calling `proc.kill()` — killing the container, not just the docker client. Verified before/after: containers left after a timeout went from **1 → 0**.
</Note>

<Warning>
  Before PR #4109, a timed-out docker execution left its container running under a random name:

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

  cfg = SandboxConfig.docker()
  cfg.resource_limits.timeout_seconds = 5

  agent = Agent(name="Coder", instructions="Run Python.", sandbox=cfg)
  agent.execute_code_sync("import time; time.sleep(300)")
  # -> SandboxStatus.TIMEOUT
  # docker ps            # (pre-#4109) still shows a running container with a random name
  # praisonai managed ps # can't see it — no managed label, no known name
  ```
</Warning>

<Note>
  **Two labels, two lifecycles.** `execute()`-path containers carry `praisonai=sandbox-exec`, **not** `praisonai=managed`. They are ephemeral (`--rm`, one per execution, named `praisonai-<uuid>`), while managed instances are long-lived (named `praisonai_<id>`). `praisonai managed ps` deliberately lists only `praisonai=managed`; to see ephemeral execution containers use `docker ps --filter label=praisonai=sandbox-exec`. Tagging `--rm` containers as managed would make `managed ps` list a name that `managed stop` could never reclaim. See [Sandbox Backends](/docs/features/sandbox-backends#docker-labels-two-lifecycles) and [Reclaim Stray Sandboxes](/docs/features/reclaim-stray-sandboxes).
</Note>

### SSH Timeout Handling

SSH backend prevents both remote process leaks and temp file accumulation:

**Remote process cleanup:** Commands are wrapped with `timeout N sh -c ...` to ensure remote processes terminate even if the SSH connection drops.

**Temp file cleanup:** File cleanup (`rm -f`) is now in a `finally` block. Even if execution raises (timeout, network blip), the remote temp file is removed. Cleanup errors are swallowed so they never mask the real execution result.

***

## Best Practices

<AccordionGroup>
  <Accordion title="Use Docker for untrusted code">
    Always use Docker sandbox when executing code from untrusted sources. Subprocess isolation is not sufficient for security-critical applications.
  </Accordion>

  <Accordion title="Enable security pre-checks">
    Keep `check_security=True` (default) when calling `execute_code()`. Review warnings in `result.metadata["security_warnings"]` for insights.
  </Accordion>

  <Accordion title="Set appropriate resource limits">
    Configure memory and timeout limits based on expected workload. Start with minimal limits and increase as needed.
  </Accordion>

  <Accordion title="Disable network by default">
    Keep `allow_network=False` unless code specifically needs network access. This prevents data exfiltration.
  </Accordion>

  <Accordion title="Use context managers for persistence">
    For multiple operations on the same sandbox, use `async with SandboxManager(config) as sandbox:` to reuse the environment efficiently.
  </Accordion>

  <Accordion title="File system boundaries are enforced">
    Filesystem boundaries are enforced — write\_file/read\_file/list\_files reject paths that escape the sandbox root, even before the backend's isolation kicks in.
  </Accordion>
</AccordionGroup>

***

<Tip>
  Prefer a repo-committed environment definition over per-call kwargs? See [Environment File](/docs/features/environment-yaml).
</Tip>

***

## Related

<CardGroup cols={2}>
  <Card title="Where Does It Run" icon="location-dot" href="/docs/features/where-does-it-run">
    Ask any agent where its thinking and tools actually execute
  </Card>

  <Card title="Sandboxed Agent" icon="shield-check" href="/docs/features/sandboxed-agent">
    Complete agent with built-in sandbox
  </Card>

  <Card title="Sandbox Backends" icon="server" href="/docs/features/sandbox-backends">
    Shell control and backend selection
  </Card>

  <Card title="Isolated Code with Tools" icon="code-branch" href="/docs/features/code-execution-with-tools#isolated-code-with-tools-bridge">
    Service registered tool calls from an isolated run via a `CodeToolBridge`
  </Card>
</CardGroup>
