OpenAI Agents SDK
Give an agent built with the OpenAI Agents SDK a Linux machine of its own. Three function tools run commands and read and write files in a froog sandbox, a full Ubuntu VM with sudo, Docker and internet, so the code the model writes never runs on your server.
Install
Python 3.10 or newer. The froog SDK is one file with no dependencies: download it next to your script, as the SDK docs say.
$ python3 -m venv .venv && source .venv/bin/activate
$ pip install openai-agents
$ curl -fsSLO https://froog.tech/sdk/froog.py # the froog client: one file, standard library only
Both keys come from the environment. Create a froog API key in the dashboard, or with the CLI.
$ export OPENAI_API_KEY=... # your model provider's key
$ export FROOG_API_KEY=froog_... # dashboard, API keys; or: froog api-key create agents --preset ci
A complete example
Save as agent.py next to froog.py and run python agent.py. The sandbox's name travels in the run context, which the SDK passes to tools but never shows the model.
from dataclasses import dataclass
from agents import Agent, RunContextWrapper, Runner
from agents.decorators import tool
from froog import Froog, FroogError
froog = Froog() # reads FROOG_API_KEY from the environment
LIMIT = 20_000 # characters of output handed back to the model
def open_sandbox(name: str) -> str:
"""Make sure the sandbox `name` is running: create it, or start it if it was stopped."""
try:
sb = froog.get_sandbox(name)
except FroogError as e:
if e.status != 404:
raise
froog.create_sandboxes({
"name": name,
"size": "small", # 2 vCPU, 4 GB
"ttl_secs": 1800, # auto-stop 30 minutes after it starts; the disk is kept
"environment": False, # none of your account's variables or files
"labels": {"app": "openai-agents-demo"},
})
sb = {"state": "creating"}
if sb["state"] == "stopping":
froog.wait_until(name, "stopped")
sb["state"] = "stopped"
if sb["state"] == "stopped":
froog.start_sandbox(name, {})
froog.wait_until(name, "running")
return name
def clip(text: str) -> str:
return text if len(text) <= LIMIT else "[...cut...]\n" + text[-LIMIT:]
@dataclass
class Conversation:
sandbox: str # the froog sandbox that belongs to this conversation
@tool
def run_command(ctx: RunContextWrapper[Conversation], command: str, timeout_secs: int = 300) -> str:
"""Run a bash command in your Linux sandbox (Ubuntu 24.04, passwordless sudo, Docker, internet).
The working directory is /home/user. Returns the exit code, stdout and stderr.
Args:
command: The bash command to run, e.g. "python3 app.py" or "pip install requests".
timeout_secs: Seconds before the command is killed (1 to 3600).
"""
r = froog.exec(ctx.context.sandbox, {"command": command, "timeout_secs": timeout_secs})
out = f"exit code {r['exit_code']}" + (" (timed out)" if r.get("timed_out") else "")
if r["stdout"]:
out += "\nstdout:\n" + clip(r["stdout"])
if r["stderr"]:
out += "\nstderr:\n" + clip(r["stderr"])
return out
@tool
def write_file(ctx: RunContextWrapper[Conversation], path: str, content: str) -> str:
"""Create or overwrite a text file in the sandbox. Parent folders are created.
Args:
path: Absolute, or relative to /home/user.
content: The whole file.
"""
froog.write_file(ctx.context.sandbox, content.encode(), path)
return f"wrote {len(content)} characters to {path}"
@tool
def read_file(ctx: RunContextWrapper[Conversation], path: str) -> str:
"""Read a text file from the sandbox.
Args:
path: Absolute, or relative to /home/user.
"""
return clip(froog.read_file(ctx.context.sandbox, path).decode("utf-8", errors="replace"))
agent = Agent[Conversation](
name="Coder",
instructions="You have a Linux sandbox. Use your tools to write and run code, and check results by running them.",
tools=[run_command, write_file, read_file],
)
def main() -> None:
convo = Conversation(sandbox=open_sandbox("chat-demo")) # one sandbox per conversation
try:
result = Runner.run_sync(
agent,
"Write primes.py that prints the first 20 primes, run it, and tell me the output.",
context=convo,
max_turns=20,
)
print(result.final_output)
finally:
froog.delete_sandbox(convo.sandbox) # or froog.stop_sandbox(...) to keep the disk
if __name__ == "__main__":
main()
How it works
open_sandboxgets the conversation's sandbox running: it creates it (asmallUbuntu VM, ready in under a minute), or starts it if it was stopped, and waits forrunning.- Three tools bound to that sandbox: run a command (
POST /v1/sandboxes/{name}/exec, up to an hour, output and exit code back), write a file and read a file (PUTandGET /v1/sandboxes/{name}/files/content). Output is cut to the last 20,000 characters so a noisy build doesn't fill the context window. - Cleanup in
finally: the example deletes the sandbox. In a real app, stop it or let its auto-stop timer stop it (see the notes).
Notes
- One sandbox per conversation or user. Name it after the conversation, thread or user id (
chat-42,user-7: 1–40 lowercase letters, digits and dashes) so every turn lands in the same machine with the same files.open_sandboxabove creates it the first time and starts it again if it was stopped. Labels ("labels": {"user": "7"}) let you find a user's sandboxes later withGET /v1/sandboxes?label=user=7. - Stop instead of delete when the user may come back. The example's
ttl_secs: 1800stops the sandbox 30 minutes after it starts, busy or not (PATCHa newttl_secsto push it back while the conversation goes on). Its disk is kept for 14 days, a start takes about 15–30 seconds (about a second within 10 minutes of the stop), and installed packages and files are still there. Delete it when the conversation is over for good. - Cost. Running time is billed per second, only while the sandbox runs:
small(2 vCPU, 4 GB) is $0.0137 an hour,default(4 vCPU, 8 GB) $0.0274. A 10-minute conversation onsmallcosts about a quarter of a cent; the idle time before the TTL stops it counts as running time, so pick a TTL that matches how long users pause. A stopped sandbox costs no running time. See pricing. - Templates. Install what your agent needs once (Python packages, a repo, a database), then save it:
froog snapshot my-box --name agent-base. Create each conversation's sandbox with"from": "agent-base"and it starts with all of it, instead of the agent spending its first turns onpip install. See snapshots. - Keep your secrets out.
"environment": falsestarts the sandbox without your account's environment variables, repos and secret files, since the model runs whatever it likes in there. Pass only what that sandbox needs inenv. Use a scoped API key for your backend (froog api-key create agents --preset cican only touch the sandboxes it created) and never send it to a browser. See API keys. - More tools. The same pattern covers the rest of the API:
POST /sandboxes/{name}/urls(host_port) gives a running web server a public URL the agent can return to the user,start_jobruns servers and long builds in the background,exec_streamstreams output as it arrives, andfork_sandboxcopies a sandbox to try several approaches at once. Every call is in the HTTP API. - Async apps. In an async server, use
await Runner.run(...), make the toolsasync defand call froog throughasyncio.to_thread, since the froog SDK is synchronous. - Sessions. If you keep conversation history with the SDK's sessions, use the same id for the session and the sandbox name.
- Sandbox agents. The Agents SDK also has
SandboxAgentwith clients for some sandbox providers. froog doesn't ship a client for it yet, so function tools, as above, are the way to use froog today.
Questions
Why not let the agent run code on my own server?
Model-written code can delete files, read your secrets or mine crypto. A froog sandbox is a separate KVM virtual machine with its own kernel and disk, so whatever runs there can't touch your server, and you throw it away when you're done.
Does the sandbox keep its files between turns?
Yes. Tools act on the same machine for the whole conversation, and a stopped sandbox keeps its disk for 14 days, so a user who comes back finds their files and installed packages.
7 days free: 25 hours of running time, 2 sandboxes at once. Then from $0.0137 an hour, billed per second.