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.

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

Diagram

Sandbox Command Execution

False

True

📝 Command

🔍 shell=?

🛡️ Safe Parse

⚠️ Shell Features

✅ Execute

Quick Start

  1. 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")
  2. With Configuration

    Pick a specific backend via SandboxConfig or the CLI --sandbox-type flag:

    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

Diagram
ProcessShellSandboxAgentProcessShellSandboxAgentalt[shell=False (Safe)][shell=True(Features)]run_command(cmd, shell=False)shlex.split(cmd)exec(*argv)sh -c “cmd”shell executionResultSandboxResult
BackendUse CaseSecurity Level
SubprocessSandboxLocal development, scriptsMedium (OS-level isolation, POSIX only)
DockerSandboxProduction, untrusted codeHigh (container isolation)
SSHSandboxRemote executionHigh (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.txt inside 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.
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.run_command("echo hello > f.txt")
    r = await sandbox.run_command("cat f.txt")
    print(r.stdout)   # "hello"
    await sandbox.stop()

asyncio.run(demo())
PropertyValue
Workspace inside container/sandbox
Default working directory/sandbox
Persistence across commandsYes, via bind mount
Persistence across sandboxesNo (_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.

LabelApplied byContainer lifetimeListed by praisonai managed ps
praisonai=managedDockerCompute (managed instance for run_on= / compute=)Long-lived; reclaimed by managed stop, the finalizer, or ashutdown()✅
praisonai=sandbox-execDockerSandbox.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:

bash
docker ps --filter label=praisonai=sandbox-exec

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

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

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"

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.

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

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

ClauseEnforced by DockerSandbox?
blocked_commandsYes — on run_command() before dispatch
allowed_commandsYes — on run_command() (has no container analogue)
blocked_pathsYes — realpath + expanduser, on run_command()
allowed_pathsYes — realpath + expanduser, on run_command()
max_output_sizeYes — on both run_command() and execute() output buffers
allow_subprocessNo — 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.

Diagram

Yes, fast dev

Yes, hardened

Container isolation

Remote server

Cloud sandbox

Novita

Where does code run?

Run locally?

subprocess
default, zero setup

native / sandlock
OS-level isolation

docker
production, untrusted code

ssh
network isolation

modal / e2b / daytona / novita
managed cloud runtime

novita


All Built-in Sandbox Backends

PR #2003 exposes all built-in sandboxes through SandboxRegistry — selectable by string name from the CLI or Python.

NameClassTypical use
dockerDockerSandboxContainer isolation for production
subprocessSubprocessSandboxFast local development (default)
sandlockSandlockSandboxHardened local sandbox
sshSSHSandboxRemote server execution
modalModalSandboxModal cloud sandboxes — no file storage (Modal caveats)
daytonaDaytonaSandboxDaytona cloud sandboxes (requires the daytona extra + DAYTONA_API_KEY)
e2bE2BSandboxE2B cloud code interpreter
novitaNovitaSandboxNovita cloud sandboxes (requires the novita extra + NOVITA_API_KEY)

Plugin-registered backends (installed separately, resolved via the praisonai.sandbox entry-point group):

NameSourceTypical use
capsulePlugin (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:

BackendOne-line description
subprocessA separate process on this machine — not a security boundary
sandlockA locked-down process (Landlock + seccomp-bpf) — the only real local boundary
dockerA Docker container
e2bAn E2B cloud sandbox
modalA Modal cloud sandbox — no file storage, use execute(code=...) (Modal caveats)
daytonaA Daytona cloud sandbox
novitaA Novita cloud sandbox
sshA 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:

ProviderOne-line description
localThis machine, in a local subprocess
dockerA Docker container
e2bAn E2B cloud sandbox
modalA Modal cloud sandbox
daytonaA Daytona cloud sandbox
flyioA Fly.io machine
tenkiA Tenki cloud sandbox
novitaA Novita cloud sandbox (accepted by run_on= / compute= since PR #4071)

Select by Name

CLI

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

Python

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

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

python
from praisonai_sandbox import NovitaSandbox

sandbox = NovitaSandbox()
result = await sandbox.execute("print('Hello, World!')")
print(result.stdout)  # Hello, World!
bash
praisonai sandbox run --code "import sys; print(sys.version)" --type novita

Using the Daytona backend

Run agent code in a Daytona cloud sandbox by selecting the daytona backend.

python
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")
bash
praisonai sandbox run --code "import sys; print(sys.version)" --type daytona

Using the Novita backend

Run agent code in a Novita cloud sandbox by selecting the novita backend.

python
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")
bash
praisonai sandbox run --code "import sys; print(sys.version)" --type novita

The Novita backend runs agent code end to end in Novita’s cloud.

Diagram
Novita CloudNovita SandboxAgentUserNovita CloudNovita SandboxAgentUser“Print the Python version”execute(code, language=”python”)AsyncSandbox.create(timeout=…)sandbox handlecommands.run(“python3 -c …”)stdout / exit_codeSandboxResult(status=COMPLETED, stdout=…)“Python 3.11.9”

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.

BackendPreferred installAlso works (legacy shim)
subprocessBuilt in — no extra required—
sandlockBuilt in — no extra required—
dockerpip install 'praisonai-sandbox[docker]'pip install 'praisonai[docker]'
sshpip install 'praisonai-sandbox[ssh]'pip install 'praisonai[ssh]'
modalpip install 'praisonai-sandbox[modal]'pip install 'praisonai[modal]'
daytonapip install 'praisonai-sandbox[daytona]' (+ DAYTONA_API_KEY)pip install 'praisonai[daytona]'
e2bpip install 'praisonai-sandbox[e2b]'pip install 'praisonai[e2b]'
novitapip install 'praisonai-sandbox[novita]' (+ NOVITA_API_KEY)pip install 'praisonai[novita]'
capsulepip 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:

bash
$ 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 $?
2
Diagram

installed

missing

–sandbox-type X

Registry

✅ Run

❌ Error + hint\nexit code 2

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

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

bash
pip install "praisonai-plugins[capsule]"
python
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:

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

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

bash
pip install praisonai-sandbox[docker]
praisonai-sandbox --help

Legacy shim

Existing code keeps working — praisonai.sandbox transparently re-exports from praisonai_sandbox:

python
from praisonai.sandbox import SubprocessSandbox   # shim → praisonai_sandbox

Which Sandbox Should I Pick?

Diagram

Local machine

Remote server

Cloud provider

No, fast dev

Yes

Modal

Daytona

E2B

Novita

Need sandbox execution?

Where should code run?

Need container isolation?

ssh

Which platform?

subprocess or sandlock

docker

modal

daytona

e2b

novita


Configuration Options

Shell Parameter Control

shell=False (Default)

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

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

Diagram

No

Yes

Yes

No

Need to run command?

Need pipes/globs?

Use shell=False

Input trusted?

Use shell=True

Sanitize OR use shell=False

Use CaseRecommended shell Value
Running a single executable with argumentsFalse
Pipelines (`grep \sort`)True
Globs and env-var expansionTrue
Untrusted / model-generated commandsFalse

Common Patterns

Backend Selection

Development

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

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

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

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

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

python
# ✅ 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():

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

python
# ✅ 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: SubprocessSandbox for speed and convenience (no longer inherits host environment)
  • Production: DockerSandbox for container-level isolation
  • Remote: SSHSandbox for 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`:

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

In CI pipelines, install the required extra before running:

bash
pip install "praisonai-sandbox[docker]"
praisonai sandbox run --code "print('hello')" --type docker

Related