n4nAI

How to sandbox code execution for autonomous agents

Practical guide to building a secure code sandbox for autonomous agents: container isolation, resource limits, syscall filtering, and a verified execution harness.

n4n Team4 min read807 words

Audio narration

Coming soon — every post will get a voice note here.

Running model-generated code on your infrastructure is a liability unless you isolate it properly. Sandbox code execution AI agents rely on needs to be disposable, resource-bounded, and stripped of privileges—not a bare subprocess on your laptop. Below is a concrete build of a container-based sandbox you can drop into an agent loop today.

Step 1: Pick the isolation boundary

The cheapest defensible option is a container runtime with a hardened seccomp profile. If you can take the operational hit, run it under gVisor (runsc) or a microVM (Kata, Firecracker). The threat model for sandbox code execution AI agents is not “might crash” but “will try to escape,” so treat the kernel surface as hostile.

Install gVisor on a Linux host:

curl -fsSL https://gvisor.dev/archive.key | sudo apt-key add -
echo "deb https://storage.googleapis.com/gvisor/releases release main" | sudo tee /etc/apt/sources.list.d/gvisor.list
sudo apt-get update && sudo apt-get install -y runsc
sudo runsc install
sudo systemctl restart docker

Verify with docker info | grep -i runtime. You now have a runtime that intercepts syscalls in user space, making container breakouts substantially harder than with the default runc.

Step 2: Lock down resources and network

Never give the sandbox network egress. Block it at the container level, not inside the code. Set hard memory, CPU, and PID ceilings so a fork bomb or memory leak can’t take down the node.

docker run --rm \
  --runtime=runsc \
  --network none \
  --memory 128m \
  --cpus 0.5 \
  --pids-limit 64 \
  --security-opt no-new-privileges \
  --security-opt seccomp=profile.json \
  -v "$PWD/executor.py:/executor.py:ro" \
  python:3.11-slim \
  python /executor.py

The --network none flag is non-negotiable. If your agent needs to fetch data, do it in the orchestrator and pass it in as arguments. The --pids-limit is what actually stops recursive spawn attacks; the memory cap stops greedy tensor allocations from OOM-killing the host.

Step 3: Apply a tight seccomp profile

Docker’s default profile allows too much. Write a minimal profile that permits the syscalls Python actually needs and denies the rest. Below is a stripped example; tune for your interpreter.

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": ["SCMP_ARCH_X86_64"],
  "syscalls": [
    { "names": ["read","write","close","fstat","mmap","mprotect","munmap","brk","rt_sigaction","rt_sigprocmask","sigreturn","ioctl","sched_yield","madvise","dup","dup2","getpid","clone","execve","exit_group","wait4","fcntl","gettimeofday","clock_gettime","nanosleep","pipe","poll","select"], "action": "SCMP_ACT_ALLOW" }
  ]
}

Note we deliberately omitted socket, connect, bind, and listen. The defaultAction of ERRNO fails closed: any syscall not on the list returns permission denied. If your executor needs stat or lseek, add them explicitly. Test with docker run --security-opt seccomp=profile.json and a quick python -c "import os; os.system('echo hi')"—it should fail.

Step 4: Build the in-sandbox executor

A good harness for sandbox code execution AI agents must be stateless and dead-simple. It reads code from stdin, writes to a temp file, runs it with a wall-clock timeout, and returns JSON.

import sys, subprocess, tempfile, os, json

def run(code: str, timeout: int = 10) -> dict:
    with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False) as f:
        f.write(code)
        path = f.name
    try:
        proc = subprocess.run(
            [sys.executable, path],
            capture_output=True,
            text=True,
            timeout=timeout,
            env={"PYTHONPATH": "/sandbox_libs"}
        )
        return {"ok": True, "stdout": proc.stdout, "stderr": proc.stderr, "rc": proc.returncode}
    except subprocess.TimeoutExpired:
        return {"ok": False, "error": "timeout"}
    finally:
        os.unlink(path)

if __name__ == "__main__":
    code = sys.stdin.read()
    print(json.dumps(run(code)))

This executor runs inside the container. The orchestrator passes code via stdin and reads the JSON line. Keep the timeout aggressive; a well-behaved agent task rarely needs more than 10 seconds of raw compute.

Step 5: Enforce a library and import policy

Even inside a container, you don’t want the model importing os or subprocess. Parse the AST before execution and reject banned nodes. This is a policy layer, not a security boundary—defense in depth.

import ast, sys

BANNED = {"import": {"os","subprocess","socket","shutil","pathlib"}, "from": {"os","subprocess","socket"}}

def check(code: str):
    tree = ast.parse(code)
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for n in node.names:
                if n.name.split(".")[0] in BANNED["import"]:
                    raise ValueError(f"import {n.name} blocked")
        elif isinstance(node, ast.ImportFrom):
            if node.module and node.module.split(".")[0] in BANNED["from"]:
                raise ValueError(f"from {node.module} blocked")

if __name__ == "__main__":
    src = sys.stdin.read()
    try:
        check(src)
        print("POLICY_OK")
    except ValueError as e:
        print("POLICY_FAIL", e)

Run this check in the orchestrator before shipping code to the container. It cuts down on noisy escape attempts and makes logs readable.

Step 6: Expose a local execution API

Wrap the container in a tiny FastAPI service so your agent talks HTTP, not Docker sockets. Use the Docker SDK to spawn a fresh container per request and tear it down.

from fastapi import FastAPI
import docker

app = FastAPI()
client = docker.from_env()

@app.post("/run")
def run_code(payload: dict):
    code = payload["code"]
    # policy check omitted for brevity
    container = client.containers.run(
        "python:3.11-slim",
        command="python /executor.py",
        runtime="runsc",
        network_mode="none",
        mem_limit="128m",
        cpu_quota=50000,
        pids_limit=64,
        security_opt=["no-new-privileges","seccomp=profile.json"],
        volumes={"/host/executor.py": {"bind": "/executor.py", "mode": "ro"}},
        detach=False,
        remove=True,
        stdin_open=True
    )
    # In real impl, stream code via stdin using container.exec_run
    return {"status": "ran"}

In production, pre-bake the executor and code into an ephemeral image or use docker exec with stdin. The key is one container per execution, zero reuse. Never leave a container warm for the next request—state bleed is how agents exfiltrate prior context.

Step 7: Connect your agent and handle failures

Your agent loop should treat the sandbox as a flaky external service. Timeouts, non-zero exits, and missing outputs are expected. If your agent calls LLMs through n4n.ai, its OpenAI-compatible endpoint with automatic fallback keeps the loop alive when a provider is degraded, while the sandbox contains the code side.

import requests

def execute_via_sandbox(code: str) -> dict:
    resp = requests.post("http://sandbox.local/run", json={"code": code}, timeout=15)
    if resp.status_code != 200:
        return {"ok": False, "error": "sandbox unavailable"}
    return resp.json()

# Agent step
result = execute_via_sandbox(model_generated_code)
if not result.get("ok"):
    prompt_repair = f"Code failed: {result.get('stderr')}. Fix it."
    # hand back to LLM...

For sandbox code execution AI agents, always cap the retry loop. Three failed executions means the model is flailing; bail out and return a structured error to the caller.

Step 8: Verify the sandbox holds

Verification is not optional. Run three probes after deployment:

  1. File escape: open("/etc/shadow").read() → should raise PermissionError or be blocked by mount namespace.
  2. Fork bomb: import os; [os.fork() for _ in range(100)] → should hit pids-limit and be killed.
  3. Network call: import socket; socket.create_connection(("1.1.1.1",53), timeout=2) → should fail with Network is unreachable.

If any probe succeeds, your profile is wrong. Automate these as a CI job against a test container. Successful verification means the agent can execute arbitrary generated Python, get stdout, and the host remains untouched. That is the bar for shipping sandbox code execution AI agents to production.

Operational notes

  • Log every execution ID, model ID, and exit code. You’ll need it when a prompt injection tries to exfiltrate data.
  • Rotate the executor image weekly; pin digests, not tags.
  • Never mount your source repo or cloud creds into the sandbox. Pass data explicitly as serialized arguments.
  • Consider WebAssembly (Wasmtime, Wasmer) if your workloads are pure number-crunching; it’s a smaller surface than Linux containers and starts in milliseconds.
  • Monitor container churn. If your agent is spawning 50 sandboxes per user request, fix the agent prompt before scaling the cluster.

Build the sandbox first, then let the agent loose. Not the other way around.

Tagssandboxingcode-executionautonomous-agentssecurity

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All sandboxing & guardrails for autonomous agents posts →