| name | openapi-spec-conventions |
| description | Treadstone OpenAPI spec architecture, SDK generation, and sandbox proxy path conventions. Use whenever adding or modifying API routes, changing OpenAPI tags/operationIds, regenerating the Python SDK, updating sandbox runtime paths in docs, or working with treadstone/openapi_spec.py, scripts/export_openapi.py, or scripts/sandbox_openapi_base.json. Also use when someone asks how the OpenAPI docs work, why sandbox routes don't appear in the SDK, or what "three-tier spec" means. |
OpenAPI Spec Conventions
Treadstone's API surface is published through three distinct OpenAPI artifacts — each
serving a different consumer. Understanding which artifact does what prevents
accidentally breaking the SDK or the docs.
The Three-Tier Spec
FastAPI routes
│
▼
build_full_openapi_spec(app) ← treadstone/openapi_spec.py
│
├─► openapi.json ← Full spec (admin + audit + control plane)
│ │ Used by: make gen-web-types
│ │ Contains: ALL routes
│ │
│ └─► filter_public_openapi()
│ │
│ └─► openapi-public.json ← SDK spec (control plane only)
│ │ Used by: make gen-sdk-python
│ │ Excludes: /v1/admin, /v1/audit
│ │ Excludes: sandbox runtime paths
│
└─► merge_sandbox_paths() ← treadstone/openapi_spec.py
│
└─► Runtime GET /openapi.json ← Swagger UI docs
│ Used by: /docs page
│ = public spec + sandbox proxy paths
│ Excludes: /v1/admin, /v1/audit
└─► Displayed as "Sandbox: shell", "Sandbox: file" tag groups
Tier 1 — openapi.json (full, exported)
Generated by make gen-openapi → scripts/export_openapi.py.
Contains every route including admin and audit.
Used only to generate web/src/api/schema.d.ts via make gen-web-types.
Never served at runtime. Gitignored.
Tier 2 — openapi-public.json (SDK source)
Derived from Tier 1 by filter_public_openapi(), which drops all paths under
/v1/admin and /v1/audit. Sandbox runtime proxy paths are not merged here —
the SDK stays focused on the control plane.
Source for make gen-sdk-python → sdk/python/. Gitignored.
Tier 3 — Runtime /openapi.json (Swagger UI)
Served by _public_openapi() in treadstone/main.py.
Starts from Tier 2, then merges sandbox runtime paths via merge_sandbox_paths().
This is what users see at /docs.
Sandbox Runtime Paths
Why they exist in docs but not in the SDK
The real proxy implementation (treadstone/api/sandbox_proxy.py) uses
include_in_schema=False — it is a transparent HTTP forwarder, not a typed API
endpoint, so it intentionally produces no OpenAPI schema.
To help developers understand the sandbox's capabilities, merge_sandbox_paths()
reads a static snapshot of the sandbox's own OpenAPI spec and injects those paths
into the runtime docs at the correct proxy prefix.
The Python SDK does not include these paths. Data-plane operations are meant to
be called via agent_sandbox.Sandbox(base_url=sandbox.urls.proxy), not through
generated Treadstone SDK methods.
How merge_sandbox_paths() works
Source file: treadstone/openapi_spec.py
scripts/sandbox_openapi_base.json ← static snapshot of sandbox internal OpenAPI
│
├─ rename conflicting schemas:
│ Response → SandboxApiResponse
│ ValidationError → SandboxValidationError
│ HTTPValidationError → SandboxHTTPValidationError
│
├─ prefix every path:
│ /v1/shell/exec → /v1/sandboxes/{sandbox_id}/proxy/v1/shell/exec
│
├─ inject sandbox_id path parameter into every operation
│
└─ retag operations:
["shell"] → ["Sandbox: shell"]
["file"] → ["Sandbox: file"]
...
Called from: main.py → _public_openapi() only.
Not called from export_openapi.py (SDK generation must stay clean).
Updating the sandbox spec snapshot
When the sandbox runtime adds new routes or changes its schema, update the snapshot:
cp /path/to/new/sandbox_openapi.json scripts/sandbox_openapi_base.json
python3 -c "
import json
with open('scripts/sandbox_openapi_base.json') as f: s = json.load(f)
s['paths'].pop('/terminal', None)
with open('scripts/sandbox_openapi_base.json', 'w') as f: json.dump(s, f, indent=2)
"
Then restart the API server — _public_openapi() caches on first call, so a restart
is required for the updated spec to appear in /docs.
Adding New API Routes
Adding a Treadstone control-plane route
- Add the FastAPI route in
treadstone/api/<module>.py with a tags=["tag-name"].
- Assign a clear
operationId via the function name (FastAPI generates it as
{tag}-{function_name} using custom_generate_unique_id in main.py).
- Run
make gen-openapi to regenerate both openapi.json and openapi-public.json.
- If the web app needs the new types, run
make gen-web-types.
- If the SDK needs new methods, run
make gen-sdk-python.
- Check for the
@audit_log decorator — changes to auth, admin, sandbox lifecycle,
or API key management require audit log coverage (see AGENTS.md).
Two-commit pattern for large SDK diffs (see AGENTS.md → OpenAPI / SDK Generation):
- Commit 1: source changes (
treadstone/, tests/, alembic/, web/src/api/schema.d.ts)
- Commit 2:
chore: regenerate Python SDK from OpenAPI — sdk/python/ only
Hiding a route from the public SDK (admin / audit style)
Add the path prefix to HIDDEN_FROM_PUBLIC_PATH_PREFIXES in treadstone/openapi_spec.py:
HIDDEN_FROM_PUBLIC_PATH_PREFIXES: tuple[str, ...] = ("/v1/admin", "/v1/audit")
Routes matching these prefixes appear in openapi.json (web types) but are stripped
from openapi-public.json (SDK) and from the runtime /openapi.json (Swagger UI).
Hiding a route from ALL OpenAPI output (proxy style)
Use include_in_schema=False on the FastAPI route decorator. This is appropriate
for transparent proxies or internal endpoints that have no meaningful schema.
SDK Generation
The Python SDK lives in sdk/python/ and is generated by openapi-python-client
from openapi-public.json.
make gen-openapi
make gen-sdk-python
make gen-clients
SDK method naming derives from tags[0] and the FastAPI function name.
Example: POST /v1/sandboxes with tags=["sandboxes"] and function create_sandbox
→ SDK method sandboxes_create_sandbox.sync(...).
Keeping tags consistent and function names descriptive is important for the SDK's
usability.
Control Plane vs Data Plane (Examples Pattern)
The examples/ directory illustrates the two-plane architecture:
Control plane → treadstone_sdk.AuthenticatedClient
Manages: sandbox lifecycle, templates, API keys
Data plane → agent_sandbox.Sandbox(base_url=sandbox.urls.proxy)
Operates inside: shell, file, browser, jupyter, mcp
The Treadstone Python SDK intentionally does not include data-plane methods.
Users connect the two planes by extracting sandbox_detail.urls.proxy from the
control plane and passing it as base_url to agent_sandbox.Sandbox.
See examples/data_plane/01_agent_sandbox_runtime.py for the authoritative connection pattern.
Key Files
| File | Purpose |
|---|
treadstone/openapi_spec.py | build_full_openapi_spec, filter_public_openapi, merge_sandbox_paths |
treadstone/main.py | _public_openapi() — runtime spec served at /openapi.json |
scripts/export_openapi.py | Exports openapi.json + openapi-public.json for SDK/web generation |
scripts/sandbox_openapi_base.json | Static snapshot of sandbox internal OpenAPI (no /terminal) |
openapi-client-config.yaml | SDK generator config (package name overrides, post-gen hooks) |
sdk/python/ | Generated Python SDK — do not hand-edit |
Checklist: When touching API or OpenAPI