Run code in the sandbox
This page shows the two ways code reaches the sandbox: the run_python tool your agents already have (the normal path), and the raw sandbox API (for operators with cluster access). It also documents every request field, response field, and error.
Before you begin
- To use
run_python, you need a deployed agent. See Invoke an agent. - To call the API directly, you need
kubectlaccess to the cluster. The sandbox service (sandboxd) has no public endpoint and no authentication — it is reachable only inside the cluster, on purpose. If you only use the web console and CLI, you never need direct access; your agents reach it automatically.
Run code from an agent (the normal path)
Every agent gets the run_python tool. When the model decides code is needed, the tool sends the snippet to the sandbox and returns the output as part of the agent's reply.
- Invoke an agent with a request that needs computation:
platformctl invoke research-buddy "Please compute 2**32 in python."
The reply contains 4294967296, and the tool-call list shows run_python. That number came from a real Python process in a single-use pod, not from the model guessing.
- If you write custom agent code, you can hand the tool to your agent explicitly. It is a plain Python function:
from crusoe_adk.tools import run_python
The tool takes one argument, code (a string of Python source), and returns a formatted string with the sandbox's stdout, stderr, and exit code. When called through run_python, the timeout is fixed at 20 seconds.
Call the sandbox API directly
Direct API access requires kubectl and is meant for platform operators and debugging. The service has no authentication — never expose it outside the cluster.
- Port-forward the service to your machine:
kubectl -n cai-system port-forward svc/sandboxd 8080:8080 &
export SANDBOX="http://localhost:8080"
- Run a snippet:
curl -s -X POST "$SANDBOX/v1/execute" \
-H "Content-Type: application/json" \
-d '{"code":"print(2**32)","timeout_s":10}'
You should see:
{"sandbox_id":"sandbox-x7k2p","stdout":"4294967296\n","stderr":"","exit_code":0,"duration_ms":38}
- Check the warm pool:
curl -s "$SANDBOX/v1/pool"
You should see:
{"warm":3,"target":3,"in_flight":0,"created_total":18}
created_total goes up by one for every execution — proof that each run got a fresh pod.
POST /v1/execute — run a Python snippet
Runs one snippet in a warm pod, then destroys the pod. Every response carries an X-Request-Id header. Errors are JSON: {"error": "<message>"}.
Request fields
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
code | string | yes | — | Python source to run. |
language | string | no | "python" | Only Python is supported. Anything else returns 400 with unsupported language "<x>". |
timeout_s | integer | no | 20 | Wall-clock limit in seconds. Maximum 60. 0 or absent means "use the default". Out of range returns 400 with timeout_s must be between 1 and 60. |
files | array | no | — | Files to write into the working directory before the code runs. Each entry is {"path": "...", "content_b64": "..."} with base64-encoded content. |
Response fields (200)
| Field | Type | Notes |
|---|---|---|
sandbox_id | string | The single-use pod that ran your code (e.g. sandbox-x7k2p). Different on every call. |
stdout | string | Everything the code printed. |
stderr | string | Errors and warnings. On timeout: timed out after <N>s. |
exit_code | integer | 0 on success. -1 when the run was killed at the timeout. |
duration_ms | integer | How long the code ran, in milliseconds. |
Send input files
Files are written into the working directory before your code starts. Paths must stay inside the working directory.
B64=$(printf 'name,qty\nwidget,2\n' | base64)
curl -s -X POST "$SANDBOX/v1/execute" \
-H "Content-Type: application/json" \
-d "{\"code\":\"print(open('data/input.csv').read())\",\"files\":[{\"path\":\"data/input.csv\",\"content_b64\":\"$B64\"}]}"
You should see:
{"sandbox_id":"sandbox-7fq3n","stdout":"name,qty\nwidget,2\n\n","stderr":"","exit_code":0,"duration_ms":44}
A path that tries to escape the working directory (for example ../etc/passwd) is rejected inside the pod with file path escapes working directory: '<path>', surfaced to you as a 502.
Timeouts kill, cleanly
curl -s -X POST "$SANDBOX/v1/execute" \
-H "Content-Type: application/json" \
-d '{"code":"while True: pass","timeout_s":5}'
You should see:
{"sandbox_id":"sandbox-9d4qf","stdout":"","stderr":"timed out after 5s","exit_code":-1,"duration_ms":5003}
The whole process group is killed at the deadline. An infinite loop cannot hold a pod hostage.
Error responses
| Status | Error message | Meaning |
|---|---|---|
400 | invalid JSON body | The request body did not parse. |
400 | unsupported language "<x>" | Only "python" is accepted. |
400 | timeout_s must be between 1 and 60 | Timeout out of range. |
405 | method not allowed | Only POST works on /v1/execute. |
502 | sandbox execution failed: <err> | The pod could not run the code. |
502 | sandbox pod returned HTTP <code>: <detail> | The in-pod agent rejected the request (includes file-path escapes). |
502 | invalid response from sandbox pod | The pod answered with something unparseable. |
503 | no warm sandbox pod available, try again shortly | The pool was empty for 30 seconds (the maximum wait). Retry. |
GET /v1/pool — pool status
Returns 200:
{"warm": 3, "target": 3, "in_flight": 0, "created_total": 17}
| Field | Meaning |
|---|---|
warm | Pods ready right now. |
target | The pool size the platform maintains (default 3). |
in_flight | Executions running at this moment. |
created_total | Pods created since the service started — a lifetime counter. |
POST /v1/call — sandboxed tool calls
This is the second mode: running one of your own tool functions in a one-use pod built from the agent's own image, with none of the agent's credentials. Agents use this automatically when tool sandboxing is on; you rarely call it yourself. Request bodies are capped at 1 MiB.
{
"image": "localhost:30500/agents/my-agent:abc123",
"module": "agent",
"function": "get_weather",
"args": {"city": "Reykjavik"},
"timeout_s": 30
}
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
image | string | yes | — | The agent's container image. Missing returns 400 with image and function are required. |
function | string | yes | — | The tool function to call. |
module | string | no | "agent" | The Python module that defines the function. |
args | object | no | — | Keyword arguments for the function. |
timeout_s | integer | no | 30 | Maximum 120. Out of range returns 400 with timeout_s must be between 1 and 120. |
Success (200):
{"sandbox_id": "tool-xyz99", "result": {"temp_c": 4}, "duration_ms": 8123}
Both result and error are omitted when empty, so a successful call carries no error key at all. When the tool raises, error appears and carries the exception text.
Tool-call errors:
| Status | Error message | Meaning |
|---|---|---|
503 | could not start a sandbox for this tool call: <err> | The pod could not be created. |
503 | the tool sandbox did not start: the tool sandbox exited before it was ready (phase Failed); the agent image must contain crusoe_adk.toolrunner | The image lacks the tool runner. |
503 | timed out waiting for the tool sandbox to become ready | The pod did not become ready within 110 seconds. |
502 | tool execution failed: <err> | The call itself failed. |
502 | invalid response from the tool sandbox | The runner's reply was unusable (replies are capped at 8 MiB). |
Unlike /v1/execute, tool calls have no warm pool: every call pays pod scheduling plus an image pull, so expect several seconds, not one.
Next steps
- Security and limits — what the sandbox does and does not protect you from.
- Agent tools — the full tool surface your agents get.