# Runbox API — Code-Interpreter Sandboxes > Stateful Python code-execution sandboxes for AI agents. Upload a user's data file, > run pandas/matplotlib code with Jupyter semantics, get charts/files back as public > URLs (never base64). Sessions persist variables between runs, one session per > conversation. Base URL: https://runbox.api.insyde.one ## Authentication Every route except `GET /health` and artifact downloads (`GET /a/…`) requires: Authorization: Bearer {tenant}:{hash} (e.g. cnn:55abc47caf008d347a51) `?key={token}` also works as a query-string fallback. Tokens are issued per tenant by the operator. A token only ever sees its own tenant's sessions — anything else returns a uniform 404. ## Intended agent flow 1. `POST /sessions` once per conversation → keep the `sessionId`. 2. `POST /sessions/{id}/files` for each user attachment → note the returned `path`. 3. `POST /sessions/{id}/run` per turn. Variables persist between runs (`df` defined in run 1 is available in run 2). 4. Show the user any `artifacts[].url` (images render directly in ``). 5. `DELETE /sessions/{id}` when the conversation ends (optional — sessions self-expire). ## Endpoints ### POST /sessions Create a sandbox session. Request body (all optional): { "conversationId": "conv-123", "idleTtlSec": 900, "maxLifetimeSec": 3600 } Response 201: { "sessionId": "mdl_sb-abc123…", "expiresAt": "2026-06-04T21:06:04.274Z" } - `idleTtlSec` (60–300, default 120): session dies after this long WITHOUT a run. It is a sliding window — every run renews it — so short back-and-forth keeps the session warm. DELETE the session when the conversation ends so the sandbox stops billing immediately. - `maxLifetimeSec` (300–7200, default 3600): hard ceiling. - 409 if the tenant already has too many live sessions (default 5). ### POST /sessions/{id}/files Put a file into the sandbox filesystem. Two forms: - multipart/form-data with a `file` field, or - JSON `{ "url": "https://…/data.xlsx", "name": "data.xlsx" }` (server-side fetch). Response: `{ "path": "/work/data.csv" }` — use this exact path in your code. Max 25 MB (413 above that). ### POST /sessions/{id}/run Execute Python in the session. Request: { "code": "import pandas as pd\n…", "language": "python", "timeoutMs": 30000 } - `timeoutMs` 1000–120000, default 30000. `language` must be `"python"`. Response 200 (ALWAYS 200 when the code ran, even if it raised — read `ok`/`error`): { "ok": true, "exitCode": 0, "stdout": "…", // print() output, capped at 64KB "stderr": "", // capped at 64KB "truncated": false, // true if stdout/stderr hit the cap "result": "42", // repr of the LAST EXPRESSION (Jupyter style), or null "error": null, // or {"ename":"ZeroDivisionError","evalue":"…","traceback":["…"]} "displays": [ // rich representations of the result value { "mimeType": "text/html", "data": "…
" } // e.g. DataFrames ], "artifacts": [ // files captured automatically from this run { "name": "figure-1-1.png", "mimeType": "image/png", "url": "https://runbox.api.insyde.one/a/…/figure-1-1.png", "sizeBytes": 9981 } ], "durationMs": 776 } Execution semantics: - The value of the last expression becomes `result` (like a Jupyter cell). Statements only → `result: null`. - Every open matplotlib figure is saved as a PNG artifact automatically — do NOT call plt.savefig() or plt.show(). - Any file the code writes to `outputs/` (relative) or `/work/outputs/` also becomes an artifact. - `artifacts[].url` is public and unguessable — pass it straight to the user/chat UI. Never inline file bytes into the model context. - Artifact URLs stay valid for **7 days** (or until the session is DELETEd, which purges them immediately). Persist anything you need longer on your side. - On exception, `ok:false` and `error` carries a clean traceback → fix the code and run again in the SAME session (state survives errors). - A run that exceeds `timeoutMs` returns `error.ename = "TimeoutError"`; the session survives. HTTP 504 only appears when a run is lost entirely. ### GET /sessions/{id} { "sessionId": "…", "alive": true, "createdAt": "…", "expiresAt": "…", "conversationId": "…" } ### GET /sessions/{id}/files — list files (optional `?path=`) ### GET /sessions/{id}/files/{path} — download one file (binary) ### DELETE /sessions/{id} — kill the sandbox and purge its artifacts ### GET /health — { "ok": true, "service": "runbox", "providers": ["mdl"] } ## Sandbox environment - Python 3.12. Preinstalled packages (pip name → import name where they differ): - Data analysis: pandas, numpy, scipy, statsmodels - Plotting: matplotlib, seaborn, squarify (treemaps) - Vega/D3-style: altair (Vega-Lite), vl-convert (offline SVG/PNG render, no browser) - Geo/maps: geopandas, shapely, pyproj, pyogrio (read shp/geojson/gpkg), mapclassify (choropleth bins) - Parliament: parliamentarch (hemicycle SVG), cairosvg (rasterize SVG→PNG) - Spreadsheets: openpyxl (read/write xlsx), xlsxwriter (write xlsx), pyarrow (parquet/feather) - PDF: pdfplumber (extract text/tables), pypdf (split/merge/metadata), reportlab (build laid-out PDFs) - Documents: python-docx → `import docx`, Pillow → `import PIL` - Parsing: beautifulsoup4 → `import bs4`, lxml, unidecode (strip accents), tabulate (text tables) - Text & locale: charset-normalizer → `import charset_normalizer` (encoding detection), babel (number/date/currency formatting) - requests is installed but unusable (network blocked) - Charts come PRE-STYLED with the insyde.one brand (navy #0f0051, green #00ff80, blue #3cc5f5, DM Sans, pt_BR comma decimals). It is the matplotlib default — just plot; do NOT call plt.style.use(...) or sns.set_theme(). `plt.style.use("insyde")` also exists if you reset rcParams. - For VECTOR output (broadcast/editable), save SVG: fig.savefig("outputs/chart.svg"). Auto-capture only makes PNG; emit .svg explicitly when you want vector. It still comes back as an artifact URL. - For the polished declarative (D3-style) look use Altair — it renders OFFLINE via vl-convert (no browser). Apply the brand theme with one line then save: import insyde_theme, altair as alt chart = alt.Chart(df).mark_bar().encode(x="cat", y="val") chart.save("outputs/chart.svg") # or .png - Hemicycle/parliament: parliamentarch builds the seat SVG (rasterize with cairosvg.svg2png if you need PNG), or just arrange dots in concentric arcs with matplotlib (inherits the brand style, outputs PNG directly). - Map boundaries are baked in at /opt/runbox/data/geo/ (read with geopandas.read_parquet, EPSG:4326), so choropleths work offline: - br_estados.parquet (27 rows: code_state, abbrev_state, name_state, name_region) - br_municipios.parquet (~5570 rows: code_muni, name_muni, abbrev_state) - mundo_paises.parquet (~240 rows: name, iso_a3, continent) - mundo_continentes.parquet (~8 rows: continent) Join your data on the key column, then gdf.plot(column=..., scheme="quantiles", legend=True). - 1 CPU, 2 GB RAM. Working directory `/work`; uploads land at `/work/{name}`. - **No network access** — `requests.get(...)` raises ConnectionError. Don't try to pip install or download anything; everything needed is preinstalled. - No secrets in the environment. Filesystem is destroyed with the session. ## Errors All errors are `{ "error": "message" }`: | Status | Meaning | |--------|---------| | 401 | missing/invalid token | | 404 | session unknown, expired, or belongs to another tenant | | 409 | concurrent-session quota exceeded — reuse or DELETE an existing session | | 413 | file larger than 25 MB | | 422 | bad input (missing code, language ≠ python, bad URL) | | 502 | sandbox provider failure — retry once | | 504 | run lost (rare) — retry; if it persists, create a new session | ## Tool definitions (copy-paste) OpenAI tools format: [ { "type": "function", "function": { "name": "runbox_create_session", "description": "Create a Python sandbox session for this conversation. Call once and reuse the sessionId for every run. State (variables, files) persists between runs.", "parameters": { "type": "object", "properties": { "conversationId": { "type": "string" } }, "required": [] } } }, { "type": "function", "function": { "name": "runbox_upload_url", "description": "Load a file from a URL into the sandbox filesystem. Returns the absolute path to use in code (e.g. /work/data.csv).", "parameters": { "type": "object", "properties": { "sessionId": { "type": "string" }, "url": { "type": "string" }, "name": { "type": "string" } }, "required": ["sessionId", "url"] } } }, { "type": "function", "function": { "name": "runbox_run", "description": "Execute Python in the session (Jupyter semantics: last expression is returned as result; matplotlib figures and files written to outputs/ come back as artifact URLs). On error, read error.traceback, fix the code and call again — state survives.", "parameters": { "type": "object", "properties": { "sessionId": { "type": "string" }, "code": { "type": "string" }, "timeoutMs": { "type": "integer" } }, "required": ["sessionId", "code"] } } } ] Anthropic tools use the same parameter objects under `input_schema`. Executor: each tool is one HTTP call with the Bearer token — create → `POST /sessions`; upload → `POST /sessions/{id}/files` (JSON {url,name}); run → `POST /sessions/{id}/run` (JSON {code,timeoutMs}). ## Agent best practices - One session per conversation; store the sessionId in conversation state. If a call returns 404, the session expired — create a new one and re-upload files. - Keep the model's view textual: stdout, result, error, and artifact NAMES; give the UI the artifact URLs. Never fetch artifact bytes into the prompt. - Prefer small, incremental runs (the state persists) over one giant script — errors are cheaper to fix. - `displays[0].data` (HTML table) is for the chat UI; summarize it in text yourself. - Rates: a fresh session takes ~3s to boot; runs are typically 0.5–2s.