Skip to main content

Running commands

POST /sandbox/execute (MCP: execute-sandbox-command, scope quave:write:dangerous) runs one command to completion inside a sandbox and returns its result in the same response.

{
"accountId": "ACCOUNT_ID",
"sandboxId": "SANDBOX_ID",
"command": ["sh", "-c", "cd /workspace/persist/repo && npm test"],
"cwd": "/workspace",
"envVars": { "GITHUB_TOKEN": "..." },
"timeoutSeconds": 600
}
FieldDescription
commandArgv array, never a shell string. Use ["sh", "-c", "..."] for pipes, redirects, &&, and globbing.
cwdWorking directory.
envVarsExtra environment variables for this command only. Use them for API keys and tokens: they are never stored, logged, or kept in the sandbox after the command.
timeoutSecondsDeadline. Default 120, maximum 900.

The response wraps the result:

{
"success": true,
"result": {
"exitCode": 0,
"stdout": "UHl0aG9uIDMuMTIuMTUKaGVsbG8K"
}
}
  • stdout and stderr are base64-encoded. Decode them before reading (echo "$STDOUT" | base64 -d, Buffer.from(stdout, 'base64'), base64.b64decode(stdout)). This keeps binary output intact. The example above decodes to Python 3.12.15 and hello.
  • A stream that produced no output is omitted.
  • reason is present when the process did not exit on its own (see below).
  • stdoutTruncated / stderrTruncated are true when very large output was cut. Write big output to a file and read it in pieces instead.

Quave ONE does not store the output, the command line, or the environment; the audit trail records only that a command ran and its exit code.

Before a command can run​

  • The sandbox must be READY or BUSY. Poll GET /sandbox after creating it.
  • A SUSPENDED (paused) sandbox is resumed by the execute call itself. If the resume finishes within the request, the command runs; otherwise you get 409 with data.retryable: true and data.retryAfterSeconds (and a Retry-After header). Repeat the same call after the delay.
  • A TERMINATED or FAILED sandbox cannot run commands. Create a new one (attach the same volumeName to keep the data).

How a command can end​

OutcomeWhat you see
Normal exitexitCode of the process.
Deadline reachedThe process is stopped; reason: "deadline_exceeded", usually with exitCode 143. Output written before the deadline is returned.
Out of memoryMemory is a hard limit of the sandbox size. The process is killed with exitCode 137 and reason: "killed". The sandbox keeps running; retry with less work in parallel or a bigger template revision.
Sandbox paused or terminated during the commandreason: "agent_shutdown".
Canceledreason: "canceled".

Long-running work​

One execute call lasts at most 900 seconds (timeoutSeconds), and its HTTP request has to stay open the whole time. For longer builds, test suites, or agent runs, start the command and check on it instead. There is no 15-minute ceiling: the command runs for as long as the sandbox lives.

StepRESTMCPScope
StartPOST /sandbox/command/startstart-sandbox-commandquave:write:dangerous
Read state and outputGET /sandbox/commandget-sandbox-commandquave:read
StopPOST /sandbox/command/killkill-sandbox-commandquave:write:dangerous

1. Start. The body takes the same command, cwd and envVars as execute. timeoutSeconds is optional (up to 7 days); without it, only the sandbox lifetime bounds the command. Send an idempotencyKey and reuse it when you retry the call: the same key returns the same execution instead of starting the command twice.

{
"accountId": "ACCOUNT_ID",
"sandboxId": "SANDBOX_ID",
"command": ["sh", "-c", "cd /workspace/persist/repo && npm ci && npm test"],
"envVars": { "NPM_TOKEN": "..." },
"idempotencyKey": "test-run-42"
}

The answer comes back as soon as the process runs:

{ "success": true, "execution": { "executionId": "EXECUTION_ID", "processId": 17, "startedAt": "2026-10-11T12:00:00Z" } }

2. Read. GET /sandbox/command?accountId=...&sandboxId=...&executionId=...&from=0&waitSeconds=20 returns the state and the output since from:

{
"command": {
"executionId": "EXECUTION_ID",
"state": "running",
"stdoutBytes": 5120,
"stderrBytes": 0,
"outputTruncated": false,
"events": [
{ "seq": 1, "stdout": "PiBucG0gY2kK" },
{ "seq": 2, "stdout": "YWRkZWQgMzEyIHBhY2thZ2VzCg==" }
],
"nextSeq": 3,
"complete": false
}
}
  • Each event has a seq and one of stdout / stderr (base64), exit (exitCode, reason), or truncated.
  • Pass nextSeq as from on the next call. You get every event once, with no gaps, even after a disconnect.
  • waitSeconds (0-25) waits for new output while the command runs, so a polling loop does not need its own sleep.
  • When the command ends, state is exited with exitCode and, for a non-natural end, reason. complete: true means every event, including exit, was delivered.
  • Each command keeps its most recent 4 MiB of output. If you read less often than output arrives, the oldest events are dropped and you get a truncated event that says how much was lost. Write large output to a file and read the file instead.
  • The output is passed through to you and never stored by Quave ONE.

3. Stop. POST /sandbox/command/kill with executionId and an optional signal: SIGTERM (default), SIGINT (Ctrl-C) or SIGKILL. The signal goes to the command and its child processes.

A sandbox is never terminated for idleness while a started command is running. The maximum lifetime (hardLifetimeSeconds) still applies, so size it for your longest job. A SUSPENDED sandbox resumes on start, with the same 409 + retryAfterSeconds contract as execute; pausing or terminating a sandbox stops its commands (reason: "agent_shutdown").

The same process API is also available directly on the region gateway through a gateway session, with a live output stream, stdin, and terminals.

Limits​

LimitValue
One command900 s with execute (timeoutSeconds). Started commands run until they exit, their optional deadline, or the sandbox lifetime.
CPU and memoryThe template revision's zClouds (0.5 vCPU + 512 MB each), reserved and capped.
ProcessespidLimit of the revision, when set: a hard limit on processes and threads.
Temporary diskephemeralDiskMb of the revision, when set. Going over it evicts the sandbox.
Idle timeidleTimeoutSeconds of the revision: the sandbox is terminated after this long without API activity and with no started command running.
LifetimehardLifetimeSeconds of the revision: the sandbox is terminated at expiresAt.
Concurrent sandboxesNo per-app limit. One account-wide safety ceiling on active sandboxes (default 200) guards against runaway loops; over it, create answers 429 with data.reason: "account_concurrency_exceeded" and data.retryAfterSeconds. Paused sandboxes do not count. Ask support to raise it.

Handling errors​

ResponseMeaningWhat to do
400Invalid request (unknown region, bad field, volume mode on a warm-pool template, pause on EPHEMERAL).Fix the request.
403No access: missing scope, not an account admin, account not entitled to sandboxes, or region not enabled for the account.Do not retry. Report it.
404Sandbox, template, or volume not found.Check the ids and the account.
409State conflict: region without the add-on, sandbox not in a state that allows the call, resume in progress, volume pending purge.If data.retryable is true, wait data.retryAfterSeconds and retry. Otherwise read the message.
429Capacity limit.Wait data.retryAfterSeconds (30) and retry with the same idempotency key.
502, 503 on pauseSnapshot failed or the sandbox is not reachable yet. The sandbox keeps running.Retry after data.retryAfterSeconds.

A FAILED sandbox carries errorCode and errorRetryable (codes). For retryable codes, create again with the same idempotency key. SANDBOX_EXPIRED means it reached its maximum lifetime: create a new sandbox and attach the same volume.