Examples
Runnable examples live in examples/. These are product patterns, not toy demos.
Use them as starting points for CI fixers, review bots, hosted agent APIs, tenant-isolated services, and multi-agent coding workflows. Every example source is embedded below.
First runs
| File | Shows |
|---|---|
examples/plain_docker_shell.py |
Shell backend in plain Docker |
examples/docker_sbx_codex.py |
Codex backend in Docker SBX |
examples/docker_sbx_opencode.py |
OpenCode backend in Docker SBX |
examples/cli_runner.py |
reusable Runner loaded by sbxa CLI |
examples/cli_runner_codex_default.py |
sbxa runner using Docker SBX Codex defaults and host-managed auth |
examples/plain_docker_shell.py
from pathlib import Path
from sbx_agents import Agent, Runner
from sbx_agents.backends import Shell
from sbx_agents.sandboxes import DockerSandbox
agent = Agent(name="python-version", backend=Shell())
result = Runner.run_sync(
agent,
sandbox=DockerSandbox(workspace=Path("."), image="python:3.12-slim"),
prompt="python --version",
)
print(result.output)
examples/docker_sbx_codex.py
from pathlib import Path
from sbx_agents import Agent, Runner
from sbx_agents.backends import Codex
from sbx_agents.sandboxes import DockerSbxSandbox
agent = Agent(
name="fix-tests",
backend=Codex(),
instructions="Fix failing tests with the smallest safe diff.",
)
result = Runner.run_sync(
agent,
sandbox=DockerSbxSandbox(workspace=Path("."), branch="agent/fix-tests"),
prompt="Fix the failing tests.",
)
print(result.output)
examples/docker_sbx_opencode.py
from pathlib import Path
from sbx_agents import Agent, Runner
from sbx_agents.backends import OpenCode
from sbx_agents.sandboxes import DockerSbxSandbox
agent = Agent(
name="reviewer",
backend=OpenCode(),
instructions="Review the repo and produce a risk-ranked plan.",
)
result = Runner.run_sync(
agent,
sandbox=DockerSbxSandbox(workspace=Path("."), branch="agent/review"),
prompt="Review this repository.",
)
print(result.output)
examples/cli_runner.py
from pathlib import Path
from sbx_agents import Agent, Runner
from sbx_agents.backends import Codex
from sbx_agents.sandboxes import DockerSbxSandbox
# This file is meant to be loaded by the `sbxa` CLI. Importing it should not run the
# agent. `sbxa run examples/cli_runner.py` loads this object and calls `run_sync()`.
#
# Docker SBX examples require login first:
# sbx login
# sbxa doctor --require docker-sbx
runner = Runner(
Agent(
name="fix-tests",
backend=Codex(
model="LiquidAI/LFM2.5-1.2B-Instruct-MLX-8bit",
model_provider="local-mlx",
extra_config={
"model_providers": {
"local-mlx": {
"name": "Local MLX",
# Inside Docker SBX, localhost is the sandbox. Use host.docker.internal
# for a model server running on your host at localhost:8080.
"base_url": "http://host.docker.internal:8080/v1",
# Codex requires an env_key even for local unauthenticated providers.
# PATH is always present and is only used to satisfy that check.
"env_key": "PATH",
"wire_api": "responses",
}
}
},
),
instructions="Fix failing tests with the smallest safe diff. Run tests before finishing.",
),
sandbox=DockerSbxSandbox(
workspace=Path("."),
name="cli-fix-tests",
branch="agent/cli-fix-tests",
# Docker SBX reaches the host via host.docker.internal, but policy logs may
# report the proxied request as localhost:8080. Allow both.
network_allow=["host.docker.internal:8080", "localhost:8080"],
),
default_prompt="Fix the failing pytest suite.",
)
examples/cli_runner_codex_default.py
from pathlib import Path
from sbx_agents import Agent, RunConfig, Runner
from sbx_agents.backends import Codex
from sbx_agents.sandboxes import DockerSbxSandbox
# CLI runner for Docker SBX + Codex defaults.
#
# This does not set a model. Codex chooses its default model. It does set the
# provider to OpenAI so stale project .codex/config.toml from other examples cannot
# accidentally point this run at a local provider.
# Codex declares OpenAI OAuth auth; Docker SBX implements it with its global
# OpenAI OAuth secret.
#
# One-time setup:
# sbx login
# sbxa doctor --require docker-sbx
#
# Run:
# sbxa run examples.cli_runner_codex_default:runner
runner = Runner(
Agent(
name="codex-default-fixer",
backend=Codex(model_provider="openai", auth="openai_oauth"),
instructions="Fix failing tests with the smallest safe diff. Run tests before finishing.",
),
sandbox=DockerSbxSandbox(
workspace=Path("."),
name="codex-default-fixer",
branch="agent/codex-default-fixer",
),
default_prompt="Fix the failing pytest suite.",
run_config=RunConfig(json_events=True),
)
Composition
| File | Shows |
|---|---|
examples/multi_step_codex.py |
diagnose -> fix -> verify with backend-native resume |
examples/multi_step_app_managed.py |
app-managed context between turns |
examples/multi_agent_same_sandbox.py |
reviewer, fixer, and verifier share one sandbox |
examples/one_agent_multiple_sandboxes_copy_files.py |
same agent across sandboxes with explicit file transfer |
examples/multi_step_codex.py
from pathlib import Path
from sbx_agents import Agent, RunConfig, Runner
from sbx_agents.backends import Codex
from sbx_agents.sandboxes import DockerSbxSandbox
agent = Agent(
name="fix-tests",
backend=Codex(model="gpt-5-codex"),
instructions=(
"You are a careful coding agent. Use the smallest safe diff. "
"Run relevant tests before finishing."
),
)
sandbox = DockerSbxSandbox(
workspace=Path("."),
name="fix-tests",
branch="agent/fix-tests",
)
# Step 1: inspect only. JSON events are opt-in; Codex may emit a thread_id.
diagnosis = Runner.run_sync(
agent,
sandbox=sandbox,
prompt="Inspect the repo and identify why tests are failing. Do not edit files yet.",
run_config=RunConfig(json_events=True),
)
session_id = diagnosis.thread_id or diagnosis.session_id
print("Diagnosis:")
print(diagnosis.output)
# Step 2: resume the backend conversation if possible.
fix = Runner.run_sync(
agent,
sandbox=sandbox,
prompt="Now implement the smallest safe fix. Do not do unrelated cleanup.",
run_config=RunConfig(
json_events=True,
resume_session_id=session_id,
),
)
session_id = fix.thread_id or fix.session_id or session_id
print("Fix output:")
print(fix.output)
print("Diff:")
print(fix.diff)
# Step 3: verify in the same conversation.
verify = Runner.run_sync(
agent,
sandbox=sandbox,
prompt="Run the relevant test suite and report the result. Fix only if needed.",
run_config=RunConfig(
json_events=True,
resume_session_id=session_id,
),
)
print("Verification:")
print(verify.output)
print("Final diff:")
print(verify.diff)
examples/multi_step_app_managed.py
from pathlib import Path
from sbx_agents import Agent, Runner
from sbx_agents.backends import Shell
from sbx_agents.sandboxes import DockerSandbox
agent = Agent(
name="shell-workflow",
backend=Shell(),
instructions="Run shell commands requested by the orchestration layer.",
)
sandbox = DockerSandbox(
workspace=Path("."),
image="python:3.12-slim",
)
# Step 1: collect context.
inspect = Runner.run_sync(
agent,
sandbox=sandbox,
prompt="python --version",
)
print("Inspect:")
print(inspect.output)
# Step 2: app carries prior output into the next prompt.
test = Runner.run_sync(
agent,
sandbox=sandbox,
prompt=f"""
Previous command output:
{inspect.output}
Now check whether pytest is installed:
python -m pytest --version
""".strip(),
)
print("Test check:")
print(test.output)
# Step 3: app decides what to do from prior result.
summary = Runner.run_sync(
agent,
sandbox=sandbox,
prompt=f"""
Summarize this workflow result in one line.
Step 1 output:
{inspect.output}
Step 2 output:
{test.output}
""".strip(),
)
print("Summary:")
print(summary.output)
examples/multi_agent_same_sandbox.py
from pathlib import Path
from sbx_agents import Agent, PermissionPolicy, Runner
from sbx_agents.backends import Codex, OpenCode, Shell
from sbx_agents.sandboxes import DockerSbxSandbox
# One sandbox config, many agents. Each run uses the same workspace and branch.
# This lets agents hand off through repo state and `git diff`.
sandbox = DockerSbxSandbox(
workspace=Path("."),
name="multi-agent-fix",
branch="agent/multi-agent-fix",
)
reviewer = Agent(
name="reviewer",
backend=OpenCode(model="anthropic/claude-sonnet-4-5"),
instructions="Review the repo. Identify likely test failures and risks. Do not edit files.",
permission=PermissionPolicy(default="ask", tools={"edit": "deny", "bash": "allow"}),
)
fixer = Agent(
name="fixer",
backend=Codex(model="gpt-5-codex"),
instructions="Implement the smallest safe fix. Do not do unrelated cleanup.",
)
verifier = Agent(
name="verifier",
backend=Shell(),
instructions="Run verification commands and report results.",
)
# Step 1: OpenCode performs a read-only review.
review = Runner.run_sync(
reviewer,
sandbox=sandbox,
prompt="Review this repository and identify the smallest useful fix plan.",
)
print("Review:")
print(review.output)
# Step 2: Codex applies the fix in the same sandbox workspace/branch.
fix = Runner.run_sync(
fixer,
sandbox=sandbox,
prompt=f"""
Use this review as context:
{review.output}
Implement the smallest safe fix now.
""".strip(),
)
print("Fix:")
print(fix.output)
print("Diff after fix:")
print(fix.diff)
# Step 3: Shell verifies the result in the same sandbox workspace/branch.
verify = Runner.run_sync(
verifier,
sandbox=sandbox,
prompt="pytest -q",
)
print("Verification:")
print(verify.output)
print("Final diff:")
print(verify.diff)
examples/one_agent_multiple_sandboxes_copy_files.py
from pathlib import Path
from tempfile import TemporaryDirectory
from sbx_agents import Agent, Runner
from sbx_agents.backends import Shell
from sbx_agents.sandboxes import DockerSandbox
agent = Agent(
name="portable-worker",
backend=Shell(),
instructions="Run requested shell commands inside whichever sandbox is provided.",
)
with TemporaryDirectory() as tmp:
root = Path(tmp)
producer_workspace = root / "producer"
consumer_workspace = root / "consumer"
producer_workspace.mkdir()
consumer_workspace.mkdir()
producer = DockerSandbox(
workspace=producer_workspace,
image="python:3.12-slim",
name="producer-sandbox",
)
consumer = DockerSandbox(
workspace=consumer_workspace,
image="python:3.12-slim",
name="consumer-sandbox",
)
# Put an input file only into the producer sandbox.
producer.put_file(
"inputs/spec.txt",
"Build a JSON artifact with status, source, and spec length.",
)
# Same agent works in sandbox A.
producer_result = Runner.run_sync(
agent,
sandbox=producer,
prompt="""
python - <<'PY'
from pathlib import Path
spec = Path('inputs/spec.txt').read_text()
Path('artifacts').mkdir(exist_ok=True)
Path('artifacts/report.json').write_text(
'{"status":"ok","source":"producer","spec_chars":%d}\n' % len(spec)
)
PY
""".strip(),
)
print("Producer output:")
print(producer_result.output)
# Copy one selected file from sandbox A to sandbox B through app code.
# This keeps transfer explicit and tenant/workspace boundaries clear.
report = producer.get_file("artifacts/report.json")
consumer.put_file("incoming/report.json", report)
# Same agent now works in sandbox B against copied file.
consumer_result = Runner.run_sync(
agent,
sandbox=consumer,
prompt="""
python - <<'PY'
import json
from pathlib import Path
report = json.loads(Path('incoming/report.json').read_text())
Path('verified.txt').write_text(f"verified {report['status']} from {report['source']}\n")
print(Path('verified.txt').read_text(), end='')
PY
""".strip(),
)
print("Consumer output:")
print(consumer_result.output)
print("Consumer files:")
print(consumer.list_files("**/*"))
verified = consumer.get_file("verified.txt")
print("Verified file:")
print(verified)
Agent config
| File | Shows |
|---|---|
examples/docker_sbx_codex_with_mcp_and_skill.py |
Codex with skill and stdio MCP |
examples/docker_sbx_opencode_with_http_mcp_and_skill.py |
OpenCode with skill and HTTP MCP |
examples/complex_codex_agent_config.py |
Codex reasoning, MCP, skills, permissions, JSON events, output schema |
examples/complex_opencode_agent_config.py |
OpenCode permissions, LSP, formatter, MCP, skills, session flags |
examples/docker_sbx_codex_with_mcp_and_skill.py
from pathlib import Path
from sbx_agents import Agent, Runner
from sbx_agents.backends import Codex
from sbx_agents.mcp import StdioMCP
from sbx_agents.sandboxes import DockerSbxSandbox
from sbx_agents.skills import Skill
agent = Agent(
name="fix-tests",
backend=Codex(
model="gpt-5-codex",
approval_policy="on-request",
sandbox_mode="workspace-write",
),
instructions="Fix failing tests with the smallest safe diff. Run tests before finishing.",
skills=[Skill.from_dir(Path(__file__).parent / "skills" / "fix-tests")],
mcp_servers=[
StdioMCP(
name="context7",
command="npx",
args=["-y", "@upstash/context7-mcp"],
)
],
)
result = Runner.run_sync(
agent,
sandbox=DockerSbxSandbox(
workspace=Path("."),
name="fix-tests",
branch="agent/fix-tests",
),
prompt="Fix the failing pytest suite.",
)
print(result.output)
print(result.diff)
examples/docker_sbx_opencode_with_http_mcp_and_skill.py
from pathlib import Path
from sbx_agents import Agent, PermissionPolicy, Runner
from sbx_agents.backends import OpenCode
from sbx_agents.mcp import HttpMCP
from sbx_agents.sandboxes import DockerSbxSandbox
from sbx_agents.skills import Skill
agent = Agent(
name="reviewer",
backend=OpenCode(
model="anthropic/claude-sonnet-4-5",
),
permission=PermissionPolicy(tools={"edit": "deny", "bash": "allow"}),
instructions="Review the repo and produce a risk-ranked plan.",
skills=[Skill.from_dir(Path(__file__).parent / "skills" / "review-risk")],
mcp_servers=[
HttpMCP(
name="docs",
url="https://mcp.example.com/mcp",
bearer_token_env_var="DOCS_MCP_TOKEN",
)
],
)
result = Runner.run_sync(
agent,
sandbox=DockerSbxSandbox(
workspace=Path("."),
name="reviewer",
branch="agent/review",
),
prompt="Review this repository for release risk.",
)
print(result.output)
examples/complex_codex_agent_config.py
from pathlib import Path
from sbx_agents import Agent, PermissionPolicy, RunConfig, Runner
from sbx_agents.backends import Codex
from sbx_agents.mcp import HttpMCP, StdioMCP
from sbx_agents.sandboxes import DockerSbxSandbox
from sbx_agents.skills import Skill
agent = Agent(
name="release-risk-fixer",
backend=Codex(
model="gpt-5-codex",
model_provider="openai",
model_reasoning_effort="high",
model_reasoning_summary="auto",
model_verbosity="medium",
approval_policy="on-request",
sandbox_mode="workspace-write",
web_search="enabled",
features={"web_search_request": True},
shell_environment_policy={
"inherit": "core",
"include_only": ["PATH", "HOME", "OPENAI_API_KEY", "GITHUB_TOKEN"],
},
extra_config={
"model_context_window": 200000,
},
),
instructions=(
"Act as a release engineer. Identify high-risk failures, fix only the smallest "
"safe issue, and return structured release risk output."
),
permission=PermissionPolicy(default="ask", tools={"bash": "allow", "edit": "ask"}),
skills=[
Skill.from_dir(Path(__file__).parent / "skills" / "fix-tests"),
Skill.from_dir(Path(__file__).parent / "skills" / "review-risk"),
],
mcp_servers=[
StdioMCP(
name="context7",
command="npx",
args=["-y", "@upstash/context7-mcp"],
env={"NODE_OPTIONS": "--max-old-space-size=2048"},
startup_timeout_sec=20,
tool_timeout_sec=60,
enabled_tools=["resolve-library-id", "get-library-docs"],
),
HttpMCP(
name="internal-docs",
url="https://mcp.example.com/mcp",
bearer_token_env_var="DOCS_MCP_TOKEN",
required=True,
),
],
)
result = Runner.run_sync(
agent,
sandbox=DockerSbxSandbox(
workspace=Path("."),
name="release-risk-fixer",
branch="agent/release-risk-fixer",
cpus=4,
memory="8g",
),
prompt="Review release risk, fix the smallest critical issue, and summarize validation.",
run_config=RunConfig(
json_events=True,
output_schema={
"type": "object",
"properties": {
"summary": {"type": "string"},
"risk": {"type": "string", "enum": ["low", "medium", "high"]},
"tests_run": {"type": "array", "items": {"type": "string"}},
"follow_ups": {"type": "array", "items": {"type": "string"}},
},
"required": ["summary", "risk", "tests_run"],
},
extra_args=["--skip-git-repo-check"],
),
)
print(result.output)
print(result.diff)
examples/complex_opencode_agent_config.py
from pathlib import Path
from sbx_agents import Agent, PermissionPolicy, RunConfig, Runner
from sbx_agents.backends import OpenCode
from sbx_agents.mcp import HttpMCP, StdioMCP
from sbx_agents.sandboxes import DockerSbxSandbox
from sbx_agents.skills import Skill
agent = Agent(
name="read-only-reviewer",
backend=OpenCode(
model="anthropic/claude-sonnet-4-5",
small_model="anthropic/claude-haiku-4-5",
agent="build",
permission={
"edit": "deny",
"bash": {
"pytest*": "allow",
"git diff*": "allow",
"*": "ask",
},
},
lsp=True,
formatter=True,
instructions=["README.md", "CONTRIBUTING.md"],
disabled_providers=["example-disabled-provider"],
snapshot=True,
share="manual",
compaction={"enabled": True, "threshold": 0.8},
),
instructions="Review the repository for release risk. Do not modify files.",
permission=PermissionPolicy(default="ask", tools={"edit": "deny", "bash": "allow"}),
skills=[Skill.from_dir(Path(__file__).parent / "skills" / "review-risk")],
mcp_servers=[
StdioMCP(
name="context7",
command="npx",
args=["-y", "@upstash/context7-mcp"],
startup_timeout_sec=20,
),
HttpMCP(
name="docs",
url="https://mcp.example.com/mcp",
headers={"X-Workspace": "release"},
),
],
)
result = Runner.run_sync(
agent,
sandbox=DockerSbxSandbox(
workspace=Path("."),
name="read-only-reviewer",
branch="agent/read-only-reviewer",
),
prompt="Review this repository and return a risk-ranked report.",
run_config=RunConfig(
json_events=True,
extra_args=["--title", "Release risk review"],
),
)
print(result.output)
Sandbox config
| File | Shows |
|---|---|
examples/complex_sandbox_setup.py |
Docker resource/env args and Docker SBX branch/resources |
examples/sandbox_file_management.py |
put_file, get_file, list_files, delete_file |
examples/plain_docker_shell_non_strict_skills.py |
skipping unsupported shell features with strict=False |
examples/complex_sandbox_setup.py
from pathlib import Path
from sbx_agents import Agent, Runner
from sbx_agents.backends import Shell
from sbx_agents.sandboxes import DockerSandbox, DockerSbxSandbox
workspace = Path(".")
agent = Agent(
name="sandbox-smoke-test",
backend=Shell(),
instructions="Run diagnostics inside the sandbox and report the environment.",
)
# Plain Docker: useful in CI or Linux environments where Docker SBX is unavailable.
# `docker_args` is the escape hatch for Docker-native settings the SDK has not modeled yet.
plain_docker = DockerSandbox(
workspace=workspace,
image="python:3.12-slim",
name="sbx-agents-smoke-test",
workdir="/workspace",
env={
"PYTHONUNBUFFERED": "1",
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
},
docker_args=[
"--cpus",
"2",
"--memory",
"4g",
"--network",
"bridge",
"--tmpfs",
"/tmp:rw,noexec,nosuid,size=512m",
"-v",
f"{(Path.home() / '.cache' / 'pip').resolve()}:/root/.cache/pip",
],
)
plain_result = Runner.run_sync(
agent,
sandbox=plain_docker,
prompt="python --version && pwd && ls",
)
print("Plain Docker output:")
print(plain_result.output)
# Docker SBX: better local isolation for real coding agents, with branch/resource controls.
docker_sbx = DockerSbxSandbox(
workspace=workspace,
name="sbx-agents-isolated-smoke-test",
branch="agent/sandbox-smoke-test",
cpus=4,
memory="8g",
remove_on_exit=True,
)
sbx_result = Runner.run_sync(
agent,
sandbox=docker_sbx,
prompt="pwd && git status --short && python --version || true",
)
print("Docker SBX output:")
print(sbx_result.output)
print("Docker SBX diff:")
print(sbx_result.diff)
examples/sandbox_file_management.py
from pathlib import Path
from sbx_agents import Agent, Runner
from sbx_agents.backends import Shell
from sbx_agents.sandboxes import DockerSandbox
sandbox = DockerSandbox(
workspace=Path("."),
image="python:3.12-slim",
)
agent = Agent(
name="file-worker",
backend=Shell(),
instructions="Read and write only requested files.",
)
# Put a specific input file into the sandbox workspace before the run.
sandbox.put_file(
"tmp/sbx-agent-spec.md",
"""
# Task Spec
Create a short JSON report at tmp/sbx-agent-report.json.
""".strip(),
)
result = Runner.run_sync(
agent,
sandbox=sandbox,
prompt="""
python - <<'PY'
from pathlib import Path
spec = Path('tmp/sbx-agent-spec.md').read_text()
Path('tmp/sbx-agent-report.json').write_text(
'{"status":"ok","spec_chars":%d}\n' % len(spec)
)
PY
""".strip(),
)
print(result.output)
# Get a specific output file back from the sandbox workspace after the run.
report = sandbox.get_file("tmp/sbx-agent-report.json")
print(report)
# List and clean specific files.
print(sandbox.list_files("tmp/sbx-agent-*"))
sandbox.delete_file("tmp/sbx-agent-spec.md")
sandbox.delete_file("tmp/sbx-agent-report.json")
examples/plain_docker_shell_non_strict_skills.py
from pathlib import Path
from sbx_agents import Agent, Runner
from sbx_agents.backends import Shell
from sbx_agents.sandboxes import DockerSandbox
from sbx_agents.skills import Skill
agent = Agent(
name="shell-test",
backend=Shell(),
skills=[Skill.from_dir(Path(__file__).parent / "skills" / "fix-tests")],
strict=False,
)
result = Runner.run_sync(
agent,
sandbox=DockerSandbox(workspace=Path("."), image="python:3.12-slim"),
prompt="python --version",
)
print(result.output)
Skills
| File | Shows |
|---|---|
examples/skills/fix-tests/SKILL.md |
portable skill with OpenCode-compatible frontmatter |
examples/skills/review-risk/SKILL.md |
review skill with risk-focused instructions |
examples/skills/fix-tests/SKILL.md
---
name: fix-tests
description: Fix failing tests with smallest safe diff
compatibility: sbx-agents
---
# Fix Tests
When fixing tests:
- Reproduce the failure first.
- Prefer the smallest safe diff.
- Run the targeted test before widening scope.
- Explain any test you could not run.
examples/skills/review-risk/SKILL.md
---
name: review-risk
description: Review code changes by release risk
compatibility: sbx-agents
---
# Risk Review
Review code changes by ranking findings from highest to lowest risk.
Focus on correctness, security, data loss, migrations, concurrency, and missing tests.
Services and CI
| File | Shows |
|---|---|
examples/fastapi_agent_service.py |
minimal FastAPI API for runs and file endpoints |
examples/fastapi_multi_tenant_isolation.py |
tenant-scoped workspaces, jobs, files, and sandbox names |
examples/github_actions_fix_tests.py |
GitHub Actions script entrypoint |
examples/github-actions/fix-tests.yml |
workflow template that runs the SDK and opens a PR |
examples/precommit_runner.py |
import-safe Runner for Git pre-commit hooks |
examples/git-hooks/pre-commit |
Git pre-commit hook template that calls sbxa |
examples/fastapi_agent_service.py
from pathlib import Path
from uuid import uuid4
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from sbx_agents import Agent, PermissionPolicy, RunConfig, Runner, RunResult
from sbx_agents.backends import Codex, OpenCode, Shell
from sbx_agents.sandboxes import DockerSandbox, DockerSbxSandbox
app = FastAPI(title="sbx-agents example")
jobs: dict[str, RunResult] = {}
class RunRequest(BaseModel):
prompt: str
workspace: Path = Path(".")
agent_backend: str = "shell"
sandbox_backend: str = "docker"
branch: str | None = None
json_events: bool = False
readonly: bool = False
docker_image: str = "python:3.12-slim"
extra_args: list[str] = Field(default_factory=list)
class RunResponse(BaseModel):
job_id: str
output: str
returncode: int
diff: str | None
command: list[str]
class FileWriteRequest(BaseModel):
workspace: Path = Path(".")
path: Path
content: str
class FileReadRequest(BaseModel):
workspace: Path = Path(".")
path: Path
class FileReadResponse(BaseModel):
path: Path
content: str
class FileListResponse(BaseModel):
files: list[Path]
@app.post("/runs", response_model=RunResponse)
def run_agent(request: RunRequest) -> RunResponse:
agent = _build_agent(request)
sandbox = _build_sandbox(request)
result = Runner.run_sync(
agent,
sandbox=sandbox,
prompt=request.prompt,
run_config=RunConfig(
json_events=request.json_events,
extra_args=request.extra_args,
),
)
job_id = str(uuid4())
jobs[job_id] = result
return RunResponse(
job_id=job_id,
output=result.output,
returncode=result.returncode,
diff=result.diff,
command=result.command,
)
@app.get("/runs/{job_id}", response_model=RunResult)
def get_run(job_id: str) -> RunResult:
result = jobs.get(job_id)
if result is None:
raise HTTPException(status_code=404, detail="Run not found")
return result
@app.post("/files")
def put_file(request: FileWriteRequest) -> dict[str, str]:
sandbox = DockerSandbox(workspace=request.workspace)
path = sandbox.put_file(request.path, request.content)
return {"path": str(path)}
@app.post("/files/read", response_model=FileReadResponse)
def read_file(request: FileReadRequest) -> FileReadResponse:
sandbox = DockerSandbox(workspace=request.workspace)
try:
content = sandbox.get_file(request.path)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail="File not found") from exc
return FileReadResponse(path=request.path, content=content)
@app.get("/files", response_model=FileListResponse)
def list_files(workspace: Path = Path("."), pattern: str = "**/*") -> FileListResponse:
sandbox = DockerSandbox(workspace=workspace)
return FileListResponse(files=sandbox.list_files(pattern))
@app.delete("/files")
def delete_file(workspace: Path, path: Path) -> dict[str, str]:
sandbox = DockerSandbox(workspace=workspace)
try:
sandbox.delete_file(path)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail="File not found") from exc
return {"deleted": str(path)}
def _build_agent(request: RunRequest) -> Agent:
permission = None
if request.readonly:
permission = PermissionPolicy(default="ask", tools={"edit": "deny", "bash": "allow"})
if request.agent_backend == "codex":
backend = Codex(model="gpt-5-codex")
elif request.agent_backend == "opencode":
backend = OpenCode(model="anthropic/claude-sonnet-4-5")
elif request.agent_backend == "shell":
backend = Shell()
else:
raise HTTPException(status_code=400, detail="Unsupported agent_backend")
return Agent(
name=f"api-{request.agent_backend}",
backend=backend,
instructions="Run inside sandbox. Keep changes focused and report what happened.",
permission=permission,
)
def _build_sandbox(request: RunRequest) -> DockerSandbox | DockerSbxSandbox:
if request.sandbox_backend == "docker":
return DockerSandbox(
workspace=request.workspace,
image=request.docker_image,
)
if request.sandbox_backend == "docker_sbx":
return DockerSbxSandbox(
workspace=request.workspace,
branch=request.branch,
)
raise HTTPException(status_code=400, detail="Unsupported sandbox_backend")
# Run:
# uv add --dev fastapi uvicorn
# uv run uvicorn examples.fastapi_agent_service:app --reload
#
# Example request:
# curl -X POST http://127.0.0.1:8000/runs \
# -H 'content-type: application/json' \
# -d '{"prompt":"python --version","agent_backend":"shell","sandbox_backend":"docker"}'
#
# Write a file:
# curl -X POST http://127.0.0.1:8000/files \
# -H 'content-type: application/json' \
# -d '{"path":"tmp/spec.md","content":"# Spec"}'
#
# Read a file:
# curl -X POST http://127.0.0.1:8000/files/read \
# -H 'content-type: application/json' \
# -d '{"path":"tmp/spec.md"}'
examples/fastapi_multi_tenant_isolation.py
from __future__ import annotations
import re
import shutil
import tempfile
from pathlib import Path
from typing import Annotated
from uuid import uuid4
from fastapi import Depends, FastAPI, Header, HTTPException
from pydantic import BaseModel, Field
from sbx_agents import Agent, RunConfig, Runner, RunResult
from sbx_agents.backends import Codex, OpenCode, Shell
from sbx_agents.sandboxes import DockerSandbox, DockerSbxSandbox
app = FastAPI(title="sbx-agents multi-tenant isolation example")
# Production services should use durable storage and authenticated tenant identity.
# This example keeps per-tenant workspaces under one temp root for clarity.
TENANT_ROOT = Path(tempfile.gettempdir()) / "sbx-agents-tenants"
jobs: dict[tuple[str, str], TenantJob] = {}
class TenantContext(BaseModel):
tenant_id: str
user_id: str
root: Path
class TenantJob(BaseModel):
tenant_id: str
user_id: str
run_id: str
workspace: Path
result: RunResult
class TenantRunRequest(BaseModel):
prompt: str
agent_backend: str = "shell"
sandbox_backend: str = "docker"
docker_image: str = "python:3.12-slim"
json_events: bool = False
files: dict[str, str] = Field(default_factory=dict)
class TenantRunResponse(BaseModel):
tenant_id: str
run_id: str
output: str
returncode: int
diff: str | None
class TenantFileRequest(BaseModel):
path: Path
content: str
class TenantFileResponse(BaseModel):
path: Path
content: str
@app.post("/runs", response_model=TenantRunResponse)
def run_agent(
request: TenantRunRequest,
tenant: Annotated[TenantContext, Depends(require_tenant)],
) -> TenantRunResponse:
run_id = uuid4().hex
workspace = tenant.root / run_id
workspace.mkdir(parents=True, exist_ok=False)
sandbox = _build_sandbox(request, tenant, run_id, workspace)
for path, content in request.files.items():
sandbox.put_file(path, content)
result = Runner.run_sync(
_build_agent(request, tenant),
sandbox=sandbox,
prompt=request.prompt,
run_config=RunConfig(json_events=request.json_events),
)
jobs[(tenant.tenant_id, run_id)] = TenantJob(
tenant_id=tenant.tenant_id,
user_id=tenant.user_id,
run_id=run_id,
workspace=workspace,
result=result,
)
return TenantRunResponse(
tenant_id=tenant.tenant_id,
run_id=run_id,
output=result.output,
returncode=result.returncode,
diff=result.diff,
)
@app.get("/runs/{run_id}", response_model=RunResult)
def get_run(
run_id: str,
tenant: Annotated[TenantContext, Depends(require_tenant)],
) -> RunResult:
return _tenant_job(tenant, run_id).result
@app.post("/runs/{run_id}/files", response_model=dict[str, str])
def put_file(
run_id: str,
request: TenantFileRequest,
tenant: Annotated[TenantContext, Depends(require_tenant)],
) -> dict[str, str]:
job = _tenant_job(tenant, run_id)
sandbox = DockerSandbox(workspace=job.workspace)
sandbox.put_file(request.path, request.content)
return {"path": str(request.path)}
@app.post("/runs/{run_id}/files/read", response_model=TenantFileResponse)
def read_file(
run_id: str,
request: TenantFileRequest,
tenant: Annotated[TenantContext, Depends(require_tenant)],
) -> TenantFileResponse:
job = _tenant_job(tenant, run_id)
sandbox = DockerSandbox(workspace=job.workspace)
try:
content = sandbox.get_file(request.path)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail="File not found") from exc
return TenantFileResponse(path=request.path, content=content)
@app.delete("/runs/{run_id}", response_model=dict[str, str])
def delete_run(
run_id: str,
tenant: Annotated[TenantContext, Depends(require_tenant)],
) -> dict[str, str]:
job = _tenant_job(tenant, run_id)
jobs.pop((tenant.tenant_id, run_id), None)
shutil.rmtree(job.workspace, ignore_errors=True)
return {"deleted": run_id}
def require_tenant(
x_tenant_id: Annotated[str, Header(alias="X-Tenant-ID")],
x_user_id: Annotated[str, Header(alias="X-User-ID")],
) -> TenantContext:
tenant_id = _safe_id(x_tenant_id, "tenant")
user_id = _safe_id(x_user_id, "user")
root = TENANT_ROOT / tenant_id
root.mkdir(parents=True, exist_ok=True)
return TenantContext(tenant_id=tenant_id, user_id=user_id, root=root)
def _build_agent(request: TenantRunRequest, tenant: TenantContext) -> Agent:
if request.agent_backend == "shell":
backend = Shell()
elif request.agent_backend == "codex":
backend = Codex(model="gpt-5-codex")
elif request.agent_backend == "opencode":
backend = OpenCode(model="anthropic/claude-sonnet-4-5")
else:
raise HTTPException(status_code=400, detail="Unsupported agent_backend")
return Agent(
name=f"tenant-{tenant.tenant_id}-{request.agent_backend}",
backend=backend,
instructions=(
"You are running in an isolated tenant workspace. "
"Never read or write outside the workspace."
),
)
def _build_sandbox(
request: TenantRunRequest,
tenant: TenantContext,
run_id: str,
workspace: Path,
) -> DockerSandbox | DockerSbxSandbox:
name = f"sbx-{tenant.tenant_id}-{run_id[:12]}"
if request.sandbox_backend == "docker":
return DockerSandbox(
workspace=workspace,
image=request.docker_image,
name=name,
docker_args=["--network", "none"],
)
if request.sandbox_backend == "docker_sbx":
return DockerSbxSandbox(
workspace=workspace,
name=name,
branch=f"agent/{tenant.tenant_id}/{run_id}",
)
raise HTTPException(status_code=400, detail="Unsupported sandbox_backend")
def _tenant_job(tenant: TenantContext, run_id: str) -> TenantJob:
job = jobs.get((tenant.tenant_id, run_id))
if job is None:
raise HTTPException(status_code=404, detail="Run not found for tenant")
return job
def _safe_id(value: str, label: str) -> str:
if not re.fullmatch(r"[a-zA-Z0-9_-]{1,64}", value):
raise HTTPException(status_code=400, detail=f"Invalid {label} id")
return value
# Run:
# uv add --dev fastapi uvicorn
# uv run uvicorn examples.fastapi_multi_tenant_isolation:app --reload
#
# Tenant A:
# curl -X POST http://127.0.0.1:8000/runs \
# -H 'content-type: application/json' \
# -H 'X-Tenant-ID: tenant-a' \
# -H 'X-User-ID: alice' \
# -d '{"prompt":"cat spec.md","files":{"spec.md":"tenant A spec"}}'
#
# Tenant B cannot read Tenant A run, even if it guesses run_id:
# curl http://127.0.0.1:8000/runs/<tenant-a-run-id> \
# -H 'X-Tenant-ID: tenant-b' \
# -H 'X-User-ID: bob'
examples/github_actions_fix_tests.py
import os
from pathlib import Path
from sbx_agents import Agent, PermissionPolicy, RunConfig, Runner
from sbx_agents.backends import Codex, OpenCode, Shell
from sbx_agents.sandboxes import DockerSandbox, DockerSbxSandbox
def main() -> None:
workspace = Path(os.environ.get("GITHUB_WORKSPACE", "."))
prompt = os.environ.get("SBX_AGENT_PROMPT", "Run the test suite and fix failures.")
agent = Agent(
name="github-actions-agent",
backend=_backend(),
instructions=(
"You are running in GitHub Actions. Keep diffs small. "
"Run relevant tests before finishing."
),
permission=PermissionPolicy(default="ask", tools={"bash": "allow"}),
)
result = Runner.run_sync(
agent,
sandbox=_sandbox(workspace),
prompt=prompt,
run_config=RunConfig(json_events=_bool_env("SBX_JSON_EVENTS")),
)
print(result.output)
if result.diff:
print("\n--- diff ---")
print(result.diff)
summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
if summary_path:
Path(summary_path).write_text(_summary(result.output, result.diff), encoding="utf-8")
if result.returncode != 0:
raise SystemExit(result.returncode)
def _backend() -> Shell | Codex | OpenCode:
backend = os.environ.get("SBX_AGENT_BACKEND", "shell")
if backend == "shell":
return Shell()
if backend == "codex":
return Codex(model=os.environ.get("SBX_CODEX_MODEL"))
if backend == "opencode":
return OpenCode(model=os.environ.get("SBX_OPENCODE_MODEL"))
raise ValueError(f"Unsupported SBX_AGENT_BACKEND: {backend}")
def _sandbox(workspace: Path) -> DockerSandbox | DockerSbxSandbox:
sandbox = os.environ.get("SBX_SANDBOX_BACKEND", "docker")
if sandbox == "docker":
return DockerSandbox(
workspace=workspace,
image=os.environ.get("SBX_DOCKER_IMAGE", "python:3.12-slim"),
)
if sandbox == "docker_sbx":
return DockerSbxSandbox(
workspace=workspace,
name="github-actions-agent",
branch=os.environ.get("SBX_BRANCH"),
)
raise ValueError(f"Unsupported SBX_SANDBOX_BACKEND: {sandbox}")
def _summary(output: str, diff: str | None) -> str:
body = ["# sbx-agents run", "", "## Output", "", "```", output, "```"]
if diff:
body.extend(["", "## Diff", "", "```diff", diff, "```"])
return "\n".join(body) + "\n"
def _bool_env(name: str) -> bool:
return os.environ.get(name, "").lower() in {"1", "true", "yes", "on"}
if __name__ == "__main__":
main()
examples/github-actions/fix-tests.yml
name: sbx-agents fix tests
on:
workflow_dispatch:
inputs:
prompt:
description: Agent prompt
required: true
default: Run pytest and fix failures with the smallest safe diff.
agent_backend:
description: shell, codex, or opencode
required: true
default: shell
permissions:
contents: write
pull-requests: write
jobs:
agent:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: astral-sh/setup-uv@v5
- name: Install project
run: uv sync
- name: Run sandboxed agent
env:
SBX_AGENT_PROMPT: ${{ inputs.prompt }}
SBX_AGENT_BACKEND: ${{ inputs.agent_backend }}
SBX_SANDBOX_BACKEND: docker
SBX_DOCKER_IMAGE: python:3.12-slim
SBX_JSON_EVENTS: "true"
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: uv run python examples/github_actions_fix_tests.py
- name: Show diff
run: git diff --stat && git diff
- name: Create pull request
if: ${{ github.event_name == 'workflow_dispatch' }}
uses: peter-evans/create-pull-request@v6
with:
branch: agent/fix-tests-${{ github.run_id }}
title: Fix tests with sbx-agents
body: Generated by `sbx-agents` GitHub Actions example.
commit-message: Fix tests with sbx-agents
examples/precommit_runner.py
from pathlib import Path
from sbx_agents import Agent, Runner
from sbx_agents.backends import Shell
from sbx_agents.sandboxes import DockerSandbox
# Import-safe runner for a Git pre-commit hook.
# The hook calls this through `sbxa run examples/precommit_runner.py`.
# Keep pre-commit runners deterministic and non-mutating; hooks should fail fast.
runner = Runner(
Agent(
name="precommit-checks",
backend=Shell(),
instructions="Run deterministic pre-commit checks. Do not modify files.",
),
sandbox=DockerSandbox(
workspace=Path("."),
image="python:3.12-slim",
name="sbx-agents-precommit",
),
default_prompt="python -m compileall .",
)
examples/git-hooks/pre-commit
#!/bin/sh
set -eu
# Example Git pre-commit hook for sbx-agents.
# Install:
# cp examples/git-hooks/pre-commit .git/hooks/pre-commit
# chmod +x .git/hooks/pre-commit
#
# This hook runs a Python-defined Runner through `sbxa`. It only runs when staged
# Python files exist, and it blocks the commit if the sandboxed check fails.
if ! command -v sbxa >/dev/null 2>&1; then
echo "sbxa not found. Install with: uv tool install -e ."
exit 1
fi
if ! git diff --cached --name-only -- '*.py' | grep -q .; then
exit 0
fi
echo "Running sbx-agents pre-commit checks..."
sbxa run examples/precommit_runner.py
How to run examples
Most examples are scripts:
$ uv add sbx-agents
$ uv run python examples/plain_docker_shell.py
The CLI runner example is import-safe and runs through sbxa:
$ sbx login
$ sbxa doctor --require docker-sbx
$ sbxa run examples/cli_runner.py
$ sbxa run examples/cli_runner.py "Only fix lint failures."
Use Codex defaults and Docker SBX OAuth auth:
$ sbxa run examples.cli_runner_codex_default:runner
The runner declares auth on the Codex backend. Docker SBX adds required OpenAI network policy automatically:
backend=Codex(model_provider="openai", auth="openai_oauth")
FastAPI examples require FastAPI and Uvicorn:
$ uv add sbx-agents fastapi uvicorn
$ uv run uvicorn examples.fastapi_agent_service:app --reload
Docker SBX examples require sbx and agent credentials.
Install the pre-commit hook example:
$ cp examples/git-hooks/pre-commit .git/hooks/pre-commit
$ chmod +x .git/hooks/pre-commit
Next steps
- Get started — run the first example.
- Customize — understand complex config examples.
- FastAPI services — build service integrations.