Skip to main content
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.
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. It also does not contain the whole workflow. To let the model run code, hand it a tool explicitly (see What sandbox= configures), or run the whole workflow in a real container with AgentFlow(run_on="docker").
Not sure whether the model is calling out to a container? Run print(agent.where_does_it_run()) — see Where Your Agent Runs.
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 for the from praisonai_sandbox import … import path.
Need model-generated code to call your registered tools? See Code Execution with Tools. A sandbox can now service tool calls via a CodeToolBridge — the script runs isolated while tool calls are gated in the parent by the same allow-list and approval gate.
The user asks the agent to execute generated code; work stays inside an isolated sandbox instead of the host shell.

Quick Start

1

Simple Usage

Enable the sandbox with a single line, then invoke it explicitly with execute_code_sync().
2

With Configuration

Use factory methods for specific sandbox types.
3

Full Configuration

Complete control over sandbox settings.
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")).

Agent Sandbox API

When sandbox is configured, agents gain powerful execution capabilities:

SandboxMixin API


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.
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. sandbox= is a restriction flag, not a capability grant — the model cannot call the sandbox unless you explicitly hand it a tool.
Verify that no tools are attached — the model sees an empty tool list from sandbox= alone:
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=:
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).

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=.
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").

Sandbox Backends

New import path

Backends now live in praisonai_sandbox. The old praisonai.sandbox path still works as a shim.
Install just the backend you need with pip install "praisonai-sandbox[docker]" — see the praisonai-sandbox Package. Choose the right backend for your security and performance needs:
Fastest, minimal isolation - development only
  • 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.
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.

What SecurityPolicy actually controls in subprocess

The default subprocess backend does not enforce most of SecurityPolicy on the execute() code path. Only run_command() currently checks the policy — execute() does not. Verified behavior on the default backend:
  • allow_network=False does not block outbound HTTPS — execute() never checks it (HTTPS returned 200 with the flag set).
  • blocked_paths=['~/.ssh', ...] does not stop reading an SSH private key.
  • blocked_imports=['subprocess', ...] does not stop import subprocess.
  • 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) or run the whole workflow in a remote sandbox with AgentFlow(run_on=…).

Resource limits in practice

POSIX setrlimit mapping enforces hard limits:
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

Timeout handling

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

Subprocess execution flow


Security Pre-checks

Static code analysis warns about potentially dangerous patterns before execution:

Security API

SecurityWarning fields:
Security pre-checks do not block execution - they provide warnings only. The sandbox provides the real isolation.

Agent Integration

Agents automatically run security checks unless disabled:
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.

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:
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. Your write_file / read_file are symlink-race safe on the Docker backend — no change to how you call them:
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:
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.
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 / issue #3214.
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:

SandboxManager

Factory and async context manager for sandbox backends:

SandboxManager API


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.

Factory Shortcuts

SandboxConfig Options

ResourceLimits Presets

SecurityPolicy Presets


Common Patterns


Timeout Behavior & Resource Cleanup

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

Docker Timeout Handling

Docker containers get deterministic names and are properly killed on timeout: 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.
This is the shipped behaviour as of PR #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.
Before PR #4109, a timed-out docker execution left its container running under a random name:
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 and Reclaim Stray Sandboxes.

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

Always use Docker sandbox when executing code from untrusted sources. Subprocess isolation is not sufficient for security-critical applications.
Keep check_security=True (default) when calling execute_code(). Review warnings in result.metadata["security_warnings"] for insights.
Configure memory and timeout limits based on expected workload. Start with minimal limits and increase as needed.
Keep allow_network=False unless code specifically needs network access. This prevents data exfiltration.
For multiple operations on the same sandbox, use async with SandboxManager(config) as sandbox: to reuse the environment efficiently.
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.

Prefer a repo-committed environment definition over per-call kwargs? See Environment File.

Where Does It Run

Ask any agent where its thinking and tools actually execute

Sandboxed Agent

Complete agent with built-in sandbox

Sandbox Backends

Shell control and backend selection

Isolated Code with Tools

Service registered tool calls from an isolated run via a CodeToolBridge