Sandboxing Agent Tool Execution: Running Untrusted Code and Commands Safely
Learn how to sandbox AI agent tool execution using Docker containers, restricted file systems, timeout enforcement, and resource limits to prevent agents from causing damage through code execution tools.
The Danger of Unrestricted Tool Execution
AI agents that can execute code, run shell commands, or interact with file systems have enormous utility — and enormous risk. A coding assistant that runs user-submitted Python code could execute os.system("rm -rf /"). An agent with shell access might be tricked via prompt injection into exfiltrating environment variables containing API keys. Without sandboxing, every tool call is a potential security breach.
Sandboxing isolates tool execution in a controlled environment where damage is limited, resources are bounded, and sensitive systems are unreachable. This post implements a complete sandboxing system for AI agent tools using Docker and Python.
Sandbox Architecture
A production sandbox has four isolation layers: process isolation, filesystem isolation, network isolation, and resource limits:
from dataclasses import dataclass
from typing import Optional
from enum import Enum
class SandboxStatus(Enum):
SUCCESS = "success"
TIMEOUT = "timeout"
ERROR = "error"
RESOURCE_EXCEEDED = "resource_exceeded"
@dataclass
class SandboxConfig:
max_execution_seconds: int = 30
max_memory_mb: int = 256
max_cpu_percent: float = 50.0
max_output_bytes: int = 1_000_000 # 1 MB
network_enabled: bool = False
writable_paths: list[str] | None = None
allowed_commands: list[str] | None = None
@dataclass
class SandboxResult:
status: SandboxStatus
stdout: str
stderr: str
exit_code: int
execution_time_ms: int
memory_used_mb: float
Docker-Based Code Execution Sandbox
Docker provides the strongest isolation for code execution. Each tool call runs in a fresh, ephemeral container:
import docker
import tempfile
import time
import os
class DockerSandbox:
def __init__(self, config: SandboxConfig):
self.config = config
self.client = docker.from_env()
self.image = "python:3.12-slim"
def execute_python(self, code: str) -> SandboxResult:
"""Run Python code in an isolated Docker container."""
start_time = time.time()
with tempfile.TemporaryDirectory() as tmpdir:
script_path = os.path.join(tmpdir, "script.py")
with open(script_path, "w") as f:
f.write(code)
try:
container = self.client.containers.run(
image=self.image,
command=["python", "/sandbox/script.py"],
volumes={
tmpdir: {"bind": "/sandbox", "mode": "ro"},
},
mem_limit=f"{self.config.max_memory_mb}m",
cpu_period=100000,
cpu_quota=int(100000 * self.config.max_cpu_percent / 100),
network_disabled=not self.config.network_enabled,
read_only=True,
tmpfs={"/tmp": "size=64m"},
security_opt=["no-new-privileges:true"],
user="nobody",
detach=True,
)
exit_info = container.wait(
timeout=self.config.max_execution_seconds
)
stdout = container.logs(stdout=True, stderr=False).decode()
stderr = container.logs(stdout=False, stderr=True).decode()
return SandboxResult(
status=SandboxStatus.SUCCESS,
stdout=stdout[:self.config.max_output_bytes],
stderr=stderr[:self.config.max_output_bytes],
exit_code=exit_info["StatusCode"],
execution_time_ms=int((time.time() - start_time) * 1000),
memory_used_mb=0,
)
except docker.errors.ContainerError as e:
return SandboxResult(
status=SandboxStatus.ERROR,
stdout="",
stderr=str(e),
exit_code=1,
execution_time_ms=int((time.time() - start_time) * 1000),
memory_used_mb=0,
)
finally:
try:
container.remove(force=True)
except Exception:
pass
Command Allowlisting
For agents that run shell commands, allowlisting prevents execution of dangerous operations:
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
import shlex
class CommandAllowlist:
"""Restrict which shell commands the agent can execute."""
SAFE_COMMANDS = {
"ls", "cat", "head", "tail", "wc", "grep", "find",
"sort", "uniq", "cut", "awk", "jq", "echo", "date",
"python", "node", "curl",
}
BLOCKED_PATTERNS = [
"rm -rf", "mkfs", "dd if=", "chmod 777",
"> /dev/", "| sh", "| bash", "eval ",
"export ", "env ", "printenv", "set ",
"sudo ", "su ", "passwd",
]
def validate_command(self, command: str) -> tuple[bool, str]:
"""Return (is_allowed, reason)."""
normalized = command.strip().lower()
for pattern in self.BLOCKED_PATTERNS:
if pattern in normalized:
return False, f"Blocked pattern detected: {pattern}"
try:
parts = shlex.split(command)
except ValueError as e:
return False, f"Could not parse command: {e}"
base_command = os.path.basename(parts[0]) if parts else ""
if base_command not in self.SAFE_COMMANDS:
return False, f"Command '{base_command}' is not in allowlist"
return True, "Command approved"
def execute_safe(self, command: str, sandbox: "DockerSandbox") -> SandboxResult:
allowed, reason = self.validate_command(command)
if not allowed:
return SandboxResult(
status=SandboxStatus.ERROR,
stdout="",
stderr=f"Command blocked: {reason}",
exit_code=1,
execution_time_ms=0,
memory_used_mb=0,
)
return sandbox.execute_command(command)
Timeout Enforcement
Timeout handling ensures runaway processes cannot consume resources indefinitely:
import signal
import subprocess
from contextlib import contextmanager
class TimeoutEnforcer:
"""Enforce execution timeouts at the process level."""
@staticmethod
@contextmanager
def timeout(seconds: int):
def handler(signum, frame):
raise TimeoutError(f"Execution exceeded {seconds}s limit")
old_handler = signal.signal(signal.SIGALRM, handler)
signal.alarm(seconds)
try:
yield
finally:
signal.alarm(0)
signal.signal(signal.SIGALRM, old_handler)
@staticmethod
def run_with_timeout(
command: list[str],
timeout_seconds: int,
cwd: str | None = None,
) -> SandboxResult:
start = time.time()
try:
proc = subprocess.run(
command,
capture_output=True,
text=True,
timeout=timeout_seconds,
cwd=cwd,
)
return SandboxResult(
status=SandboxStatus.SUCCESS,
stdout=proc.stdout,
stderr=proc.stderr,
exit_code=proc.returncode,
execution_time_ms=int((time.time() - start) * 1000),
memory_used_mb=0,
)
except subprocess.TimeoutExpired:
return SandboxResult(
status=SandboxStatus.TIMEOUT,
stdout="",
stderr=f"Process killed after {timeout_seconds}s",
exit_code=-1,
execution_time_ms=timeout_seconds * 1000,
memory_used_mb=0,
)
Integrating Sandboxing with an Agent Framework
from agents import Agent, function_tool
sandbox = DockerSandbox(SandboxConfig(
max_execution_seconds=30,
max_memory_mb=256,
network_enabled=False,
))
allowlist = CommandAllowlist()
@function_tool
def execute_code(code: str) -> str:
"""Run Python code in a sandboxed environment."""
result = sandbox.execute_python(code)
if result.status == SandboxStatus.TIMEOUT:
return "Error: Code execution timed out after 30 seconds."
if result.status == SandboxStatus.ERROR:
return f"Error: {result.stderr}"
return result.stdout or "(no output)"
agent = Agent(
name="Coding Assistant",
instructions="You help users write and test Python code. Use execute_code to run code.",
tools=[execute_code],
)
FAQ
Is Docker isolation sufficient for production use?
Docker provides strong isolation for most use cases, but it is not a security boundary in the same way as a virtual machine. For highest-security environments (running code from untrusted external users), consider gVisor or Firecracker microVMs which provide an additional kernel-level isolation layer. For internal tools and controlled environments, Docker with the security flags shown above (read-only root, no-new-privileges, non-root user, network disabled) is robust.
How do I handle agents that need network access for tool execution?
Grant network access selectively using Docker network policies. Create an isolated Docker network that only allows connections to specific hosts and ports. For example, if the agent needs to call a specific API, create a network rule that permits only that destination. Never allow unrestricted outbound network access from sandboxed containers.
What about file system access for tools that need to read or write files?
Mount only the specific directories needed as Docker volumes, and use read-only mounts whenever possible. For tools that need to write files, mount a temporary directory and copy out only the expected output files after execution. Never mount host directories containing sensitive data, credentials, or configuration files into the sandbox.
#Sandboxing #Docker #AISafety #ToolExecution #Security #AgenticAI #LearnAI #AIEngineering
CallSphere Team
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.