The gap: agents that think but can't act
The OpenAI Agents SDK is excellent at orchestration. You define agents with instructions and tools, wire them together with handoffs, add guardrails for safety, and run them with Runner.run(). The model decides which tools to call and in what order.
But the SDK ships with no built-in compute tools. There's no execute_code. No run_shell_command. No open_browser. Your agent can reason about code, but it can't run it. It can plan a web research task, but it can't navigate a URL.
OpenAI's cloud Codex product solves this with bundled containers, but that locks you into GPT models and Codex's pricing. The Agents SDK is model-agnostic in theory — it works with OpenAI, Azure, and any OpenAI-compatible API. The compute layer should be model-agnostic too.
Prerequisites
Python 3.10+ with the SDK installed
pip install openai-agents httpx
API keys
Set OPENAI_API_KEY for the model and SANDBOX_API_KEY for the sandbox platform.
Building sandbox tools
The Agents SDK uses the @function_tool decorator to register Python functions as tools the model can call. Each function gets a docstring (which becomes the tool description) and typed parameters (which become the JSON schema).
We'll build four tools: create a sandbox, execute a command, launch a browser, and save a file.
The sandbox client
# sandbox_tools.py import os import httpx SANDBOX_URL = os.environ.get("SANDBOX_URL", "https://sandbox.dev.ab0t.com") SANDBOX_API_KEY = os.environ["SANDBOX_API_KEY"] client = httpx.Client( base_url=SANDBOX_URL, headers={"Authorization": f"Bearer {SANDBOX_API_KEY}"}, timeout=120.0, ) # Track the active sandbox _active_sandbox_id: str | None = None
Tool: create_sandbox
from agents import function_tool @function_tool def create_sandbox(name: str, instance_type: str = "ab0t.medium") -> str: """Create an isolated Linux sandbox for code execution. Use ab0t.micro for light tasks, ab0t.medium for builds and tests, ab0t.gpu for GPU workloads. Returns the sandbox ID.""" global _active_sandbox_id resp = client.post("/api/sandboxes", json={ "name": name, "instance_type": instance_type, "auto_stop_minutes": 60, }) resp.raise_for_status() data = resp.json() _active_sandbox_id = data["sandbox_id"] return ( f"Sandbox created: {data['sandbox_id']}\n" f"Status: {data['status']}\n" f"Cost: ${data.get('hourly_cost', '0.04')}/hr\n" f"Ready for commands." )
Tool: execute_command
@function_tool def execute_command(command: str) -> str: """Execute a shell command in the active sandbox. Use for: installing packages, running tests, git operations, building projects, running scripts. Returns stdout, stderr, and exit code.""" if not _active_sandbox_id: return "No active sandbox. Call create_sandbox first." resp = client.post( f"/api/sandboxes/{_active_sandbox_id}/execute", json={"command": command}, ) resp.raise_for_status() data = resp.json() parts = [] if data.get("stdout"): parts.append(data["stdout"]) if data.get("stderr"): parts.append(f"STDERR:\n{data['stderr']}") parts.append(f"Exit code: {data.get('exit_code', 0)}") if data.get("execution_time_ms"): parts.append(f"Time: {data['execution_time_ms']}ms") return "\n".join(parts)
Tool: launch_browser
@function_tool def launch_browser(url: str = "", browser_type: str = "chrome") -> str: """Launch a cloud browser for web automation. Returns a CDP URL for connecting with Playwright or Puppeteer, plus an access URL for viewing the browser in your own browser. Use for: web research, form filling, scraping, testing.""" resp = client.post("/api/browsers", json={ "browser_type": browser_type, "homepage_url": url, }) resp.raise_for_status() data = resp.json() return ( f"Browser launched: {data['container_id']}\n" f"CDP URL: {data.get('cdp_url', 'pending...')}\n" f"View URL: {data.get('access_url', 'pending...')}\n" f"Connect: browser = await pw.chromium.connect_over_cdp(cdp_url)" )
Tool: save_file
@function_tool def save_file(filename: str, content: str) -> str: """Save a file to the sandbox's /workspace directory. Use for: writing scripts, config files, test fixtures.""" if not _active_sandbox_id: return "No active sandbox." resp = client.post( f"/api/sandboxes/{_active_sandbox_id}/files", json={"filename": filename, "content": content}, ) resp.raise_for_status() return f"Saved: /workspace/{filename}" @function_tool def stop_sandbox() -> str: """Stop the active sandbox. Preserves data. Stops billing.""" global _active_sandbox_id if _active_sandbox_id: client.post(f"/api/sandboxes/{_active_sandbox_id}/stop") sid = _active_sandbox_id _active_sandbox_id = None return f"Sandbox {sid} stopped. Billing paused." return "No active sandbox."
Creating the agent
Now wire the tools into an Agent. The agent gets instructions that tell it about the sandbox and when to use each tool.
# agent.py from agents import Agent, Runner from sandbox_tools import ( create_sandbox, execute_command, launch_browser, save_file, stop_sandbox ) coding_agent = Agent( name="Sandbox Developer", model="gpt-4.1", instructions="""You are a software developer with access to a cloud sandbox. You can create isolated Linux environments, execute shell commands, launch browsers for web tasks, and save files. WORKFLOW: 1. Always create_sandbox first before executing commands. 2. Use execute_command for: git, pip, npm, pytest, build commands. 3. Use launch_browser for: web research, scraping, testing web UIs. 4. Use save_file to write code, scripts, or config files. 5. Always stop_sandbox when the task is complete to stop billing. The sandbox is a fresh Ubuntu VM. Install any packages you need. You have root access via sudo.""", tools=[create_sandbox, execute_command, launch_browser, save_file, stop_sandbox], )
Full example: clone, test, and fix
Here's a complete script that gives the agent a task, lets it work autonomously in a sandbox, and prints the results:
# run_agent.py import asyncio from agents import Agent, Runner from sandbox_tools import ( create_sandbox, execute_command, launch_browser, save_file, stop_sandbox ) agent = Agent( name="Code Reviewer", model="gpt-4.1", instructions="""You review code repositories. For each task: 1. Create a sandbox 2. Clone the repo 3. Install dependencies 4. Run the test suite 5. Analyze failures and suggest fixes 6. Stop the sandbox when done Be thorough. Read test output carefully. If tests fail, look at the relevant source code to understand why.""", tools=[create_sandbox, execute_command, save_file, stop_sandbox], ) async def main(): result = await Runner.run( agent, input="Review https://github.com/fastapi/fastapi — clone it, " "run the tests, and report which ones fail and why.", ) print("\n=== Agent Output ===") print(result.final_output) asyncio.run(main())
When you run this, the agent will:
- Call
create_sandbox("fastapi-review", "ab0t.medium") - Call
execute_command("git clone https://github.com/fastapi/fastapi && cd fastapi && pip install -e '.[all,dev]'") - Call
execute_command("cd fastapi && python -m pytest tests/ -x --tb=short -q") - Read the output, reason about failures, possibly look at source files
- Call
stop_sandbox() - Return a structured analysis
The model sees the tool descriptions and decides what to call. It doesn't know about the sandbox API directly — it just sees functions like "execute a shell command in the active sandbox" and decides when to use them based on the task.
Streaming output
For long-running tasks, use Runner.run_streamed() to see tool calls and results in real time:
async def main_streamed(): result = Runner.run_streamed( agent, input="Clone the repo, run tests, fix any failures, run tests again.", ) async for event in result.stream_events(): if event.type == "tool_call": print(f" Tool: {event.tool_call.name}({event.tool_call.arguments})") elif event.type == "tool_output": output = event.output[:200] # Truncate for display print(f" Result: {output}...") elif event.type == "text_delta": print(event.text, end="", flush=True) print(f"\n\nFinal: {result.final_output}")
Multiple agents, multiple sandboxes
The SDK supports handoffs between agents. Give each specialized agent its own sandbox:
# Each agent manages its own sandbox via separate tool instances from agents import Agent, Runner # Research agent uses browsers researcher = Agent( name="Researcher", model="gpt-4.1", instructions="You research topics using web browsers. " "Launch a browser, navigate to relevant pages, " "and extract key findings.", tools=[create_sandbox, launch_browser, execute_command, stop_sandbox], ) # Coding agent uses terminals developer = Agent( name="Developer", model="gpt-4.1", instructions="You write and test code in a sandbox. " "Create a sandbox, write code, run tests, iterate.", tools=[create_sandbox, execute_command, save_file, stop_sandbox], ) # Orchestrator hands off between them orchestrator = Agent( name="Project Lead", model="gpt-4.1", instructions="You manage a research project. First, hand off to " "Researcher to gather information. Then hand off to " "Developer to implement the findings as code.", handoffs=[researcher, developer], ) async def main(): result = await Runner.run( orchestrator, input="Research the top 5 Python testing frameworks in 2026, " "then write a comparison script that benchmarks each one " "against a sample project.", ) print(result.final_output)
Codex CLI on a sandbox
OpenAI's Codex CLI is a terminal agent similar to Claude Code. Same pattern applies — SSH into a sandbox and run it there:
# Create a sandbox SANDBOX=$(curl -s -X POST "$SANDBOX_URL/api/sandboxes" \ -H "Authorization: Bearer $SANDBOX_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"codex-dev","instance_type":"ab0t.medium","auto_stop_minutes":120}') # SSH in ssh ubuntu@$(echo $SANDBOX | jq -r '.instance_ip') # Inside the sandbox: npm install -g @openai/codex export OPENAI_API_KEY="sk-YOUR_KEY" # Run Codex in Full Auto mode cd /home/ubuntu/workspace git clone https://github.com/your-org/your-project.git cd your-project codex --approval-mode full-auto "Run the tests and fix any failures"
Codex Cloud provides its own containers but locks you into GPT models and their pricing. Codex CLI on a sandbox gives you the same experience with transparent compute costs ($0.08/hr for ab0t.medium) and the freedom to switch models later.
What it costs
| Scenario | Sandbox | Duration | Compute Cost | Model Cost (est.) |
|---|---|---|---|---|
| Clone + test a repo | ab0t.medium | ~15 min | $0.01 | ~$0.05 |
| Multi-agent research + code | 2x ab0t.medium | ~30 min | $0.04 | ~$0.15 |
| Browser scraping (10 sites) | 10x Chrome | ~10 min | $0.07 | ~$0.10 |
| Codex CLI full session | ab0t.medium | ~2 hrs | $0.08 | ~$0.50 |
Codex Cloud bundles compute into its pricing. With a sandbox, compute and model costs are separate and transparent. A typical task that costs $0.50 in model tokens uses $0.01–$0.04 in sandbox compute.
Tips
Always call stop_sandbox
Add it to the agent instructions and as the last tool in the list. If the agent forgets, the auto_stop_minutes setting catches it, but explicit stops are cheaper.
Set timeouts on long commands
The HTTP client timeout is 120 seconds by default. For long builds, increase it: timeout=300.0. Or break long commands into steps so the agent gets intermediate feedback.
Use guardrails for dangerous commands
The SDK's guardrails feature can intercept tool calls before execution. Add a guardrail that blocks rm -rf /, shutdown, and other destructive commands.
Log tool calls for debugging
Use Runner.run_streamed() during development to see every tool call and result. This makes it easy to spot when the agent is going off-track.
Troubleshooting
Agent doesn't call create_sandbox first
Add explicit instructions: "ALWAYS call create_sandbox before any other sandbox tool." Put it in bold in the instructions. GPT-4.1 follows this reliably.
"No active sandbox" errors
The _active_sandbox_id is module-level state. If you restart the script, it forgets the ID. Either create a new sandbox or persist the ID to a file.
Tool calls timeout
The sandbox API returns results synchronously. For commands that take >2 minutes, increase the httpx timeout or use execute_command("nohup long_command &") and poll for results.
Agent creates too many sandboxes
The agent may create a new sandbox for each subtask. Add to instructions: "Reuse the existing sandbox unless you need a different instance type."
What's next
Give your OpenAI agent a computer
Terminals, browsers, and desktops. Model-agnostic compute for any agent.
Get Started Free