Skip to content
Guides · Integrations

Run OpenAI Agents SDK Tools in a Cloud VM and Get Files Back

Updated October 27, 2026 .md

Short answer: Wrap sandboxer-sdk calls in @function_tool functions, so the agent's shell commands and file reads run in a cloud Ubuntu VM, and add a download tool that copies results to your machine. Or write no tool code at all: connect MCPServerStreamableHttp to https://sandboxer.com/mcp with a Bearer API key and get 18 tools.

This guide builds on the OpenAI Agents SDK docs page. It adds a file-download tool, a way to test your tools without an OpenAI key, and the hosted MCP server. We ran everything below on Sandboxer with openai-agents 0.24.0, sandboxer-sdk 0.1.1 and Python 3.13. We had no OpenAI key, so a scripted model stood in for the LLM. Section 4 explains exactly which part that covers.

Which way to connect#

Function tools (sandboxer-sdk) Hosted MCP (MCPServerStreamableHttp) Local MCP (MCPServerStdio + sbx mcp)
What the agent sees Only the tools you write 18 tools: create, exec, files, ports, snapshots, stop, delete The same tools, plus handoff_session
Install pip install sandboxer-sdk Nothing beyond openai-agents The sbx CLI on the host
Getting files back Your own tool writes them to the host disk read_file returns text into the model's context (up to 5 MiB) Same as hosted
Who decides the sandbox's size and lifetime Your code The model, through create_sandbox arguments sbx mcp --type --ttl defaults, which the model can override
Best for Production agents with a fixed, narrow tool set Trying it out, and agents that need the whole lifecycle Desktop or CI hosts that already have sbx

Version 0.24 of the Agents SDK also ships sandbox agents, with hosted clients for several providers (OpenAI docs). Sandboxer isn't one of those clients. Use function tools or MCP instead.

1. Install#

python3 -m venv .venv && . .venv/bin/activate
pip install openai-agents sandboxer-sdk
export SANDBOXER_API_KEY=sbx_live_…

Install sandboxer-sdk, not sandboxer. The PyPI package named sandboxer is an unrelated project. To run a real model later, also set OPENAI_API_KEY.

2. Define the tools#

Save this as sandbox_tools.py. It creates one small sandbox (2 vCPU, 4 GB, $0.018/hour) the first time the agent calls a tool. Every later call reuses it.

sandbox_tools.py
import pathlib

from agents import Agent, function_tool
from sandboxer import AsyncSandboxer, SandboxerError

sandboxer = AsyncSandboxer()   # reads SANDBOXER_API_KEY
sandbox = None                 # one sandbox per task, created on first use


async def get_sandbox():
    global sandbox
    if sandbox is None:
        sandbox = await sandboxer.create(type="small", ttl_seconds=1800, name="agent-task")
    return sandbox


def tool_error(ctx, error: Exception) -> str:
    # The SDK's default hides the reason ("An error occurred while running the tool").
    # Sandboxer errors are safe to show the model, so it can fix the path or command.
    if isinstance(error, SandboxerError):
        return f"error: {error}"
    return "error: the tool failed"


@function_tool(failure_error_function=tool_error)
async def run_shell(command: str) -> str:
    """Run a bash command in the cloud sandbox (Ubuntu 24.04, cwd /home/user). Returns exit code, stdout and stderr."""
    sb = await get_sandbox()
    result = await sb.exec(command, timeout_seconds=300)
    return f"exit code {result.exit_code}\n{result.stdout}{result.stderr}".strip()


@function_tool(failure_error_function=tool_error)
async def read_file(path: str) -> str:
    """Read a UTF-8 text file from the sandbox (up to 5 MiB). Relative paths are under /home/user."""
    sb = await get_sandbox()
    return await sb.files.read(path)


@function_tool(failure_error_function=tool_error)
async def download_file(path: str) -> str:
    """Copy a file (up to 50 MiB) from the sandbox into ./artifacts on the host and return where it was saved."""
    sb = await get_sandbox()
    data = await sb.files.download(path)
    out = pathlib.Path("artifacts") / pathlib.Path(path).name   # basename only: the model can't pick a host path
    out.parent.mkdir(exist_ok=True)
    out.write_bytes(data)
    return f"saved {out} ({len(data)} bytes)"


agent = Agent(
    name="Analyst",
    instructions="Do all work in the sandbox with run_shell. Save results as files and return them with download_file.",
    tools=[run_shell, read_file, download_file],
)

How the tools split the work:

  • read_file puts a file's text into the model's context. Use it for logs and small outputs the model must reason about.
  • download_file writes the bytes to your disk and gives the model only a one-line receipt. Use it for reports, CSVs, images and archives. The model never sees the contents, so they cost no tokens.
  • tool_error exists because, by default, the SDK hides why a tool failed. Without it, the model sees only "An error occurred while running the tool. Please try again." With it, the model sees Sandboxer's error message and can fix its next call.
  • The host path stays under your control. download_file keeps only the file's basename and saves it under ./artifacts, so the model can't write anywhere else on your machine.

3. Test the tools without a model#

A FunctionTool has an on_invoke_tool(ctx, json_args) method. The Runner calls the same method, so calling it yourself tests the real tool code path with no LLM:

call_tools.py
import asyncio
import json

from agents.tool_context import ToolContext

import sandbox_tools
from sandbox_tools import download_file, read_file, run_shell


async def call(tool, **args):
    raw = json.dumps(args)
    ctx = ToolContext(context=None, tool_name=tool.name, tool_call_id="manual-1", tool_arguments=raw)
    return await tool.on_invoke_tool(ctx, raw)   # the same entry point the Runner uses


async def main() -> None:
    try:
        print(await call(run_shell, command=(
            "python3 -c \"import csv,platform,os; w=csv.writer(open('report.csv','w')); "
            "w.writerow(['key','value']); w.writerow(['python',platform.python_version()]); "
            "w.writerow(['cpus',os.cpu_count()])\" && ls -l report.csv"
        )))
        print(await call(read_file, path="report.csv"))
        print(await call(download_file, path="report.csv"))
        print(await call(read_file, path="missing.txt"))
    finally:
        if sandbox_tools.sandbox is not None:
            print("sandbox", sandbox_tools.sandbox.id)
            await sandbox_tools.sandbox.delete()


asyncio.run(main())
$ python call_tools.py
exit code 0
-rw-r--r-- 1 user user 34 Oct 27 02:08 report.csv
key,value
python,3.12.3
cpus,2

saved artifacts/report.csv (34 bytes)
error: sandbox_direct_failed: no such file: /home/user/missing.txt (status 502) [request_id=req_SqZPHoFraw2G7JosNvx9]
sandbox sb_jrdd1ft5ar

The whole script took about 8 seconds in our single run on October 27, 2026, including creating and deleting the sandbox. artifacts/report.csv is now on your machine. The last call shows what the model gets when it asks for a file that doesn't exist. Since a fix later on October 27, 2026, that call returns file_not_found with status 404 and isn't marked retryable (see API error codes). Python inside the sandbox is 3.12, whatever version runs on your host.

4. Run the agent#

With an OpenAI key, run the agent the usual way:

result = await Runner.run(agent, "Check the machine and save when you ran.")

We had no OpenAI key, so we didn't run a real model. Instead we ran the Runner with the SDK's ScriptedModel (in agents.testing), which plays back the tool calls a model would make. The Runner, the tool dispatch, the sandbox and the download were all real. Only the model's decisions were scripted.

scripted_run.py
import asyncio

from agents import Runner
from agents.testing import ScriptedModel, assistant_message, function_call

import sandbox_tools
from sandbox_tools import agent

# Stands in for the LLM: each step is one model turn.
agent.model = ScriptedModel([
    [function_call("run_shell", {"command": "uname -sr && nproc && free -h | head -2 && date -u > ran-at.txt"}, call_id="call_1")],
    [function_call("download_file", {"path": "ran-at.txt"}, call_id="call_2")],
    [assistant_message("Done. The timestamp file is in artifacts/ran-at.txt.")],
])


async def main() -> None:
    result = await Runner.run(agent, "Check the machine and save when you ran.")
    for item in result.new_items:
        if item.type == "tool_call_output_item":
            print(f"--- tool output\n{item.output}")
    print("--- final_output\n" + result.final_output)
    print("--- sandbox", sandbox_tools.sandbox.id)


asyncio.run(main())
$ python scripted_run.py
--- tool output
exit code 0
Linux 6.18.56-sandboxer
2
               total        used        free      shared  buff/cache   available
Mem:           3.8Gi       304Mi       3.1Gi       352Ki       624Mi       3.5Gi
--- tool output
saved artifacts/ran-at.txt (32 bytes)
--- final_output
Done. The timestamp file is in artifacts/ran-at.txt.
--- sandbox sb_qdxatv1dvg
OPENAI_API_KEY is not set, skipping trace export
$ cat artifacts/ran-at.txt
Tue Oct 27 02:08:36 AM UTC 2026

The last line of output is the SDK's tracing. With no OpenAI key it skips sending the trace. ScriptedModel is also a cheap way to test your tools in CI.

5. Get bigger outputs back, then clean up#

download_file handles single files up to 50 MiB. For whole folders or anything bigger, pull them with the sbx CLI once the run ends. This sandbox was left running on purpose:

sbx scp sb_qdxatv1dvg:/home/user/ran-at.txt ./
sbx scp -r sb_qdxatv1dvg:/home/user/out ./
sbx delete sb_qdxatv1dvg --yes

sbx scp prints nothing on success. The -r copy brought back an out/ folder (a 23,893-byte file we had made in the sandbox with sbx exec). In your own code, end each task in a finally block:

  • await sandbox.delete() removes the sandbox.
  • await sandbox.stop() keeps its files and stops billing.

The ttl_seconds=1800 in get_sandbox is the safety net: if your process crashes, the sandbox stops itself after 30 minutes.

Use the hosted MCP server instead#

If you'd rather not write tools, point the SDK at Sandboxer's MCP server. It needs no install and no sbx binary on the host, which suits containers and serverless functions.

hosted_mcp.py
import asyncio
import os

from agents.mcp import MCPServerStreamableHttp


async def main() -> None:
    async with MCPServerStreamableHttp(
        name="sandboxer",
        params={
            "url": "https://sandboxer.com/mcp",
            "headers": {"Authorization": f"Bearer {os.environ['SANDBOXER_API_KEY']}"},
        },
        client_session_timeout_seconds=900,   # create_sandbox and long exec calls outlast the 5 s default
    ) as sandboxer:
        tools = await sandboxer.list_tools()
        print(len(tools), "tools")
        for tool in tools:
            print(f"{tool.name:16} {(tool.description or '').split('.')[0]}")


asyncio.run(main())
$ python hosted_mcp.py
18 tools
create_sandbox   Create a cloud sandbox (an Ubuntu microVM with root, Docker, git, Node and Python) and wait until it is ready
list_sandboxes   List your sandboxes with their state, URL and labels
get_sandbox      Show one sandbox: state, URL, when it stops automatically, labels and stored bytes
exec             Run a shell command (bash -c) in the sandbox
command_status   Status of a detached command: running or its exit code, and its output
kill_command     Stop a detached command
read_file        Read a file from the sandbox (up to 5 MiB)
write_file       Create or overwrite a file in the sandbox (up to 5 MiB)
host_port        Serve a port of the sandbox on a stable public HTTPS URL
…
delete_sandbox   Delete a sandbox permanently: its disk, memory and automatic snapshots go (named snapshots saved from it stay)
save_snapshot    Save the sandbox as a named snapshot to create new sandboxes from, and wait until it is saved
list_snapshots   List your named snapshots: name, version, labels, created time, stored bytes (what counts toward your storage) and disk size (logical)
delete_snapshot  Delete one named snapshot (one version, e

Pass the server to an agent with Agent(..., mcp_servers=[sandboxer]) inside the async with block. To offer only some tools, add a filter. This version lists exactly five:

from agents.mcp import create_static_tool_filter

MCPServerStreamableHttp(
    ...,
    tool_filter=create_static_tool_filter(
        allowed_tool_names=["create_sandbox", "exec", "read_file", "write_file", "stop_sandbox"]
    ),
)
# list_tools() → ['create_sandbox', 'exec', 'read_file', 'write_file', 'stop_sandbox']

Over MCP, read_file returns a file's text to the model, which is fine for small outputs. To get binary files or large results onto your disk, pair MCP with a download_file function tool from section 2, or use sbx scp after the run. For a headless agent, use a scoped API key (sbx api-key create agent --preset ci: it can create sandboxes and delete only the ones it created), so the agent can't touch the rest of your account.

FAQ#

Can I test OpenAI Agents SDK tools without an OpenAI API key?#

Yes. Call tool.on_invoke_tool(ToolContext(...), json_args) to run one tool directly. To run the full Runner loop, use ScriptedModel from agents.testing, which plays back tool calls you script. Both run your real tool code against a real sandbox.

How large a file can the agent send back?#

files.download in sandboxer-sdk returns up to 50 MiB. files.read and the MCP read_file tool return up to 5 MiB. For folders or bigger files, use sbx scp -r from the host, which copies over SSH instead of the file API.

Why does my tool error just say "An error occurred while running the tool"?#

That's the Agents SDK's default failure_error_function, which hides exception details from the model. Pass your own function to @function_tool(failure_error_function=...) to return a message the model can act on.

Does the hosted MCP server work without installing the sbx CLI?#

Yes. MCPServerStreamableHttp connects to https://sandboxer.com/mcp over HTTPS with your API key in the Authorization header. Only openai-agents needs to be installed.

What does a run cost?#

A small sandbox is $0.018/hour and a default is $0.036/hour, billed per second while running. A stopped sandbox costs nothing. Model usage is billed by OpenAI on your own key.

More guides

Top AI sandbox platforms for code execution in 2026, ranked

For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.

Read the guide
Best sandboxes for running coding agents (Claude Code, Codex, Gemini CLI) in 2026, ranked

For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.

Read the guide
Best Daytona sandbox alternatives in 2026, ranked

The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.

Read the guide
Best E2B alternatives in 2026, ranked

If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.

Read the guide