# Openapi Spec Conventions

> 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.

- Skill: `earayu/openapi-spec-conventions` (Agent Skill)
- Install (CLI): `npx skillmds@latest add earayu/openapi-spec-conventions`
- Raw SKILL.md: https://api.skillmd.com/api/skills/earayu/openapi-spec-conventions/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: earayu (https://skillmd.com/u/earayu)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/earayu/openapi-spec-conventions

---


# 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:

```bash
# Fetch the updated spec from a running sandbox instance, then:
cp /path/to/new/sandbox_openapi.json scripts/sandbox_openapi_base.json
# Remove the /terminal route (HTML page, not REST API):
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

1. Add the FastAPI route in `treadstone/api/<module>.py` with a `tags=["tag-name"]`.
2. Assign a clear `operationId` via the function name (FastAPI generates it as
   `{tag}-{function_name}` using `custom_generate_unique_id` in `main.py`).
3. Run `make gen-openapi` to regenerate both `openapi.json` and `openapi-public.json`.
4. If the web app needs the new types, run `make gen-web-types`.
5. If the SDK needs new methods, run `make gen-sdk-python`.
6. 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`:

```python
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`.

```bash
make gen-openapi        # Regenerate openapi.json + openapi-public.json
make gen-sdk-python     # Regenerate sdk/python/ from openapi-public.json
make gen-clients        # Both gen-web-types + gen-sdk-python
```

**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

- [ ] Route has correct `tags=[...]` (drives SDK method names)
- [ ] Function name is descriptive (drives SDK method names)
- [ ] Admin/audit/internal routes use `HIDDEN_FROM_PUBLIC_PATH_PREFIXES` or `include_in_schema=False`
- [ ] Audit log coverage added for control-plane changes (per AGENTS.md)
- [ ] `make gen-openapi` run after any route changes
- [ ] `make gen-sdk-python` run if SDK consumers need new methods
- [ ] `_public_openapi()` cache cleared (API restart) if sandbox spec snapshot updated
- [ ] Two-commit pattern used if SDK diff is large

