Sandboxes
Package sandbox is the ONE compute primitive: a sandbox is a gVisor pod that runs somebody else's code, and every lifetime is the same object.
Package sandbox is the ONE compute primitive: a sandbox is a gVisor pod that runs somebody else's code, and every lifetime is the same object.
| Base URL | https://api.hanzo.ai |
| Operations | 15 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
sandboxes
POST /v1/sandboxes/{id}/exec
Run a command in a sandbox
Runs a command inside the sandbox and returns its exit code, stdout and stderr. A non-zero exit is a SUCCESSFUL call carrying a failed program — the HTTP status stays 200, because "the tests failed" and "the sandbox is broken" are different facts.
NOTHING RUNS IN cloud. The command is streamed to the Kubernetes exec subresource of the sandbox's pod, which runs under the gVisor runtime class. The sandbox is addressed by pod NAME through the apiserver, never by address.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes |
GET /v1/sandboxes/{id}/fs
Read a file, or list a directory
Reads one file from the sandbox's project directory as text, or lists the entries when the path names a directory. Paths resolve under the project root and a path that climbs out is refused rather than rewritten.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes |
POST /v1/sandboxes/{id}/fs
Write a file
Writes the request body to one file in the sandbox's project directory, creating parent directories. Same confinement as the read above.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes |
POST /v1/sandboxes/{id}/terminal/ticket
Open a terminal
Mints a SINGLE-USE ticket for one interactive terminal in this sandbox and returns {ticket, expiresIn, url}, where url is the terminal PAGE with the ticket already on it.
It exists because a browser carries no Authorization header into a WebSocket or an iframe, so a terminal cannot be authenticated the way every other route here is. The ticket is a credential MINTED for that one terminal: bound to this org and this sandbox, valid for thirty seconds, and gone the first time it is presented. A long-lived bearer in a query string would instead be written into every access log on the path.
Mint one per terminal, and mint a fresh one to reconnect.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes |
GET /v1/sandboxes/{id}/terminal/ws
The terminal, as a socket
Upgrades to a WebSocket carrying a login shell on a pseudo-terminal inside the sandbox — for a host that brings its own emulator. Requires ticket; a missing, expired or already-spent one answers 401 without upgrading.
THE WIRE. A text frame is stdin, unless it is the one control object {"resize":{"cols":N,"rows":M}}; a binary frame is always stdin. Output comes back as BINARY frames, because a pty emits arbitrary bytes cut at arbitrary offsets and a text frame carrying half a rune is one the browser closes the connection over.
arg names a SESSION: the shell runs under tmux new -A -s <arg>, which attaches to that session if it exists and creates it if it does not — so one sandbox holds as many terminals as a caller has names for. It is 1-64 characters of letters, digits, - or _ and may not begin with -; anything else is 400. Without arg the shell is unnamed and unmultiplexed.
The shell is zsh -l, falling back to bash -l and then to sh -l, and to the plain shell again when the image has no tmux. Every step is a preference and none is a requirement: whatever else the image carries — the hanzo CLI included — is a command to type, never a condition for getting a prompt.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes |
GET /v1/sandboxes/{id}/terminal
The terminal, as a page
A complete, self-contained terminal — xterm inline, no other origin — that opens its own socket and runs a shell in this sandbox. Embed it in an iframe and there is nothing else to build.
ticket is the credential from the POST above and arg names the session (see the socket below); both are simply carried through to the socket. The page is NOT gated — it is inert markup and does not redeem the ticket, because a ticket is spent once and a page that spent it would hold a credential that no longer opens anything.
When the terminal is up it posts {source:"hanzo-term", ready:true} to its parent frame, so a host can tell a live terminal from a page that failed into something else. frame-ancestors admits our own brands' hosts and nothing further.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes |
GET /v1/sandboxes/{id}
One sandbox
Returns one of the caller org's sandboxes. An id belonging to another org answers 404 and not 403 — a 403 would confirm the id exists, and whether a given sandbox exists is itself a cross-tenant fact.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes |
DELETE /v1/sandboxes/{id}
End a sandbox
Stops the sandbox's pod and drops the lease. The VOLUME survives by default, so a dev or desktop sandbox can be leased again over the same project and find its checkout where it left it.
purge=1 deletes the volume too. It is opt-in because it is the one part of this that cannot be undone.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes |
POST /v1/sandboxes/end
End a sandbox and release it
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
id | string | — | |
purge | boolean | — |
POST /v1/sandboxes/lease
Lease a sandbox — a real computer — or resume one you hold
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
class | string | — | |
id | string | — | |
project | string | — | |
runtime | string | — | |
ttlSec | integer | — |
POST /v1/sandboxes/read
Read a file from a sandbox you hold
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
id | string | — | |
path | string | — |
POST /v1/sandboxes/stop
Stop what a sandbox is running, and keep the sandbox
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
id | string | — |
POST /v1/sandboxes/write
Write a file into a sandbox you hold
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
data | string | — | |
id | string | — | |
path | string | — |
GET /v1/sandboxes
The sandboxes this org holds
Lists the caller org's sandboxes, newest first. project and status narrow it, and both are read from the QUERY STRING.
It answers from the org's own store rather than from the cluster, so a sandbox whose pod has since died still appears, carrying the status it was last known to have. That is deliberate: a lease you are being charged for should not vanish from the list because the thing behind it fell over.
POST /v1/sandboxes
Lease a sandbox
Creates a sandbox and returns it. class is one of exec, dev or desktop; dev and desktop are attached to a project, which is required for them and names the volume the work persists on. ttlSec bounds the lease, and image overrides the class default.
This is the ONLY path that creates cluster objects. The isolation boundary is the pod's runtime class, one field, so what a sandbox is confined by is a deployment decision rather than anything this operation negotiates.
How is this guide?