Commands & Logs

Every command runs in the sandbox's main container. There are three execution modes — pick the one that matches your caller.

Mode Endpoint Right for
Background run POST /v1/sandboxes/{id}/commands Long jobs, MCP, CI tasks.
Status check GET /v1/sandboxes/{id}/commands/{cid} Polling a background command.
Logs (cursor) GET …/commands/{cid}/logs?cursor=&stream=false Polling logs without holding a stream.
Logs (stream) GET …/commands/{cid}/logs?follow=true SSE tail for live UIs.
Interactive PTY GET /v1/sandboxes/{id}/sessions/{sid} (WS) Shells, REPLs (attach:sandbox scope).

Start a command

POST /v1/sandboxes/{id}/commands
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "argv":   ["go", "test", "./..."],
  "env":    { "GOFLAGS": "-count=1" },
  "cwd":    "/workspace",
  "detach": true
}

detach exists for forward compatibility — today's handler is always asynchronous (it returns the command_id immediately and runs exec in the background). Treating detach=true as the contract keeps clients robust to a future inline-sync mode.

Response:

{ "command_id": "0193…", "phase": "running", "started_at": "…" }

exec:sandbox is required.

Inspect status

GET /v1/sandboxes/{id}/commands/{cid}
{
  "command_id": "0193…",
  "phase":      "exited",
  "exit_code":  0,
  "started_at": "…",
  "exited_at":  "…"
}

phase is one of:

  • running — still executing.
  • exited — the process exited normally; see exit_code.
  • killed — user kicked it (DELETE /commands/{cid}).
  • lost — Pod disappeared mid-command. No exit_code is reported. Logs up to the failure are still readable.

Read logs

Cursor mode polls the combined stdout+stderr buffer:

GET /v1/sandboxes/{id}/commands/{cid}/logs?cursor=0&stream=false
{
  "bytes":       "...stdout+stderr from offset...",
  "next_cursor": 4096,
  "phase":       "running",
  "exit_code":   null
}

Pass next_cursor back as cursor on the next request. The byte buffer is in-process today, so cold reads after a process restart return only what's been written to that replica.

Stream mode opens an SSE channel:

GET /v1/sandboxes/{id}/commands/{cid}/logs?follow=true
Accept: text/event-stream

Both modes require read:sandbox.

Kill a command

DELETE /v1/sandboxes/{id}/commands/{cid}

204 on success. Phase flips to killed. Idempotent: deleting an already-finished command is a no-op 204.

Failure modes

Failure Behaviour
Pod dies mid-command phase=lost, exit_code=null. Logs up to the crash readable.
Sandbox deleted before logs fetched Today: in-memory log buffer is lost with the process. Future spec adds ClickHouse-backed durable logs.
Client disconnects mid-stream Cursor unchanged. Reconnect and resume from the last cursor.
Caller lacks exec:sandbox 403 forbidden.