Agent Security Basics
8 examples to get you started with agent security - 5 basic and 3 intermediate.
You will define a code-execution tool that runs in a hard sandbox: no network, no host filesystem, strict CPU/time limits, and a tiny safe builtins set.
Prerequisites
- Python 3.11+ recommended
- Comfort with functions, exceptions, and JSON tool results
- Optional later: Docker for real container isolation (covered in the next guide)
python -m venv .venv && source .venv/bin/activate
pip install pydanticBasic Examples
1. State the Threat You Are Blocking
Write the threat model before you write the sandbox.
THREATS = [
"read host secrets via open('/etc/passwd') or Path.home()",
"exfiltrate data with urllib or socket",
"fork-bomb or infinite loop",
"import os and call system()",
]
print("sandbox must block:", *THREATS, sep="\n- ")- Untrusted code is any program the model generates or the user pastes.
- The host process is powerful; the tool must not inherit that power.
- Listing threats keeps later allowlists honest.
Related: The Principle of Least Privilege Applied to Agent Tools
2. Define a Narrow Code-Exec Tool Schema
Expose one job: evaluate a pure expression or short snippet, not "run anything on the server."
from pydantic import BaseModel, Field
class RunPythonArgs(BaseModel):
source: str = Field(
description="Short pure Python to compute a result. No I/O, no imports."
)
tools = [{
"type": "function",
"function": {
"name": "run_python_sandboxed",
"description": "Run a short pure Python snippet and return stdout/result. No network or files.",
"parameters": RunPythonArgs.model_json_schema(),
},
}]- Description steers the model away from asking for shell or HTTP.
- One tool is enough for the learning path.
- Schema validation is the first gate, not the last.
3. Build a Minimal In-Process Sandbox
Start with restricted exec so you understand the control points. Production should prefer containers (next article).
import ast
from typing import Any
ALLOWED_BUILTINS = {
"abs": abs,
"min": min,
"max": max,
"sum": sum,
"len": len,
"range": range,
"enumerate": enumerate,
"sorted": sorted,
"round": round,
"float": float,
"int": int,
"str": str,
"bool": bool,
"list": list,
"dict": dict,
"tuple": tuple,
"print": print,
}
def run_sandboxed(source: str, *, timeout_s: float = 1.0) -> dict[str, Any]:
# Timeout needs threads/processes in real systems; kept simple here.
tree = ast.parse(source, mode="exec")
for node in ast.walk(tree):
if isinstance(node, (ast.Import, ast.ImportFrom)):
raise PermissionError("imports are not allowed")
if isinstance(node, ast.Attribute) and isinstance(node.attr, str):
if node.attr.startswith("_"):
raise PermissionError("private attributes are not allowed")
stdout: list[str] = []
def _print(*args, **kwargs):
stdout.append(" ".join(str(a) for a in args))
glb = {"__builtins__": {**ALLOWED_BUILTINS, "print": _print}}
loc: dict[str, Any] = {}
exec(compile(tree, "<agent>", "exec"), glb, loc) # noqa: S102 - intentional sandbox demo
result = loc.get("result", stdout[-1] if stdout else None)
return {"ok": True, "result": result, "stdout": stdout}- Ban imports and dunder poking early.
- Capture
printso the agent sees output without real console side effects. - In-process sandboxes are teaching tools; escape hatches exist - use containers for untrusted prod code.
4. Wrap Execution as an Agent Tool Handler
Validate args, catch failures, return structured observations (never raw tracebacks with host paths if you can avoid it).
import json
def handle_run_python_sandboxed(arguments_json: str) -> str:
try:
args = RunPythonArgs.model_validate_json(arguments_json)
if len(args.source) > 4_000:
raise ValueError("source too long")
out = run_sandboxed(args.source)
return json.dumps(out)
except Exception as e:
return json.dumps({"ok": False, "error": type(e).__name__, "message": str(e)[:200]})
print(handle_run_python_sandboxed('{"source": "result = sum(range(10))"}'))
print(handle_run_python_sandboxed('{"source": "import os"}'))- Errors become data for the model, not host crashes.
- Length limits reduce resource abuse.
ok: falseteaches the agent to recover without elevating privilege.
5. Prove Network and Filesystem Are Unavailable
Add deliberate negative tests to your suite.
cases = [
"result = open('secret.txt').read()",
"import socket",
"result = __import__('os').system('id')",
]
for src in cases:
obs = handle_run_python_sandboxed(json.dumps({"source": src}))
assert json.loads(obs)["ok"] is False, src
print("negative tests passed")- Ship tests with the tool, not only happy-path math.
- Re-run after every sandbox change.
- Treat a failed negative test as a release blocker.
Intermediate Examples
6. Enforce CPU Budget with a Worker Process
Move code out of the request process so infinite loops die with the worker.
import multiprocessing as mp
from typing import Any
def _worker(source: str, conn):
try:
conn.send(run_sandboxed(source))
except Exception as e:
conn.send({"ok": False, "error": type(e).__name__, "message": str(e)[:200]})
finally:
conn.close()
def run_with_timeout(source: str, timeout_s: float = 1.0) -> dict[str, Any]:
parent, child = mp.Pipe(duplex=False)
proc = mp.Process(target=_worker, args=(source, child), daemon=True)
proc.start()
child.close()
proc.join(timeout_s)
if proc.is_alive():
proc.kill()
proc.join()
return {"ok": False, "error": "Timeout", "message": f"exceeded {timeout_s}s"}
if parent.poll():
return parent.recv()
return {"ok": False, "error": "Empty", "message": "no result"}
print(run_with_timeout("result = 1 + 1"))
print(run_with_timeout("while True: pass", timeout_s=0.3))- Timeouts must be host-enforced, not prompt-enforced.
- Killing the worker is coarse but clear.
- Memory limits need OS cgroups or containers for seriousness.
Related: Containerized Sandboxes for Code-Executing Agent Tools
7. Deny by Default: Empty Env and No Credentials
Never inject API keys into the code-exec environment "for convenience."
import os
from contextlib import contextmanager
@contextmanager
def scrubbed_env(allow: set[str] | None = None):
allow = allow or set()
saved = dict(os.environ)
try:
os.environ.clear()
for k, v in saved.items():
if k in allow:
os.environ[k] = v
yield
finally:
os.environ.clear()
os.environ.update(saved)
# Agent code path should not see OPENAI_API_KEY / DB URLs
with scrubbed_env(allow={"PATH", "LANG"}):
assert "OPENAI_API_KEY" not in os.environ
print("credentials not visible to tool env")- Model traffic uses keys in the host agent process, not inside user code.
- Empty env is a form of least privilege.
- Allowlist env vars explicitly when a library needs one.
8. Put the Sandbox Behind a Bounded Agent Loop
Show the full pattern: model may request code exec; host always applies policy.
from typing import Callable
REGISTRY: dict[str, Callable[[str], str]] = {
"run_python_sandboxed": handle_run_python_sandboxed,
}
def run_agent_turn(tool_name: str, arguments_json: str, *, max_turns: int = 3) -> list[dict]:
"""Demo host: only allowlisted tools, hard turn cap."""
if tool_name not in REGISTRY:
return [{"ok": False, "error": "ToolNotAllowed"}]
observations = []
for turn in range(max_turns):
observations.append({
"turn": turn,
"tool": tool_name,
"observation": json.loads(REGISTRY[tool_name](arguments_json)),
})
# Real agents re-call the model here; we stop after one successful compute.
if observations[-1]["observation"].get("ok"):
break
return observations
print(run_agent_turn("run_python_sandboxed", '{"source": "result = 6 * 7"}'))
print(run_agent_turn("run_shell", '{"cmd": "ls"}')) # denied- Allowlists beat open-ended plugin maps.
- Max turns stop infinite repair loops.
- Combine with egress and credential recipes as you leave the learning sandbox.
What You Learned
- Agent security starts with a narrow tool and a hard sandbox, not with a stern system prompt.
- Negative tests for import, files, and network are part of the product.
- Timeouts, scrubbed env, and tool allowlists belong in the host.
- In-process restriction is a stepping stone; containers are the production default for untrusted code.
Related
- The Principle of Least Privilege Applied to Agent Tools
- Containerized Sandboxes for Code-Executing Agent Tools
- Scoping API Keys and Credentials Passed to an Agent
- Network Egress Controls for Agents That Call External Tools
- Security Checklist Before Shipping an Agent to Production
- Agent Security Best Practices
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.