Sandbox Backends
Execute commands safely with configurable shell control across different backends
Sandbox backends provide isolated command execution environments with explicit shell control to prevent injection attacks while enabling shell features when needed.
from praisonaiagents import Agent
agent = Agent(
name="Coder",
instructions="Run shell commands safely",
sandbox=True,
)
agent.start("List Python files in the current directory")The user runs commands through the agent; the sandbox backend isolates execution with explicit shell control.
Quick Start
- Simple Usage
Enable sandbox on the agent — subprocess backend is the default:
python from praisonaiagents import Agent agent = Agent( name="System Agent", instructions="Execute system commands safely", sandbox=True, ) agent.start("List files in the current directory") - With Configuration
Pick a specific backend via
SandboxConfigor the CLI--sandbox-typeflag:python from praisonaiagents import Agent, SandboxConfig agent = Agent( name="Data Agent", instructions="Process data in an isolated container", sandbox=SandboxConfig.docker("python:3.12-slim"), ) agent.start("Run pip list and summarise installed packages")bash praisonai sandbox run --code "print('hello')" --type docker
How It Works
| Backend | Use Case | Security Level |
|---|---|---|
SubprocessSandbox | Local development, scripts | Medium (OS-level isolation, POSIX only) |
DockerSandbox | Production, untrusted code | High (container isolation) |
SSHSandbox | Remote execution | High (network isolation) |
Docker Shared Workspace
DockerSandbox mounts a per-sandbox host directory at /sandbox and runs every command in it. That means:
write_file("data.txt", "…")places the file at/sandbox/data.txtinside the container.- The next
run_command("cat data.txt")sees it. execute(...)writes and reads the same directory — the script itself lands at/sandbox/code_<id>.py.- Each command is still
--rm; persistence comes from the mount, not from a reused 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.run_command("echo hello > f.txt")
r = await sandbox.run_command("cat f.txt")
print(r.stdout) # "hello"
await sandbox.stop()
asyncio.run(demo())| Property | Value |
|---|---|
| Workspace inside container | /sandbox |
| Default working directory | /sandbox |
| Persistence across commands | Yes, via bind mount |
| Persistence across sandboxes | No (_temp_dir is per DockerSandbox instance) |
| Host filesystem visible? | No (ls /Users fails) |
Docker labels: two lifecycles
PraisonAI tags Docker containers with two different labels, one per lifecycle. praisonai=managed is the sole input to DockerCompute.list_instances() — the label praisonai managed ps filters on.
| Label | Applied by | Container lifetime | Listed by praisonai managed ps |
|---|---|---|---|
praisonai=managed | DockerCompute (managed instance for run_on= / compute=) | Long-lived; reclaimed by managed stop, the finalizer, or ashutdown() | ✅ |
praisonai=sandbox-exec | DockerSandbox.execute() (per-execution) | Ephemeral (--rm); killed on timeout by name | ❌ |
The two labels also carry different name shapes: managed instances are praisonai_<id>, ephemeral execution containers are praisonai-<uuid> (named as of PR #4109 so a timed-out execution can be killed by name). managed ps deliberately does not list sandbox-exec containers — tagging --rm containers as managed would surface a name that managed stop could never reclaim. To see them directly:
docker ps --filter label=praisonai=sandbox-execSee Reclaim Stray Sandboxes and Placement.
Both spellings mean the same file
Pass either the container path (/sandbox/report.txt, the path you see inside the container) or the bare relative name (report.txt) to write_file / read_file — both resolve to the same file:
await sandbox.write_file("/sandbox/report.txt", "done")
await sandbox.read_file("report.txt") # -> "done"
await sandbox.run_command("cat /sandbox/report.txt") # -> "done"_sandbox_relative() (in praisonai_sandbox.docker) strips the exact /sandbox / /sandbox/… prefix before joining onto the sandbox root. A genuine subdirectory literally named sandbox still works — only the exact mount prefix is stripped.
| You pass | Resolves to |
|---|---|
report.txt | <temp_dir>/report.txt |
/report.txt | <temp_dir>/report.txt |
/sandbox/report.txt | <temp_dir>/report.txt |
sandbox/nested.txt | <temp_dir>/sandbox/nested.txt (a real subdir) |
Modal caveats
ModalSandbox functions are stateless, so it has no file storage — the opposite of Docker’s /sandbox mount above.
# ❌ 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"Unlike other backends, a False return from write_file() on Modal is expected — callers that branch on the result (if not await sandbox.write_file(...)) can now detect the failed write instead of trusting a false True.
Docker SecurityPolicy enforcement
DockerSandbox enforces command-level SecurityPolicy clauses on every run_command() — the same enforcement SubprocessSandbox applies, run on the host before Docker is invoked (PR #4303, closing issue #4302). A refused command returns SandboxResult(status=FAILED, error=…) and Docker is never contacted.
from praisonaiagents import Agent, SandboxConfig
from praisonaiagents.sandbox import SecurityPolicy
agent = Agent(
name="Coder",
instructions="Only run trusted Python scripts.",
sandbox=SandboxConfig(
sandbox_type="docker",
security_policy=SecurityPolicy(
allowed_commands=["python"],
blocked_paths=["/etc/passwd", "~/.ssh"],
max_output_size=1_000_000,
),
),
)
# Refused on the host before Docker starts — "Command not in allowlist: sh":
agent.execute_code_sync("import os; os.system('sh -c \"cat /etc/passwd\"')")Use it directly without an Agent:
from praisonai_sandbox import DockerSandbox
from praisonaiagents.sandbox import SandboxConfig, SecurityPolicy
config = SandboxConfig(
sandbox_type="docker",
security_policy=SecurityPolicy(allowed_commands=["python"]),
)
sandbox = DockerSandbox(image=config.image, config=config)Each clause maps to a specific check on the host:
| Clause | Enforced by DockerSandbox? |
|---|---|
blocked_commands | Yes — on run_command() before dispatch |
allowed_commands | Yes — on run_command() (has no container analogue) |
blocked_paths | Yes — realpath + expanduser, on run_command() |
allowed_paths | Yes — realpath + expanduser, on run_command() |
max_output_size | Yes — on both run_command() and execute() output buffers |
allow_subprocess | No — the container is the boundary that clause approximates |
Which Backend Should I Use?
Pick a backend based on where the code runs and how much you trust it.
All Built-in Sandbox Backends
PR #2003 exposes all built-in sandboxes through SandboxRegistry — selectable by string name from the CLI or Python.
| Name | Class | Typical use |
|---|---|---|
docker | DockerSandbox | Container isolation for production |
subprocess | SubprocessSandbox | Fast local development (default) |
sandlock | SandlockSandbox | Hardened local sandbox |
ssh | SSHSandbox | Remote server execution |
modal | ModalSandbox | Modal cloud sandboxes — no file storage (Modal caveats) |
daytona | DaytonaSandbox | Daytona cloud sandboxes (requires the daytona extra + DAYTONA_API_KEY) |
e2b | E2BSandbox | E2B cloud code interpreter |
novita | NovitaSandbox | Novita cloud sandboxes (requires the novita extra + NOVITA_API_KEY) |
Plugin-registered backends (installed separately, resolved via the praisonai.sandbox entry-point group):
| Name | Source | Typical use |
|---|---|---|
capsule | Plugin (via praisonai.sandbox) | Plugin-provided secure sandbox — install via praisonai-plugins[capsule] |
Two different destination sets
Sandbox backends and compute providers are two separate sets — a name valid for one is not automatically valid for the other.
Agent(sandbox=…) sandbox backends (via SandboxManager / SandboxRegistry) run explicit execute_code() calls. They include the local-only boundaries:
| Backend | One-line description |
|---|---|
subprocess | A separate process on this machine — not a security boundary |
sandlock | A locked-down process (Landlock + seccomp-bpf) — the only real local boundary |
docker | A Docker container |
e2b | An E2B cloud sandbox |
modal | A Modal cloud sandbox — no file storage, use execute(code=...) (Modal caveats) |
daytona | A Daytona cloud sandbox |
novita | A Novita cloud sandbox |
ssh | A remote machine over SSH (pass an SSHSandbox(...) object for the host) |
run_on= / compute= providers (via the compute bridge) place a whole flow’s tools or one LocalAgent‘s tools. They are compute providers, not SandboxManager backends:
| Provider | One-line description |
|---|---|
local | This machine, in a local subprocess |
docker | A Docker container |
e2b | An E2B cloud sandbox |
modal | A Modal cloud sandbox |
daytona | A Daytona cloud sandbox |
flyio | A Fly.io machine |
tenki | A Tenki cloud sandbox |
novita | A Novita cloud sandbox (accepted by run_on= / compute= since PR #4071) |
Select by Name
CLI
# Run code in any registered backend
praisonai sandbox run --code "print('hello')" --type subprocess
praisonai sandbox run --code "print('hello')" --type e2b
praisonai sandbox run --file script.py --type modal
praisonai sandbox run --code "ls" --type docker --image python:3.12-slimPython
from praisonaiagents import Agent
# New (recommended); the old praisonai.sandbox._registry path still works as a shim
from praisonai_sandbox._registry import SandboxRegistry
agent = Agent(name="Coder", instructions="Run code safely")
registry = SandboxRegistry.default()
sandbox_cls = registry.resolve("daytona") # or docker, e2b, novita, modal, ssh, sandlock, subprocess
sandbox = sandbox_cls()Using the Novita backend
Run agent code in a Novita cloud sandbox by selecting the novita backend.
import os
from praisonaiagents import Agent, SandboxConfig
os.environ["NOVITA_API_KEY"] = "your-api-key"
agent = Agent(
name="Cloud Runner",
instructions="Run code in a Novita cloud sandbox.",
sandbox=SandboxConfig(sandbox_type="novita"),
)
agent.start("Print the Python version")Use it directly without an Agent:
from praisonai_sandbox import NovitaSandbox
sandbox = NovitaSandbox()
result = await sandbox.execute("print('Hello, World!')")
print(result.stdout) # Hello, World!praisonai sandbox run --code "import sys; print(sys.version)" --type novitaUsing the Daytona backend
Run agent code in a Daytona cloud sandbox by selecting the daytona backend.
from praisonaiagents import Agent, SandboxConfig
agent = Agent(
name="Cloud Runner",
instructions="Run code in a Daytona cloud sandbox.",
sandbox=SandboxConfig(sandbox_type="daytona"),
)
agent.start("Print the Python version")praisonai sandbox run --code "import sys; print(sys.version)" --type daytonaUsing the Novita backend
Run agent code in a Novita cloud sandbox by selecting the novita backend.
from praisonaiagents import Agent, SandboxConfig
agent = Agent(
name="Cloud Runner",
instructions="Run code in a Novita cloud sandbox.",
sandbox=SandboxConfig.novita(),
)
agent.start("Print the Python version")praisonai sandbox run --code "import sys; print(sys.version)" --type novitaThe Novita backend runs agent code end to end in Novita’s cloud.
Installing Optional Backends
Only subprocess and sandlock ship in the base install — every other backend requires an optional extra. The standalone praisonai-sandbox package is the preferred install; the legacy praisonai[...] extras still work as compatibility shims.
| Backend | Preferred install | Also works (legacy shim) |
|---|---|---|
subprocess | Built in — no extra required | — |
sandlock | Built in — no extra required | — |
docker | pip install 'praisonai-sandbox[docker]' | pip install 'praisonai[docker]' |
ssh | pip install 'praisonai-sandbox[ssh]' | pip install 'praisonai[ssh]' |
modal | pip install 'praisonai-sandbox[modal]' | pip install 'praisonai[modal]' |
daytona | pip install 'praisonai-sandbox[daytona]' (+ DAYTONA_API_KEY) | pip install 'praisonai[daytona]' |
e2b | pip install 'praisonai-sandbox[e2b]' | pip install 'praisonai[e2b]' |
novita | pip install 'praisonai-sandbox[novita]' (+ NOVITA_API_KEY) | pip install 'praisonai[novita]' |
capsule | pip install 'praisonai-plugins[capsule]' | — |
The “Preferred install” column uses the praisonai-sandbox Package — install it when you want the sandbox subsystem without the full wrapper.
Selecting an uninstalled backend exits with code 2 and prints a fix-it hint — no silent downgrade to subprocess:
$ praisonai sandbox run --code "print('hi')" --type modal
Error: sandbox 'modal' is unavailable: <reason>
Available: ['subprocess', 'sandlock']
To install the optional backend: pip install "praisonai-sandbox[modal]"
# or (legacy): pip install "praisonai[modal]"
Or explicitly choose another sandbox: --sandbox-type subprocess
$ echo $?
2For plugin-registered backends (like capsule), SandboxManager resolves the name through SandboxRegistry and raises a clearer error when the plugin is missing. When praisonai is installed but the plugin is not registered:
ValueError: Unknown sandbox type: 'capsule'. Available: ['docker', 'subprocess', 'sandlock', 'ssh', 'modal', 'daytona', 'novita', 'e2b']When praisonai itself is not installed (the registry import fails):
ValueError: Unknown sandbox type: 'capsule'. Supported built-ins: 'docker', 'subprocess', 'e2b', 'sandlock', 'ssh', 'modal', 'daytona', 'novita'. Install a plugin package that registers 'capsule' under the 'praisonai.sandbox' entry-point group (e.g. pip install praisonai-plugins[capsule]).Third-Party Sandbox Plugins
Register custom sandboxes via the praisonai.sandbox entry-point group. Discovery is now driven from praisonai_sandbox._plugin_registry; the entry-point group name is unchanged.
# pyproject.toml
[project.entry-points."praisonai.sandbox"]
my-sandbox = "my_pkg.sandbox:MySandbox"After pip install, the new name appears alongside the built-ins when you call registry.list_names().
Example: Capsule (from praisonai-plugins)
pip install "praisonai-plugins[capsule]"from praisonaiagents import Agent, SandboxConfig
agent = Agent(
name="SecureRunner",
instructions="Execute code inside the Capsule sandbox.",
sandbox=SandboxConfig.capsule(), # strict security policy applied automatically
)
agent.start("Run a Python script")The capsule backend is registered by praisonai-plugins under the praisonai.sandbox entry-point group. SandboxManager resolves the name via SandboxRegistry the first time the sandbox starts.
Using the standalone package
The built-in backends live in a dedicated package so you can use them without pulling in the full praisonai wrapper.
Agent (default)
Nothing to change — the standalone package is installed alongside praisonai:
from praisonaiagents import Agent, SandboxConfig
agent = Agent(
name="Coder",
instructions="Run code inside a container",
sandbox=SandboxConfig.docker("python:3.12-slim"),
)
agent.start("List files and print Python version")Direct import
Import a backend directly when you don’t need an Agent:
from praisonai_sandbox import DockerSandbox
from praisonaiagents.sandbox import SandboxConfig
config = SandboxConfig.docker("python:3.12-slim")
sandbox = DockerSandbox(image=config.image, config=config)CLI
The package installs a praisonai-sandbox CLI for one-off runs and scripts:
pip install praisonai-sandbox[docker]
praisonai-sandbox --helpLegacy shim
Existing code keeps working — praisonai.sandbox transparently re-exports from praisonai_sandbox:
from praisonai.sandbox import SubprocessSandbox # shim → praisonai_sandboxWhich Sandbox Should I Pick?
Configuration Options
Shell Parameter Control
shell=False (Default)
# String commands are parsed safely
result = await sandbox.run_command("python script.py --arg value")
# List commands are executed directly
result = await sandbox.run_command(["python", "script.py", "--arg", "value"])Security: No shell injection possible. String commands are parsed with shlex.split().
shell=True (Opt-in)
# Shell features available: pipes, redirects, globs
result = await sandbox.run_command(
"find . -name '*.py' | xargs grep 'TODO' > todos.txt",
shell=True
)
# Environment variable expansion
result = await sandbox.run_command(
"echo $HOME && ls $PWD/*.log",
shell=True
)Security: Shell evaluation enabled. Only use with trusted input.
Decision Guide
| Use Case | Recommended shell Value | |
|---|---|---|
| Running a single executable with arguments | False | |
| Pipelines (`grep \ | sort`) | True |
| Globs and env-var expansion | True | |
| Untrusted / model-generated commands | False |
Common Patterns
Backend Selection
Development
# New (recommended); shim: from praisonai.sandbox import SubprocessSandbox
from praisonai_sandbox import SubprocessSandbox
# Local development with subprocess
sandbox = SubprocessSandbox()
result = await sandbox.run_command("python test.py")Production
# New (recommended); shim: from praisonai.sandbox import DockerSandbox
from praisonai_sandbox import DockerSandbox
# Isolated container execution
sandbox = DockerSandbox(
image="python:3.12-slim",
timeout=30
)
result = await sandbox.run_command("python app.py", shell=False)Remote
# New (recommended); shim: from praisonai.sandbox import SSHSandbox
from praisonai_sandbox import SSHSandbox
# Remote server execution
sandbox = SSHSandbox(
host="remote.server.com",
username="runner"
)
result = await sandbox.run_command(["python", "remote_task.py"])Safe Data Processing
# New (recommended); shim: from praisonai.sandbox import DockerSandbox
from praisonai_sandbox import DockerSandbox
sandbox = DockerSandbox()
# Process user data safely
async def process_file(filename):
# Safe: no shell injection possible
result = await sandbox.run_command([
"python", "process.py", "--input", filename
], shell=False)
return result.stdout
# Process with shell features when controlled
async def count_errors(log_file):
import shlex
# Trusted input, need shell features
result = await sandbox.run_command(
f"grep 'ERROR' {shlex.quote(log_file)} | wc -l",
shell=True
)
return int(result.stdout.strip())Resource Limits
from praisonaiagents.sandbox import ResourceLimits
# New (recommended); shim: from praisonai.sandbox import SubprocessSandbox
from praisonai_sandbox import SubprocessSandbox
limits = ResourceLimits(
timeout_seconds=30,
memory_mb=512
)
sandbox = SubprocessSandbox()
result = await sandbox.run_command(
"python heavy_task.py",
limits=limits,
shell=False
)Best Practices
Always use shell=False for untrusted input
Model-generated commands or user input should never use shell=True to prevent injection attacks. The default shell=False provides automatic protection.
# ✅ Safe with any user input
user_script = request.get("script")
result = await sandbox.run_command(f"python {user_script}", shell=False)
# ❌ Vulnerable to injection
result = await sandbox.run_command(f"python {user_script}", shell=True)Quote arguments when building shell commands
If you must use shell=True, quote all dynamic arguments with shlex.quote():
import shlex
filename = user_input # Could contain special characters
command = f"process.py --file {shlex.quote(filename)}"
result = await sandbox.run_command(command, shell=True)Prefer list form for complex commands
Using argument lists avoids shell parsing entirely:
# ✅ Clear and injection-safe
result = await sandbox.run_command([
"python", "script.py",
"--input", input_file,
"--output", output_file
], shell=False)
# ❌ Requires careful quoting
import shlex
command = f"python script.py --input {shlex.quote(input_file)} --output {shlex.quote(output_file)}"
result = await sandbox.run_command(command, shell=True)Use appropriate backend for your security needs
Choose the sandbox backend based on your isolation requirements:
- Development:
SubprocessSandboxfor speed and convenience (no longer inherits host environment) - Production:
DockerSandboxfor container-level isolation - Remote:
SSHSandboxfor network-isolated execution - High Security: Always use Docker or SSH backends with
shell=False
Handle missing backends explicitly in scripts and CI
Catch exit code 2 from `praisonai sandbox run –type
and either install the extra or fall back to –sandbox-type subprocess`:
praisonai sandbox run --code "print('hello')" --type docker
if [ $? -eq 2 ]; then
echo "Docker not available, falling back to subprocess"
praisonai sandbox run --code "print('hello')" --type subprocess
fiIn CI pipelines, install the required extra before running:
pip install "praisonai-sandbox[docker]"
praisonai sandbox run --code "print('hello')" --type docker