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

Diagram

Sandbox Code Execution

sandbox=True

subprocess

docker

e2b

novita

native

🤖 Agent

⚙️ SandboxManager

🔍 Security Check

📦 Backend

🖥️ Local

🐳 Docker

☁️ E2B Cloud

☁️ Novita Cloud

🛡️ OS-Native

✅ SandboxResult

Quick Start

  1. 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)
  2. 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)
  3. 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:

python
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

MethodSignatureDescription
has_sandboxproperty -> boolWhether sandbox is configured
execute_codeasync (code, language="python", check_security=True, **kwargs) -> SandboxResultAsync 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) -> SandboxResultSync 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_cleanupasync () -> NoneForce 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:

python
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
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=True injects a host execute_command tool that runs unsandboxed regardless of sandbox=.
python
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.

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

Choose the right backend for your security and performance needs:

Diagram

No, trusted dev

Yes

Local with Docker

Cloud, scalable

Local, no Docker

Need code execution?

Untrusted code?

subprocess
fastest, no isolation

Cloud or local?

docker
full container isolation

e2b
billed per second

native
OS sandboxing

Subprocess (Local)

Fastest, minimal isolation – development only

python
from praisonaiagents import SandboxConfig

config = SandboxConfig.subprocess()
agent = Agent(sandbox=config)
RequirementsNone (built-in)
Use caseDevelopment, trusted code only
Isolation⚠️ Limited – NOT for untrusted code
Performance✅ Fastest

Docker

Full container isolation – recommended for production

python
from praisonaiagents import SandboxConfig

config = SandboxConfig.docker("python:3.12-slim")
agent = Agent(sandbox=config)
RequirementsDocker daemon + pip install "praisonai-sandbox[docker]" (or pip install praisonaiagents[sandbox-docker])
Use caseProduction, untrusted code, full isolation
Isolation✅ Complete container isolation
PerformanceGood

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

python
import os
from praisonaiagents import SandboxConfig

os.environ.setdefault("E2B_API_KEY", os.getenv("E2B_API_KEY", ""))
config = SandboxConfig.e2b()
agent = Agent(sandbox=config)
RequirementsE2B_API_KEY + pip install "praisonai-sandbox[e2b]" (or pip install praisonaiagents[sandbox] e2b-code-interpreter)
Use caseCloud, scalable, full filesystem + shell
Isolation✅ Cloud VM isolation
PerformanceGood (network dependent)

Native OS

OS-level sandboxing without Docker

python
from praisonaiagents import SandboxConfig

config = SandboxConfig.native(
    writable_paths=["./src", "./tests"],
    network=False,
)
agent = Agent(sandbox=config)
RequirementsLinux ≥ 6.12 + praisonai-sandbox[sandlock] (Landlock + seccomp)
Use caseOS-native isolation without Docker
Isolation✅ Kernel-enforced Landlock + seccomp-bpf
PerformanceGood

Native sandbox parameters:

ParamTypeDefaultDescription
writable_pathsList[str][os.getcwd()]Directories the agent may write to
networkboolFalseWhether to allow network access

Sandlock

Kernel-level Landlock + seccomp isolation — strongest local sandbox

python
# 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")
Requirementspip install "praisonai-sandbox[sandlock]" (or pip install sandlock) + Linux ≥ 6.12, CONFIG_SECURITY_LANDLOCK=y, seccomp not stripped
Use caseOS-native high-security isolation without Docker
Isolation✅ Kernel-enforced Landlock + seccomp-bpf
Performance~5ms overhead, no root required

Other behaviour changes in PR #1367:

ChangeDetail
clean_env=True defaultHost environment variables are isolated from the child process
stdout/stderr typeDecoded to str (previously returned raw bytes)
max_cpu from limits.cpu_percentNow applied via RLIMIT_CPU (was accepted but ignored)
Timeout detectionexit_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

python
# Requires: pip install "praisonai-sandbox[ssh]"  (asyncssh)
config = SandboxConfig(sandbox_type="ssh")
Requirementspip install "praisonai-sandbox[ssh]" (asyncssh) + SSH access
Use caseRemote SSH execution
IsolationDepends on remote system

Modal Cloud

Modal cloud compute platform

python
# Requires: pip install "praisonai-sandbox[modal]"
config = SandboxConfig(sandbox_type="modal")
python
# ❌ 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"
Requirementspip install "praisonai-sandbox[modal]" + Modal account
Use caseModal cloud compute
Isolation✅ Cloud isolation

Daytona

Daytona development workspaces

Use subprocess, docker, or e2b instead.

Novita Cloud

Novita cloud sandboxes

python
# 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")
RequirementsNOVITA_API_KEY + pip install "praisonai-sandbox[novita]"
Use caseCloud, scalable, isolated Python / bash execution
Isolation✅ Cloud VM isolation
PerformanceGood (network dependent)

Capsule (Plugin)

Plugin-provided secure sandbox — installed via praisonai-plugins

python
from praisonaiagents import SandboxConfig

config = SandboxConfig.capsule()   # security_policy=strict() applied automatically
agent = Agent(sandbox=config)
Requirementspip install "praisonai-plugins[capsule]"
Use casePlugin-registered secure sandbox backend
Isolation✅ Provided by plugin backend
Security policySecurityPolicy.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.

Diagram

No — code only

Yes — write_file, read_file,
execute_file

Yes and I want Modal

Need to run code
in a sandbox

Need files
on disk?

modal
docker / subprocess / e2b / ssh
daytona / novita

docker / subprocess / ssh
daytona / novita / e2b

Not supported yet
Modal Volumes integration TBD


What SecurityPolicy actually controls in subprocess

Enforcement of blocked_imports refuses execution before the interpreter is spawned:

python
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"
SettingEffect on subprocess child
allow_network=FalseStrips HTTP_PROXY / HTTPS_PROXY / NO_PROXY from child env
allow_network=TruePasses proxy vars through from host
max_output_sizeTruncates stdout/stderr; appends [OUTPUT TRUNCATED]
allow_file_writeControls file write permissions (implementation varies by backend)
allow_subprocessControls subprocess creation (implementation varies by backend)
blocked_pathsPrevents access to specified paths
blocked_commandsBlocks execution of specified shell commands
blocked_importsEnforced 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:

python
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 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 capabilityWindows 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:

python
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

Diagram
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

Diagram

Subprocess Execution Flow

📨 Request

🔍 Policy Check

🏠 Build Minimal Env

⚖️ Apply setrlimit

🚀 Exec Process

📊 Monitor Output

✂️ Truncate Output

✅ Return Result


Security Pre-checks

Static code analysis warns about potentially dangerous patterns before execution:

python
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
from praisonaiagents.sandbox import (
    check_code_safety,
    format_warnings,
    get_security_summary,
    SecurityWarning,
)
FunctionSignatureDescription
check_code_safety(code: str, language: str = "python") -> List[SecurityWarning]Runs regex + AST analysis. Supports python, bash, generic fallback
format_warnings(warnings: List[SecurityWarning]) -> strPretty-print warnings grouped by severity
get_security_summary(warnings) -> DictReturns {"total_warnings", "by_severity", "max_severity", "is_safe", "has_critical"}

SecurityWarning fields:

FieldTypeDescription
patternstrRegex pattern or AST node that triggered
messagestrHuman-readable warning
severitystr"low", "medium", "high", "critical"
line_numberOptional[int]Line number in code
contextOptional[str]Source line text

Agent Integration

Agents automatically run security checks unless disabled:

python
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:

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

HelperWhat 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:

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

MethodDescription
`__init__(config: SandboxConfig \None)`Defaults to SandboxConfig.subprocess()
run_code(code, language="python", **kwargs) -> SandboxResultOne-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
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

FactoryWhat 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

OptionTypeDefaultDescription
sandbox_typestr"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.
imagestr"python:3.12-slim"Docker image (docker backend only)
working_dirstr"/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.
envDict[str, str]{}Environment variables
resource_limitsResourceLimitsResourceLimits()CPU, memory, timeout limits
security_policySecurityPolicySecurityPolicy()File and network access rules
auto_cleanupboolTrueAuto-cleanup after execution
persist_filesboolFalseKeep files between runs
mount_pathsList[str][]Paths to mount (host:container format)
metadataDict[str, Any]{}Additional configuration

ResourceLimits Presets

python
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
LimitMinimalStandardGenerous
memory_mb1285122048
timeout_seconds3060300
cpu_percent50100100
network_enabled❌❌✅

SecurityPolicy Presets

python
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

python
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

python
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

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

BackendLocal cleanup on timeoutRemote cleanup on timeout
subprocessProcess killedN/A
dockerClient process killed and docker kill <container> issued on the named container (praisonai-<execution_id>)Container stopped — resource limits enforced
sshLocal SSH client killedRemote process terminated via timeout N sh -c ... wrapper; remote temp file cleaned up via finally (even on error)
e2b, modal, native, sandlock, daytona, novitaBackend-managedBackend-managed

Docker Timeout Handling

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

Diagram
Containerdocker run –name praisonai-XSandboxAgentContainerdocker run –name praisonai-XSandboxAgentasyncio.TimeoutErroralt[Execution completes][Timeout occurs]execute(code, timeout=30)start containerrun codeexit 0successdocker kill praisonai-XSIGKILLterminatedtimeout resultSandboxResult

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.



Related