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:
- File escape:
open("/etc/shadow").read()→ should raisePermissionErroror be blocked by mount namespace. - Fork bomb:
import os; [os.fork() for _ in range(100)]→ should hitpids-limitand be killed. - Network call:
import socket; socket.create_connection(("1.1.1.1",53), timeout=2)→ should fail withNetwork 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.