Exec
The code interpreter: run a snippet in a sandbox, and move files in and out of the session that sandbox IS.
Also for this capability: API · CLI · SDKs
The code interpreter: run a snippet in a sandbox, and move files in and out of the session that sandbox IS.
| Base URL | https://api.hanzo.ai |
| Operations | 4 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
Specification
HIP-1212 · Exec — The Code Interpreter — Draft · read the specification →
/v1/exec runs a snippet in a sandbox and moves files in and out of the session
that sandbox is. A session IS a sandbox: the id the reply hands back is the
sandbox's, its lease and its files are the session's, and nothing else exists —
no store, no session table, no lifetime of this capability's own
(apps/exec/exec.go:1-23). It is implemented in hanzoai/cloud at apps/exec,
composing over the sandboxes capability across the internal plane.
Motivation
The previous implementation was a reverse proxy to an executor Service that had
zero endpoints for 33 days — /v1/exec answered 503 in production the whole
time, and pointing the proxy elsewhere was never the fix because the thing
pointed at did not exist (apps/exec/exec.go:25-33). The cloud already runs the
one compute primitive; this capability is the code-interpreter wire over it.
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
The name
exec is not on HIP-0139 §2.5's list, so the abbreviation is argued here: it is
the word people say. It has been the shell builtin and the execve(2) family's
name for fifty years; "execute code" is what every code-interpreter tool schema
calls this act, and nobody says "executioner" or "execution service" — the
long form is not a word anyone uses for the thing. §2.5 admits an abbreviation
exactly when it is the spoken word, and this one is.
The surface
Every address is under /v1/exec: the run itself, the file writes and reads
beside it, and one refusal. POST /v1/exec is the one typed operation — lease
the session's sandbox, write the program, run it, report what it printed and
what it wrote (apps/exec/exec.go:751). Four operations are untyped by design,
each declaring in prose why it cannot be a value (apps/exec/exec.go:788-817):
/v1/exec/upload takes multipart/form-data, and a typed body is decoded as
JSON; /v1/exec/download/{session_id}/{fileId} answers bytes, and a typed
operation always marshals a Go value; /v1/exec/files/{sid} answers a bare
JSON array because that is the wire the callers match on; and
/v1/exec/programmatic answers 501 in the open — it names a
suspend-and-resume protocol this capability does not implement.
Today's router serves upload, download and files at the root
(manifest/apps.go:367); each pair is a line in hanzoai/cloud
openapi/misfiled.txt until the fold lands. The wire is not ours — the shapes
are measured from the hanzo.chat code-interpreter clients (@hanzochat/agents'
CodeExecutor and its Files/Code client), which compose these paths off a
configurable base URL, so the fold ships as one base-URL change in lockstep
with the route move (apps/exec/exec.go:34-43).
Session lifetime is the sandbox's
This capability MUST NOT end a lease. The sandboxes reaper does, on the ttl the
lease was taken for and on idleness; a sandbox torn down at the end of a run
would 404 every download of the plot that run just made
(apps/exec/exec.go:18-23). The peer is reached over the internal plane and
never imported — an import would give this process a second sandbox service
racing the real reaper (apps/exec/exec.go:98-104).
Tenancy
One function decides the tenant, and it never reads a header
(apps/exec/exec.go:460-482). A validated IAM bearer scopes the session to
that org. The service-key credential — an opaque key on X-API-Key, compared
in constant time against CODE_EXEC_API_KEY, failing closed when none is
configured (apps/exec/exec.go:45-49) — carries no tenant, so it scopes to
the deployment's own brand org: one tenant for one deployment, which is what a
shared key with no tenant in it actually means. A context carrying neither is
refused.
Money, events, observability
One run is the billed act: the plugin declares Price: cloud.Metered
(plugin/exec/main.go:33) and exec is in the meteredApps standing list
(spend.go:300). The fee — one cent per run by default, set by
CODE_EXEC_FEE_CENTS[_RUN], zero making runs free — is gated and debited on
this subsystem's own door (apps/exec/meter.go), deliberately not inside the
exported interpreter, because apps/functions composes the same call and
already charges its own invoke fee for it. The sandbox lease underneath is the
sandboxes capability's own metered act, not folded in here. It publishes no
events on the bus, so a customer's webhooks receive nothing from it, and it
emits nothing to observability beyond the request span every route already
gets.
Stage
ga. This surface serves live chat traffic through a client whose calls can
arrive on the service key alone, and HIP-0139 §8.2's flag-404 on a non-ga
prefix would break them in production; the credential settles through IAM
without hiding the door.
Upstreams
It derives from none. The wire contract is measured from the hanzo.chat
code-interpreter clients — a wire fact, not embedded code — including the
closed language set the tool schema advertises (apps/exec/exec.go:111-119).
Rationale
The alternative to "a session is a sandbox" is a session table mapping ids to sandboxes — a second store whose rows can disagree with the leases they name, in exactly the way the dead executor's state disagreed with reality. Holding no state means every answer is about a sandbox that verifiably exists.
Security Considerations
This is an arbitrary-code-execution surface; the sandbox boundary is the
product. The two implementation wrongs that history has already shown: running
code with no key at all when the key was unset — the check now fails closed
(apps/exec/exec.go:689-699) — and trusting a caller-supplied org header, which
tenantOf now structurally refuses by reading only the admission marker on the
context (apps/exec/exec.go:475-482). The prefix list that routes admission is
fail-closed on drift: a stale entry is a 403 on a route that should work, never
a route that works without a credential (apps/exec/exec.go:763-773).
Four surfaces
| Surface | Reaches this capability as | Coverage |
|---|---|---|
| REST | exec at its own prefix | 4 operations |
| CLI | hanzo exec … | 4 of 4 |
| SDK | ExecApi in every published client | 4 methods |
| MCP | tool exec on https://api.hanzo.ai/v1/mcp | 5 operations, 0 under the document's own id — ask describe for the rest |
Quickstart
export HANZO_API_KEY=sk-... # console.hanzo.ai → API keysThen the first call. This one takes arguments — the placeholders are yours to fill. GET /v1/exec/files/{sid}, operation get_exec_files_by_sid:
hanzo exec files get <sid>import { Configuration, ExecApi } from 'hanzoai';
const api = new ExecApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getExecFilesBySid({ sid: 'sid' });from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import ExecApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = ExecApi(client).get_exec_files_by_sid(sid='sid')cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.ExecAPI.GetExecFilesBySid(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::apis::{configuration::Configuration, exec_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = exec_api::get_exec_files_by_sid(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.ExecApi;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new ExecApi(client).getExecFilesBySid();curl https://api.hanzo.ai/v1/exec/files/<sid> \
-H "Authorization: Bearer $HANZO_API_KEY"MCP reaches exec through the exec tool, which names its 5 operations with its own verbs — this one among them, under a name only MCP declares. describe explains any of them:
curl -X POST https://api.hanzo.ai/v1/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "describe",
"arguments": {
"op": "get_download"
}
}
}'Answers 200 with listing[] — ok.
Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/exec/files/{sid} | Lists the files in an execution session. |
POST /v1/exec/programmatic | Answers 501 — this deployment does not serve programmatic tool calling. |
POST /v1/exec/upload | Upload a file into an execution session |
POST /v1/exec | Run a code snippet in a sandboxed interpreter |
How is this guide?