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.
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)The user asks the agent to execute generated code; work stays inside an isolated sandbox instead of the host shell.
Quick Start
- Simple Usage
Enable the sandbox with a single line, then invoke it explicitly with
execute_code_sync().python 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) - With Configuration
Use factory methods for specific sandbox types.
python 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) - Full Configuration
Complete control over sandbox settings.
python 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)
Agent Sandbox API
When sandbox is configured, agents gain powerful execution capabilities:
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.
Verify that no tools are attached — the model sees an empty tool list from sandbox= alone:
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=:
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")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=Trueinjects a hostexecute_commandtool that runs unsandboxed regardless ofsandbox=.
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
)Sandbox Backends
New import path
Backends now live in praisonai_sandbox. The old praisonai.sandbox path still works as a shim.
# New (recommended)
from praisonai_sandbox import DockerSandbox, SubprocessSandbox
# Backward-compatible shim (still works)
from praisonai.sandbox import SubprocessSandboxInstall 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:
Subprocess (Local)
Fastest, minimal isolation – development only
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 |
Docker
Full container isolation – recommended for production
from praisonaiagents import SandboxConfig
config = SandboxConfig.docker("python:3.12-slim")
agent = Agent(sandbox=config)| 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.
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())The mount does not widen the boundary — ls /Users from inside the container still fails.
E2B Cloud
Scalable cloud VMs with full filesystem access
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) |
Native OS
OS-level sandboxing without Docker
from praisonaiagents import SandboxConfig
config = SandboxConfig.native(
writable_paths=["./src", "./tests"],
network=False,
)
agent = Agent(sandbox=config)| Requirements | Linux ≥ 6.12 + praisonai-sandbox[sandlock] (Landlock + seccomp) |
|---|---|
| Use case | OS-native isolation without Docker |
| Isolation | ✅ Kernel-enforced Landlock + seccomp-bpf |
| 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 |
Sandlock
Kernel-level Landlock + seccomp isolation — strongest local sandbox
# 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 |
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> |
SSH Remote
Execute on remote servers via SSH
# 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 |
Modal Cloud
Modal cloud compute platform
# Requires: pip install "praisonai-sandbox[modal]"
config = SandboxConfig(sandbox_type="modal")# ❌ 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 |
Daytona
Daytona development workspaces
Use subprocess, docker, or e2b instead.
Novita Cloud
Novita cloud sandboxes
# 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) |
Capsule (Plugin)
Plugin-provided secure sandbox — installed via praisonai-plugins
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 |
Which backend do I pick?
Pick a file-capable backend when your workflow writes, reads, or executes files; Modal runs code inline only.
What SecurityPolicy actually controls in subprocess
Enforcement of blocked_imports refuses execution before the interpreter is spawned:
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). Non-Python languages are not parsed and pass through. |
Resource limits in practice
Linux / macOS
POSIX setrlimit mapping enforces hard limits:
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
Windows
Resource limits are not available on Windows. The subprocess warns at runtime:
WARNING: Resource limits not supported on Windows - sandbox isolation is weakerOnly 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.
Docker
Docker enforces max_processes per container via --pids-limit, so unlike RLIMIT_NPROC — which is per user — the number means what it says:
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.
Timeout handling
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 resultOn 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:
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
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 |
Agent Integration
Agents automatically run security checks unless disabled:
result = await agent.execute_code(
code="import os; os.system('ls')",
check_security=True, # default
)
# Warnings are stored in result.metadata["security_warnings"]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:
# 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:
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. |
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:
sandbox.list_files("/") # -> ['/a.txt', '/sub/b.txt']
# host paths like '../../private/var/folders/...' are never returnedSandboxManager
Factory and async context manager for sandbox backends:
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 backendSandboxManager 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.
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) | Linux sandlock backend (Landlock + seccomp); raises ImportError on macOS. For Seatbelt/bwrap see OS Sandbox |
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
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
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
Data Science Agent
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")Secure Code Review
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")Batch Processing
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)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:
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.
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
Use Docker for untrusted code
Always use Docker sandbox when executing code from untrusted sources. Subprocess isolation is not sufficient for security-critical applications.
Enable security pre-checks
Keep check_security=True (default) when calling execute_code(). Review warnings in result.metadata["security_warnings"] for insights.
Set appropriate resource limits
Configure memory and timeout limits based on expected workload. Start with minimal limits and increase as needed.
Disable network by default
Keep allow_network=False unless code specifically needs network access. This prevents data exfiltration.
Use context managers for persistence
For multiple operations on the same sandbox, use async with SandboxManager(config) as sandbox: to reuse the environment efficiently.
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.