Run code in the sandbox
This page shows how code reaches the sandbox. Your agent calls the built-in run_python tool, and the platform runs the snippet in a throwaway container — a sandbox — which is destroyed afterward. The page then documents every request field, response field, and error message, so you can read what comes back and work out what went wrong.
Before you begin
- You need a deployed agent. See Invoke an agent.
- For the
curltab, the API address in your shell:export CAI_API=https://api.codyhill.dev. Invoking needs no token by default. - You need nothing else. The sandbox itself has no public endpoint and no authentication. That is on purpose: it is reachable only from inside the platform, and your agents reach it automatically.
Run code from an agent (the normal path)
Any agent that lists the run_python tool can run code (see built-in tools). When the model decides code is needed, the tool sends the snippet to the sandbox and folds the output into the agent's reply.
Invoke an agent with something to compute
- platformctl
- curl
- Console
platformctl invoke research-buddy "Please compute 2**32 in python."
You should see:
2**32 is 4294967296.
(session: 3f2c8a1e-9b7d-4e21-a6c0-5d8f13b2e470)
tool_call: run_python called with args={'code': 'print(2**32)'}
curl -s -X POST "$CAI_API/v1/agents/research-buddy/invoke" \
-H 'content-type: application/json' \
-d '{"message":"Please compute 2**32 in python."}'
You should see:
{
"session_id": "3f2c8a1e-9b7d-4e21-a6c0-5d8f13b2e470",
"user_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"output": "2**32 is 4294967296.",
"reasoning": "",
"tool_calls": [
{"name": "run_python", "summary": "called with args={'code': 'print(2**32)'}"}
],
"events": ["..."]
}
Invoking needs no credential by default, which is why there is no Authorization header here.
Open the agent's Test tab and type:
Please compute 2**32 in python.
You should see: a reply containing 4294967296, and a tool call entry showing that run_python ran.
That number came from a real Python process in a single-use sandbox, not from the model guessing.
Hand the tool to your agent explicitly
If you write custom agent code, you can pass run_python in yourself. Each framework wraps the same sandbox call in its own kind of tool object.
- ADK
- CrewAI
- LangGraph
A plain Python function — no parentheses, no wrapper:
from crusoe_adk.tools import run_python
tools = [run_python]
A class, so note the parentheses:
import crusoe_crewai as crusoe
tools = [crusoe.RunPython()]
A tool object, no parentheses:
import crusoe_langchain as crusoe
tools = [crusoe.run_python]
Whichever wrapper you use, the tool takes one argument: code, a string of Python source. It returns a formatted string holding three things from the sandbox. Those are stdout, whatever the code printed; stderr, whatever it wrote as errors; and the exit code, a number where 0 means success. When you go through run_python, the timeout is always 20 seconds.
Why you cannot call the sandbox yourself
You cannot call it directly, and that is deliberate. The sandbox has no public address and no way to sign in, so there is no customer-facing path to it today. If you need one, ask your administrator.
Everything below documents the request the platform sends on your behalf, and the answer that comes back. Read it anyway: those fields, limits, and error messages are exactly what show up in your agent's replies and logs. A successful run looks like this:
{"sandbox_id":"sandbox-x7k2p","stdout":"4294967296\n","stderr":"","exit_code":0,"duration_ms":38}
POST /v1/execute — run a Python snippet
This route runs one snippet in a warm sandbox, then destroys it. Every response carries an X-Request-Id header, which identifies that one request if you need to ask about it. Errors come back as 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 | How many seconds of real time the code may take. 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": "..."}. The content is base64-encoded, which is a way of writing any bytes — including binary ones — as plain text that fits in JSON. |
Response fields (200)
| Field | Type | Notes |
|---|---|---|
sandbox_id | string | The single-use sandbox that ran your code, for example sandbox-x7k2p. It is different on every call. |
stdout | string | Everything the code printed. |
stderr | string | Errors and warnings. On timeout it reads 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. |
Input files
The platform writes every entry in the files array into the working directory before your code starts. So a snippet can read data/input.csv with a plain open().
Paths must stay inside the working directory. Say a path tries to climb out of it, like ../etc/passwd. The sandbox refuses it with file path escapes working directory: '<path>', which reaches you as a 502.
The run_python tool never sends files. It takes only code.
Timeouts kill, cleanly
When a snippet runs past its deadline, the platform kills it and every process it started. Here is a while True: pass submitted with timeout_s of 5:
{"sandbox_id":"sandbox-9d4qf","stdout":"","stderr":"timed out after 5s","exit_code":-1,"duration_ms":5003}
An infinite loop cannot hold a sandbox hostage. Through run_python, the deadline is always 20 seconds.
Note the exit_code of -1: that value means the timeout killed the run, rather than the code finishing on its own.
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 sandbox could not run the code. |
502 | the sandbox returned HTTP <code>: <detail> | The runner inside the sandbox rejected the request (includes file-path escapes). |
502 | the sandbox returned a response that could not be read | The sandbox answered with something unparseable. |
503 | no sandbox is available right now - try again shortly | The pool was empty for 30 seconds (the maximum wait). Retry. |
The warm pool
The platform keeps a few sandboxes started and idle, so your code never waits for one to boot. A pool status endpoint, GET /v1/pool, reports four numbers. It is reachable only from inside the platform, so you cannot call it. The numbers are still a useful picture of what is happening:
{"warm": 3, "target": 3, "in_flight": 0, "created_total": 17}
| Field | Meaning |
|---|---|
warm | Sandboxes ready right now. |
target | The pool size the platform maintains (default 3). |
in_flight | Executions running at this moment. |
created_total | Sandboxes created since the service started — a lifetime counter. |
Every execution destroys its sandbox, and the platform creates a replacement in the background. So created_total goes up by one for every snippet that runs.
POST /v1/call — sandboxed tool calls
This is the second mode. It runs one of your own tool functions in a one-use sandbox built from the agent's own image. That sandbox carries none of the agent's credentials. Agents use this automatically when tool sandboxing is on, so you never call it yourself. Request bodies are capped at 1 MiB.
{
"image": "registry.us-east1-a.ccr.crusoecloudcompute.com/cai-ab12cd-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}
The response leaves empty fields out rather than sending them as null. So a successful call carries no error key at all. When your tool raises an exception, an error key appears and holds the exception text.
Tool-call errors:
| Status | Error message | Meaning |
|---|---|---|
503 | could not start a sandbox for this tool call: <err> | The sandbox 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 sandbox 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 waits for the platform to place a sandbox and download the agent's image. So expect several seconds rather than 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.