Containerized Sandboxes for Code-Executing Agent Tools
When an agent can run model-written code, that code must not share the host's filesystem, network, or credentials.
Search across all documentation pages
When an agent can run model-written code, that code must not share the host's filesystem, network, or credentials.
Containers (or stronger: microVMs) give you a practical isolation boundary you can automate on each tool call.
Run untrusted agent code in a disposable container with a read-only root filesystem, no host network (or tightly allowlisted egress), dropped capabilities, resource limits, and a single mounted work directory that holds only the job inputs.
The process that talks to the model holds API keys. The process that runs user/model code must not.
Use a slim base, non-root user, no compilers or cloud CLIs unless required, and pin digests in production (verify tags at build).
Start with --network none. Add egress only for documented package or API needs, never "whatever the snippet imports."
Bind-mount a per-job temp directory as the working dir. Do not mount $HOME, Docker socket, cloud metadata paths, or secret volumes.
Non-root user, read-only root FS, drop NET_RAW and other caps, no privilege escalation.
cgroup limits plus a hard kill on timeout stop fork bombs and runaway loops.
Truncate output before it re-enters the model context. Redact secret-shaped strings if inputs could contain them.
Ephemeral containers prevent persistence across tenants and turns.
This pattern shells out to Docker from a tool handler. Swap Podman or a job API as needed; the security shape stays the same.
import subprocess
import tempfile
import textwrap
from pathlib import Path
def run_in_container(source: str, *, timeout_s: int = 5) -> dict:
with tempfile.TemporaryDirectory() as td:
work = Path(td)
(work / "main.py").write_text(textwrap.dedent(source), encoding="utf-8")
cmd = [
"docker", "run", "--rm",
"--network", "none",
"--read-only",
"--tmpfs", "/tmp:rw,noexec,nosuid,size=16m",
"--user", "65534:65534",
"--cap-drop", "ALL",
"--security-opt", "no-new-privileges",
"--memory", "256m",
"--cpus", "0.5",
"--pids-limit", "64",
"-v", f"{work}:/work:ro",
"-w", "/work",
"python:3.12-slim", # pin digest in prod; verify at build
"python", "/work/main.py",
]
try:
proc = subprocess.run(
cmd, capture_output=True, text=True, timeout=timeout_s + 2
)
except subprocess.TimeoutExpired:
return {"ok": False, "error": "timeout"}
out = (proc.stdout or "")[:8_000]
err = (proc.stderr or "")[:2_000]
return {
"ok": proc.returncode == 0,
"stdout": out,
"stderr": err,
"code": proc.returncode,
}
print(run_in_container("print(sum(range(10)))"))Wire run_in_container behind your run_python_sandboxed tool; the model only sees the JSON observation, not Docker.
execIn-process restricted Python is easy to demo and hard to seal. C extensions, bytecode tricks, and host library bugs share an address space with your secrets.
A container moves the trust boundary to the OS and runtime. Escape risk is not zero, but it is a different class of problem with standard hardening playbooks.
| Choice | Why |
|---|---|
| Slim / distroless-style | Fewer packages to abuse |
| Non-root | Limits host mapping damage |
| No Docker CLI inside | Avoid nested breakout helpers |
| Pinned digest | Reproducible, less supply surprise |
| Preinstalled deps only | Runtime pip install needs network and is risky |
If snippets need third-party libraries, prebake them into the image or use a reviewed dependency allowlist with offline wheels.
--network none is the correct default for pure compute (stats, transforms, puzzles).
If code must call an internal API, prefer:
Never pass the model's OpenRouter/OpenAI key into the sandbox "so the code can call the LLM."
For multi-tenant SaaS code execution, many teams add a stronger runtime (gVisor, Kata Containers, Firecracker microVMs). Containers alone may be enough for trusted-employee agents; hostile multi-tenant usually needs more.
Log: job id, image digest, timeout, exit code, bytes of stdout, policy version. Do not log full customer source if policy forbids it; hash or sample instead.
LangGraph, CrewAI, OpenAI Agents SDK, and custom hosts should treat container exec as an ordinary tool with a schema. Isolation is not a framework feature you "turn on"; it is infrastructure you own.
--network host - Defeats isolation for convenience. Avoid.python:slim is safe forever - Rebuild and patch on a schedule; verify base image policy at build.| Approach | Strength | Weakness | Best fit |
|---|---|---|---|
| In-process restricted exec | Simple | Weak isolation | Local demos only |
| Docker/Podman per job | Practical default | Escape surface remains | Most product agents |
| gVisor / Kata / microVM | Stronger isolation | Ops complexity | Multi-tenant SaaS |
| Remote serverless sandbox products | Managed lifecycle | Vendor limits, data path | Teams without container ops |
| No code exec (structured tools only) | Smallest risk | Less flexible | Many business agents |
No. Any isolator with resource limits, filesystem jail, and network policy works. Docker is a common lowest common denominator.
Prefer prebaked images. Ad hoc installs need network and can pull malware. If you allow installs, pin indexes, allowlist packages, and still drop credentials.
Seconds for typical snippets. Hard-kill above your product SLA. Longer jobs need queues and clearer abuse controls.
A dedicated non-root UID with no sudo and no writable sensitive mounts.
Write to a size-capped output directory, copy out from the host after exit, virus-scan if untrusted, then summarize or attach metadata - not raw host paths.
Sometimes. Provide /tmp tmpfs and document that only /work (or similar) is available.
Yes for the executor path. Negative tests (network, file escape, timeout) belong in CI.
The container is how you enforce least privilege for the code tool's OS powers. Tool schema still limits when the model may call it.
Same ideas: dedicated container/VM, separate profile, no host cookie jar, limited egress. Browser tooling is high risk and needs its own threat model.
Yes if you reset state thoroughly (new filesystem layer, cleared env, no residual processes) and never mix tenants in a dirty pool.
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.
Reviewed by Chris St. John·Last updated Jul 16, 2026