Agent Safety › Sandboxing
Language AI HandbookAll chapters Agent Safety › SandboxingOne section · Sections in this chapter4 / 12 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Constraints on the action space limit what the agent is allowed to do. Sandboxing creates a technical barrier that limits what the agent is capable of doing, independent of what it intends to do. A sandboxed agent physically cannot access resources outside its sandbox, even if its code or the model driving it attempts to do so.
The distinction between "allowed" and "capable" is important. Action constraints are policy: they represent what the system agrees to do. Sandboxing is mechanism: it represents what the system can physically do. Policy enforcement can be circumvented if there is a bug in the enforcement code, or if the agent finds a way to invoke a tool that bypasses the policy layer. Mechanism-level isolation, enforced by the operating system or hardware, is much harder to circumvent.
What Sandboxing Provides
A proper sandbox enforces isolation at the operating system or container level. The agent's code execution environment cannot access:
- The host file system (only a limited virtual filesystem within the sandbox)
- Network addresses outside a whitelist
- Environment variables containing secrets (API keys, credentials)
- Other processes on the host machine
- Resources beyond the allocated CPU and memory budget
When an agent runs arbitrary code, which is one of the most powerful and dangerous capabilities an agent can have, sandboxing is essential. Without it, a single malicious prompt injection could instruct the agent to execute code that exfiltrates API keys, establishes persistence on the host system, or damages or destroys files. With a properly configured sandbox, the code runs in an isolated environment that is destroyed after execution, taking any damage with it.
The threat model for sandboxing is not just prompt injection from malicious users. It also covers bugs in the model's reasoning that lead to unintended code behavior, supply chain attacks on libraries the agent imports, and cases where an agent legitimately tries to accomplish a task but misunderstands the scope of what it is allowed to do. Sandboxing provides a hard boundary that no amount of reasoning can cross.
Container-Based Sandboxing
The most widely used sandboxing approach for agent code execution uses containerization. Docker containers provide file system isolation, network namespacing, and resource limits through Linux cgroups and namespaces. Agents execute code inside ephemeral containers that are created for a single execution and then destroyed.
The key principle of ephemeral containers is that each execution starts from a known, clean state. There is no persistent state that a previous execution could have compromised. Even if code in one execution manages to do something unexpected, that state is destroyed when the container exits. The next execution starts fresh.
import os
import subprocess
import tempfile
from typing import Tuple
class SandboxedExecutor:
"""
Executes Python code in an isolated Docker container.
Container is ephemeral: created per execution and immediately removed.
"""
def __init__(
self,
image: str = "python:3.11-slim",
timeout_seconds: int = 30,
memory_limit: str = "256m",
cpu_quota: int = 50000, # 50% of one CPU core
):
self.image = image
self.timeout_seconds = timeout_seconds
self.memory_limit = memory_limit
self.cpu_quota = cpu_quota
def execute(self, code: str) -> Tuple[str, str, int]:
"""
Returns (stdout, stderr, exit_code).
Raises TimeoutError if execution exceeds timeout.
"""
with tempfile.NamedTemporaryFile(
mode="w", suffix=".py", delete=False
) as f:
f.write(code)
script_path = f.name
try:
result = subprocess.run(
[
"docker",
"run",
"--rm",
"--network",
"none", # No network access
"--memory",
self.memory_limit,
"--cpu-quota",
str(self.cpu_quota),
"--read-only", # Read-only root filesystem
"--tmpfs",
"/tmp:size=50m", # Writable temp space
"--security-opt",
"no-new-privileges",
"-v",
f"{script_path}:/code/script.py:ro",
self.image,
"python",
"/code/script.py",
],
capture_output=True,
text=True,
timeout=self.timeout_seconds,
)
return result.stdout, result.stderr, result.returncode
except subprocess.TimeoutExpired:
raise TimeoutError(
f"Code execution exceeded {self.timeout_seconds}s limit"
)
finally:
os.unlink(script_path)SandboxedExecutor configuration: image: python:3.11-slim timeout_seconds: 30 memory_limit: 256m network: none (isolated) filesystem: read-only root + 50MB tmpfs Security properties: - No network access (--network none) - Read-only root filesystem - Memory bounded at 256MB - CPU bounded at 50% of one core - No privilege escalation (no-new-privileges) - Container destroyed after each execution
Each flag in the Docker invocation addresses a specific attack vector. --network none prevents network exfiltration and command-and-control connections. --read-only prevents the container from modifying the image filesystem, so even if the container is compromised it cannot persist changes. --memory and --cpu-quota prevent denial-of-service through resource exhaustion. --security-opt no-new-privileges prevents privilege escalation through setuid binaries. Together, these flags enforce a narrow execution environment where the code can compute but cannot cause lasting harm.
The gVisor and WebAssembly Alternatives
Docker containers share the host kernel, which means a kernel vulnerability could allow container escape. For higher-security deployments, several alternatives provide stronger isolation guarantees.
gVisor, developed by Google, provides a user-space kernel that intercepts system calls before they reach the host kernel. When code inside a gVisor sandbox calls open() or write(), those calls are handled by gVisor's user-space kernel, which validates them and then makes its own (different) calls to the host kernel. A kernel vulnerability in the host kernel is only reachable through gVisor's intermediary, adding a significant additional isolation boundary. The tradeoff is performance: gVisor adds latency of roughly 2x for system-call-heavy workloads because every system call passes through an additional interception layer.
WebAssembly (WASM) sandboxes go further still. This provides near-native execution speed in a capability-based security model. A WASM module has no access to any resources except those explicitly granted to it by the host. There are no ambient permissions, no implicit file system access, no network access by default. Systems like Wasmtime implement this with formal security guarantees rather than the heuristic-based security of container isolation. The catch is compatibility: most Python packages cannot be compiled to WASM targets, which limits what agents can run inside a WASM sandbox.
The right choice depends on your threat model and the types of code the agent executes. For most agent deployments running trusted Python packages, Docker containers with strict flags provide excellent security at low overhead. For agents that execute untrusted code, gVisor or similar kernel-intercept approaches provide better guarantees. For agents that can be redesigned to run in a WASM-compatible environment, the capability model provides the strongest guarantees.
Resource Limits and Timeout Enforcement
Beyond isolation, sandboxes must enforce resource limits that prevent a misbehaving agent from consuming unbounded compute. A common class of mistakes is an agent that gets stuck in an infinite loop, or that intentionally (through a compromised prompt) computes something expensive to exhaust the host's resources.
Timeouts are the most important resource limit. An agent action that takes more than a few minutes is almost certainly not working correctly, and a hard timeout prevents the system from hanging indefinitely. Memory limits prevent a memory-exhausting attack from affecting the host system. CPU quotas ensure that a single runaway agent cannot starve other processes.
The specific limits should be calibrated to the expected workload. A data analysis task might legitimately need 60 seconds and 1 GB of memory. A function that searches a document needs far less. Setting limits too tight causes legitimate tasks to fail; setting them too loose provides weak protection. Observing the distribution of actual resource consumption in development and then setting production limits to some multiple (say 3x) of the observed 99th percentile is a practical approach.