Secure code execution for AutoGen agents
Give an AutoGen or AG2 agent an autogen code execution sandbox in Python — a custom tool that runs the model's code inside an isolated Firecracker microVM.
An AutoGen agent that proposes Python to answer a question has to run it somewhere. Running it on your host is a liability — a hallucinated subprocess.run or open("/etc/...") is now your problem. This recipe gives the agent a code-execution function backed by a PandaStack code interpreter microVM: the model proposes code, an executor runs it inside a netns-isolated Firecracker VM, and the result — including charts as objects, not stdout — flows back into the conversation.
Which "autogen" do you mean?
The autogen name now points at three different codebases. Pick one and never mix imports — they have different import roots and incompatible APIs.
| Project | PyPI install | Import root | Notes |
|---|---|---|---|
| AG2 (the AutoGen fork) | pip install "ag2[openai]" | autogen | Active line (v0.13.4, Jun 2026). ConversableAgent + register_function. This recipe's primary path. |
| autogen-agentchat | pip install "autogen-agentchat" "autogen-ext[openai]" | autogen_agentchat | Microsoft's async actor-model rewrite (v0.7.5, Sep 2025). AssistantAgent(tools=[...]). Shown as an alternative below. |
| Microsoft Agent Framework | pip install agent-framework | agent_framework | Microsoft's stated successor to autogen-agentchat. Different API again — out of scope here. |
Two footguns to avoid:
pip install autogenresolves to AG2's alias, not Microsoft'sautogen-agentchat. If you installautogenand thenimport autogen_agentchat, the import fails — they are different packages.pip install pyautogenis the legacy 0.2 dist name. Don't use it for new code.
We use AG2 as the primary path: it is the actively-developed line, it ships first-class code-execution tooling, and its single-package install is simplest for a sandbox recipe.
Install
The PandaStack SDK reads PANDASTACK_API_KEY from the environment — never hardcode it in your tool.
export PANDASTACK_API_KEY=pds_... # your PandaStack key
export OPENAI_API_KEY=sk-... # AG2's model client
pip install pandastack "ag2[openai]"The caller / executor split
AG2's code-execution model is built on two agents: a caller (the LLM agent that decides to invoke the tool) and an executor (the agent that actually runs the function body). This maps cleanly onto PandaStack — make the executor's function call into a sandbox:
| Role | Agent | What it does |
|---|---|---|
| caller | ConversableAgent (with LLMConfig) | Proposes the tool call, reads the result, decides next step |
| executor | ConversableAgent (human_input_mode="NEVER") | Runs run_python against the PandaStack sandbox |
The function body is plain Python — its type hints and description become the tool schema the model sees.
The sandbox tool
Create a long-lived code context once — a Jupyter-style kernel inside the microVM — and reuse it across every tool call so imports and variables persist across the conversation.
import os
from autogen import ConversableAgent, LLMConfig, register_function
from pandastack import Sandbox, CodeContext
# One persistent kernel inside a Firecracker microVM, shared across tool calls.
_ctx: CodeContext | None = None
def _kernel() -> CodeContext:
global _ctx
if _ctx is None:
sandbox = Sandbox.create(
template="code-interpreter",
ttl_seconds=3600,
metadata={"tool": "autogen-code-exec"},
)
_ctx = sandbox.create_code_context(language="python")
return _ctx
def run_python(code: str) -> str:
"""Run Python code in a secure, isolated sandbox and return its output.
State persists across calls: variables and imports from earlier
executions are still available. Use this to compute, analyze data,
or verify a result by running code.
"""
ex = _kernel().run_code(code)
# Hand the model everything it needs to self-correct.
if ex.error:
return f"ERROR:\n{ex.error}\n\nstdout:\n{ex.stdout}".strip()
parts = []
if ex.stdout:
parts.append(ex.stdout.rstrip())
if ex.stderr:
parts.append(f"[stderr]\n{ex.stderr.rstrip()}")
if ex.text and ex.text not in (ex.stdout or ""):
parts.append(f"[result] {ex.text}")
if ex.png:
# A chart came back as a base64 PNG object — not stdout to parse.
parts.append("[chart] PNG produced (base64 available on ex.png)")
return "\n\n".join(parts) or "(no output)"The tool returns a string — that string is exactly what the caller agent sees on its next turn. Returning stderr and the full traceback (not just stdout) is the move that makes self-correction work: the model reads its own failure and fixes the code on the next call.
Why a code context, not exec
create_code_context() is a persistent kernel, not a one-shot. Imports and variables survive across run_code calls, so the agent builds up state over a multi-turn conversation instead of re-importing pandas every time. And on the code-interpreter template — which bakes IPython, matplotlib, plotly, kaleido, and pandas — rich outputs are captured automatically: a plt.show() or a bare DataFrame as the last expression comes back as ex.png / ex.results[0].html objects. No savefig, no save-to-disk, no second download.
Execution field | What the agent gets |
|---|---|
stdout / stderr | captured streams (feed both back so it can self-correct) |
error | formatted traceback if the cell raised |
text | convenience: the last text repr (a bare expression result) |
png | convenience: first chart as a base64 PNG object |
results | full list of rich outputs (.html, .json, .svg, .markdown, …) |
Wire it into AG2
Register run_python with the two-agent split, then start the chat. The executor runs the code; the caller reads the result back.
llm_config = LLMConfig(api_type="openai", model="gpt-4o")
with llm_config:
analyst = ConversableAgent(
name="analyst",
system_message=(
"You answer quantitative questions by writing and running Python "
"with the run_python tool. Never guess at numbers — compute them. "
"Read any error traceback the tool returns and fix your code before "
"answering. Reply with TERMINATE when the question is fully answered."
),
)
# The executor actually runs the tool; it never calls the model.
code_runner = ConversableAgent(name="code_runner", human_input_mode="NEVER")
register_function(
run_python,
caller=analyst, # the LLM agent that may CALL the tool
executor=code_runner, # the agent that actually RUNS it
name="run_python",
description="Run Python code in a secure PandaStack sandbox and return its output.",
)
try:
code_runner.initiate_chat(
analyst,
message=(
"Generate 1,000 samples from a normal distribution (mean 50, std 8) "
"with numpy. Report the sample mean and the 95th percentile, then "
"plot a histogram of the samples with matplotlib (call plt.show())."
),
max_turns=8,
)
finally:
if _ctx is not None:
_ctx.close() # tear down the kernel + sandboxThe analyst proposes a numpy snippet, code_runner executes it in the sandbox and feeds the printed values back, and — because the task asks for a plot — the final call produces a chart that comes back as ex.png. The tool reports [chart] PNG produced, and the base64 image rides on the Execution, ready to render in your UI. If the agent writes broken code, the ERROR: prefix and traceback go straight back into its context and it retries — code execution and self-correction, on an isolated Firecracker VM.
Decorator style
If you prefer decorators to register_function, the same wiring is two stacked decorators on the function. The inner register_for_llm description and the function's signature become the tool schema; register_for_execution binds it to the executor:
@code_runner.register_for_execution()
@analyst.register_for_llm(description="Run Python in a secure PandaStack sandbox.")
def run_python(code: str) -> str:
ex = _kernel().run_code(code)
if ex.error:
return f"ERROR:\n{ex.error}"
return ex.stdout or ex.text or "(no output)"AG2 also ships a built-in
PythonCodeExecutionToolunderautogen.tools.experimental. It runs code in a localWorkingDirectory/SystemPythonEnvironmenton your host — fine for trusted code, but it does not give you VM isolation. The custom tool above is the path when the code is model-generated and you want it contained.
Alternative: autogen-agentchat
If you are on Microsoft's autogen-agentchat line instead, the wiring is different but the PandaStack tool body is identical. There is no caller/executor split and no register_function — tools are plain functions passed to AssistantAgent(tools=[...]), and the whole API is async. Forgetting asyncio.run / await (and await model_client.close()) is the most common error.
import asyncio
from autogen_agentchat.agents import AssistantAgent
from autogen_ext.models.openai import OpenAIChatCompletionClient
# reuse the same _kernel() helper and run_python body from above
async def main():
model_client = OpenAIChatCompletionClient(model="gpt-4o")
agent = AssistantAgent(
name="analyst",
model_client=model_client,
tools=[run_python], # type hints + docstring become the tool schema
system_message="Use the run_python tool to execute code in the sandbox.",
)
result = await agent.run(task="Compute the mean of [3, 1, 4, 1, 5, 9, 2, 6].")
print(result)
await model_client.close()
if _ctx is not None:
_ctx.close()
asyncio.run(main())Do not mix the two paths' symbols: ConversableAgent / LLMConfig / register_function are AG2-only, and AssistantAgent(model_client=..., tools=[...]) / OpenAIChatCompletionClient are autogen-agentchat-only.
Notes
- Isolation. Each sandbox is its own Firecracker microVM with netns-isolated egress — a model that writes
rm -rf /or tries to read host files is contained to the VM, not your machine. - One kernel per conversation. The tool shares a single
CodeContextso state persists across turns. For concurrent agents, create one kernel (oneSandbox) per conversation to avoid sharing state. - Always tear down. Close the context in a
finallyso the kernel and sandbox don't linger. Idle kernels are reaped on TTL regardless, but explicit cleanup is cheaper. - TypeScript. AutoGen and AG2 are Python-only. The underlying PandaStack call maps one-to-one if you build the same tool in a TS agent framework:
const ex = await ctx.runCode(code)returns{ stdout, stderr, error?, text?, png? }.
Next steps
- Code interpreter — the full code-context result model (charts, DataFrames, JSON) this tool builds on.
- CrewAI code execution — the same wedge wired into a CrewAI custom tool.
- Run code from a LangGraph agent — the same pattern in a graph runtime.
- Build a data-analyst agent — upload a CSV and stream charts back to the model.
CrewAI code execution after CodeInterpreterTool
Build a CrewAI code execution sandbox tool with PandaStack after the built-in CodeInterpreterTool was deprecated and removed.
LlamaIndex code execution tool with PandaStack
Give a LlamaIndex agent a Python sandbox by wiring a code execution tool to a PandaStack CodeContext, so the model can run code instead of guessing.