Skip to content
Learn Agentic AI13 min read0 views

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

Share this article
C

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.