Module 8 of 24 · Open Source Track
Code Execution & Sandboxing

Mistral can reason about code but cannot run it. Five concepts — why agents need to execute code, the three sandbox options, the security model, the execute_python tool, and the self-debugging loop that turns a chatbot into a reliable computing engine.

Track 2: Tool Use ⏱ ~16 min read 8 / 24
Concept 1 of 5

The model reasons; the interpreter computes

A language model predicts text — it doesn't calculate. Ask it for 3.7 ** 12 and it produces a plausible-looking number that may be subtly wrong, with no error to warn you.

A code execution tool flips the job: the model writes Python, a sandbox runs it, and you capture stdout, stderr, and the exit code. The model authors and reasons; the interpreter delivers the deterministic answer.

Concept 1 of 5

The mathematician with a calculator

BEFORE: Imagine asking a brilliant mathematician to compute 3.7 to the 12th power entirely in their head, then trusting the number they say out loud.

PAIN: They round at each step, carry a mental slip, and confidently hand you an answer that's slightly off — and you don't notice until you check it against a calculator.

MAPPING: A sandbox is that calculator. The mathematician (the model) still decides what to compute and how, but instead of guessing the result, they type the expression and read back exactly what the machine returns. The interpreter is always right.

Concept 1 of 5

One round trip through the tool

  1. Model calls the toolIt emits a tool call named execute_python with a code string as the argument.
  2. Agent runs the codeYour framework hands the code to a local executor — a subprocess or a container.
  3. Capture the outcomeCollect stdout, stderr, and the exit code (0 means success).
  4. Return as a tool messageFeed those results back so the model can turn them into its final answer.
Concept 1 of 5

Pseudocode

# WRONG: trust the model's mental math
answer = model.ask("what is 3.7 ** 12?")
# -> a confident, maybe-wrong number

# RIGHT: model writes code, machine runs it
call = model.wants_tool()
IF call.name == "execute_python":
  result = executor.run(call.code)
  # result = stdout, stderr, exit_code
  send_back_to_model(result)
# the interpreter produced the number,
# not the language model.
Concept 1 of 5

Misconceptions

"A big enough model does math reliably on its own."
Even frontier models miscompute multi-step arithmetic and data transforms. In one benchmark, accuracy jumped from 71% to 99.4% once a Python executor was added. Reasoning is not calculating.
"Mistral has code execution built in, like Claude's interpreter."
It doesn't. Open-source models have no sandbox — every byte of execution infrastructure is code you write and control, which is actually an advantage for observability and no vendor lock-in.

Give the agent a sandbox so it computes with an interpreter instead of guessing. The model authors code; the machine returns the truth.

Reserve the model for reasoning and code authoring — deterministic work belongs to the executor.

Concept 2 of 5

Three sandboxes, three trade-offs

There is no single "right" sandbox. You choose along three axes: security, cold-start speed, and infrastructure complexity. Pushing one usually costs another.

The three practical options are a subprocess (a child process, fastest but low isolation), a Docker container (ephemeral and isolated, the production default), and E2B cloud (a managed sandbox VM, no Docker needed, works on any OS).

Concept 2 of 5

Where you let a stranger cook

BEFORE: A stranger needs to cook one dish. You have to decide which kitchen to hand them.

PAIN: Your own kitchen (subprocess) is instant and easy, but they can rummage through every drawer and torch the house. Too trusting for a stranger.

MAPPING: A rented, sealed test kitchen you tear down after (Docker container) is safe and repeatable, but takes a minute to set up. A commercial kitchen-for-hire in another city (E2B cloud) needs zero setup on your end and works from anywhere — you just ship the recipe and get the plate back. Same meal, very different blast radius.

Concept 2 of 5

The three options

  1. subprocessA child process, cold-start under 50ms, low complexity. But it shares your filesystem and network — low-to-medium security. Trusted input only.
  2. Docker containerHigh security via OS-level isolation; 1–3s cold start; medium complexity. Destroyed after each run. The production choice for untrusted input.
  3. E2B cloudVery high security, ~2s start, low complexity, no Docker required. Runs in a managed cloud VM; free tier ~100 runs/hour.
Concept 2 of 5

Which sandbox should I use?

IF code is trusted (your own model,
   dev/prototyping, tight prompt):
  → subprocess  # fastest, simplest

IF input is untrusted / user-facing
   and you run in production:
  → Docker container  # isolate it

IF no Docker available (Windows, CI,
   serverless) or you want it managed:
  → E2B cloud sandbox

# default when unsure: Docker

All three can hide behind one identical executor interface, so swapping is a one-line change.

Concept 2 of 5

Misconceptions

"A subprocess is basically isolated."
No. It runs as your user with your filesystem and network access. It can read ~/.ssh/id_rsa or open sockets. Isolation requires Docker or a cloud VM, not just a separate process.
"Pick the most secure option always."
Security isn't free — Docker adds cold-start latency and ops overhead. For trusted, dev-only code, subprocess is the pragmatic choice. Match the sandbox to the threat.

Choose the sandbox by trust level: subprocess for trusted dev, Docker for production, E2B when Docker isn't an option.

Hide all three behind one run(code) -> result interface so you can swap them with a single line.

Concept 3 of 5

Assume the code is hostile

Model-generated code can be jailbroken or simply wrong, so you defend against five real attacks: path traversal, infinite loops, fork bombs, network exfiltration, and filesystem damage.

A single SecurityPolicy object encapsulates every limit — timeout, memory cap, CPU share, no network, read-only filesystem — and the same policy configures whichever executor you use. Defense is layered, not a single wall.

Concept 3 of 5

A bank teller behind glass

BEFORE: Imagine a bank that lets any visitor walk straight into the vault to "help themselves" to the service they came for.

PAIN: Most visitors are fine, but one bad actor can empty the vault, and a confused one can knock everything over. Trusting good intentions is not a control.

MAPPING: A SecurityPolicy is the teller counter: bulletproof glass (read-only filesystem), a locked cash drawer (no network to exfiltrate), a time limit per customer (timeout), and a queue cap (CPU/memory limits). The visitor gets exactly the service, and nothing else — no matter their intent.

Concept 3 of 5

The layers, attack by attack

  1. timeout + CPU limitKills infinite loops and starves fork bombs of CPU before they hang the host.
  2. memory capAn OOM kill stops memory bombs from crashing your agent process.
  3. --network=noneBlocks exfiltration — code can't POST your secrets to attacker.com.
  4. read-only filesystemOnly a mounted /workspace is writable, so path traversal and rm -rf / find nothing to damage.
  5. ephemeral containerThe --rm flag destroys everything the run created on exit.
Concept 3 of 5

Pseudocode

SecurityPolicy:
  timeout_seconds   = 10       # kill hangs
  max_memory        = 128 MB   # OOM guard
  max_cpus          = 0.5      # half a core
  network_disabled  = true    # no exfil
  readonly_fs       = true    # no writes
  image             = "python:3.12-alpine"

# one policy, passed to ANY executor:
executor = DockerExecutor(policy)
executor.run(untrusted_code)
Concept 3 of 5

Misconceptions

"A good system prompt keeps the code safe."
Prompts are not a security boundary. Models get jailbroken and even well-meaning ones generate dangerous code from ambiguous requests. The Docker flags are not optional — enforce limits in the runtime, not the prompt.
"One limit (like a timeout) is enough."
Each limit stops a different attack. A timeout won't block exfiltration; --network=none won't stop an infinite loop. You need the layers together.

Treat every execution as untrusted and stack layered limits in a single SecurityPolicy. Timeout, memory, CPU, no network, read-only FS, ephemeral container.

The prompt is guidance; the runtime is the guarantee.

Concept 4 of 5

One tool schema, any backend

The model sees a single tool definition — execute_python with a code parameter — no matter which sandbox runs it. The schema's description is what the model reads to decide when to call it, so it must be concrete: say it returns stdout, stderr, and an exit code.

Behind that schema sit interchangeable executors (subprocess or Docker) that share one run(code, timeout) signature and return one ExecResult. Swap backends without touching the agent.

Concept 4 of 5

A standard wall socket

BEFORE: Imagine every appliance needed a differently-shaped plug wired directly to a specific power source.

PAIN: Change the power source and you rewire every appliance. Nothing is swappable; each device is welded to one supply.

MAPPING: The tool schema is a standard wall socket. The model (appliance) always plugs into the same shape — execute_python(code) — while behind the wall the supply can be a subprocess, Docker, or E2B. Change the backend and nothing on the model's side has to change.

Concept 4 of 5

From schema to result

  1. Define the schemaName it execute_python; the code string is required, timeout_seconds optional. Describe what comes back.
  2. Model emits a tool callIt fills in the code argument as valid JSON in its response.
  3. Write code to a temp fileThe executor writes it out; never run with a shell, to avoid injection.
  4. Run and captureLaunch subprocess or container, collect stdout/stderr/exit code into one ExecResult.
  5. Format for the modelTurn ExecResult into a compact tool message the model can read next turn.
Concept 4 of 5

Pseudocode

TOOL "execute_python":
  param code    : string  (required)
  param timeout : int     (default 10)
  description   : "runs Python, returns
                 stdout, stderr, exit_code"

FUNCTION run(code, timeout):
  path = write_temp_file(code)
  TRY:
    out, err, exit = launch(path, timeout)
    # shell = OFF (no injection)
  CATCH timeout:
    RETURN result(err="timed out", exit=124)
  FINALLY: delete_temp_file(path)
  RETURN result(out, err, exit)

Same signature whether the backend is a subprocess or a Docker container.

Concept 4 of 5

Misconceptions

"A vague description like 'runs Python' is fine."
The model chooses tools from the description. Spell out what it returns (stdout, stderr, exit code) and when to use it — smaller local models are more literal than GPT-4 and need the detail.
"Just pass the code to a shell to run it."
Running with a shell opens shell-injection even when the code looks safe. Write to a temp file and launch the interpreter directly with the shell disabled.

Expose one clear execute_python schema and back it with interchangeable executors sharing a single result shape.

Concrete descriptions drive tool selection; a temp file plus shell-off launch keeps execution safe.

Concept 5 of 5

Feed the error back and let it fix itself

When the executor returns a non-zero exit code, you don't give up — you append the stdout and stderr to the message history and call the model again. It reads its own error, diagnoses the bug, and rewrites the code.

This is a ReAct loop for code: write → run → read error → fix → retry, capped at a few attempts (typically 3). The retry counter lives in your code; the model just sees a fresh error each round.

Concept 5 of 5

A student with the answer key's red marks

BEFORE: Picture a student who submits homework and simply never learns whether any answer was wrong.

PAIN: Without feedback they repeat the same mistake forever — no error message, no correction, no improvement.

MAPPING: The self-debugging loop hands the graded paper back with the red marks (the stderr). The student (model) reads exactly what failed — "NameError: df not defined" — corrects that specific line, and resubmits. A cap of three tries stops an endless back-and-forth.

Concept 5 of 5

One pass of the loop

  1. Write & callThe model generates code and calls execute_python.
  2. RunThe executor runs it and returns stdout, stderr, and the exit code.
  3. Branch on exit codeIf non-zero, append the error as a tool message and ask the model to retry.
  4. Retry, cappedRepeat up to 3 attempts, then return the last error to the caller.
  5. SuccessOn exit code 0, return the final stdout as the agent's answer.
WRITE code #1
↓
RUN → exit=1  NameError
↓ append error, retry
FIX → add missing import
↓
RUN → exit=0  return
Concept 5 of 5

Pseudocode

messages = [ system, user_request ]

FOR attempt IN 1..max_retries:
  reply = model.call(messages, tools)
  IF no tool_call: RETURN reply.text

  messages.append(reply)        # FIRST
  result = executor.run(reply.code)
  messages.append(tool_result)  # THEN

  IF result.exit_code == 0:
    CONTINUE   # let model summarize
  IF attempt == max_retries:
    RETURN "failed: " + result.stderr
Concept 5 of 5

Misconceptions

"Message order doesn't matter."
It does. The assistant message (with the tool call) must be appended before the tool result. The OpenAI-compatible API requires this order or the call is rejected.
"Let it retry until it succeeds."
Unbounded retries can loop forever and burn tokens on a task the model can't solve. Cap attempts (about 3) and return the last error so the caller can handle failure.

On failure, append the error to the history and let the model read, fix, and retry — capped at ~3 attempts.

Append the assistant tool-call message before the tool result; keep the retry counter in your code, not the model.

Test the core insight

One question per concept. Tap to reveal the answer.

Open the full module on desktop

The desktop version has you build both SubprocessExecutor and DockerExecutor against a shared SecurityPolicy, wire up the full CodeExecutionAgent self-debugging loop, and run real pandas code inside a locked-down Docker container — in Python and Node.

Open M15 on Desktop → Why run code · sandboxes · security · execute_python · self-debugging · ~70 min

Module 8 of 24 · Open Source Track · Tool Use