Docs/Gateway/Sandboxes

Sandboxes

Sandboxes are isolated cloud VMs you create and drive over the gateway with your cg_sk_ key, the same key you use for model calls. Run commands synchronously or as background jobs, move files in and out, and pay only for the seconds the VM is running.

Everything below is a plain authenticated HTTP call to https://gateway.codegraff.com. No SDK is required. Send your cg_sk_ key as the bearer token on every request.

Create a sandbox

POST /v1/sandboxes boots a VM and returns its id plus the allocated spec. autoStopMinutes (default 30) is the inactivity window after which it auto-stops to stop billing.

ts
const GATEWAY = "https://gateway.codegraff.com";
const headers = {
  Authorization: `Bearer ${process.env.CODEGRAFF_API_KEY}`, // cg_sk_...
  "Content-Type": "application/json",
};

const sb = await fetch(`${GATEWAY}/v1/sandboxes`, {
  method: "POST",
  headers,
  body: JSON.stringify({ language: "javascript", autoStopMinutes: 30 }),
}).then((r) => r.json());

console.log(sb.id, sb.cpu, sb.memory, sb.disk); // id + allocated spec
python
import os, requests

GATEWAY = "https://gateway.codegraff.com"
headers = {"Authorization": f"Bearer {os.environ['CODEGRAFF_API_KEY']}"}  # cg_sk_...

sb = requests.post(
    f"{GATEWAY}/v1/sandboxes",
    headers=headers,
    json={"language": "javascript", "autoStopMinutes": 30},
).json()
print(sb["id"], sb["cpu"], sb["memory"], sb["disk"])

Run a command

POST /v1/sandboxes/:id/exec runs a shell command and, by default, blocks until it finishes, returning exitCode and combined stdout/stderr in result. cwd is optional (defaults to the home directory).

ts
const { exitCode, result } = await fetch(
  `${GATEWAY}/v1/sandboxes/${sb.id}/exec`,
  { method: "POST", headers, body: JSON.stringify({ command: "node -v" }) },
).then((r) => r.json());

console.log(exitCode, result); // 0 "v22.x\n"
python
r = requests.post(
    f"{GATEWAY}/v1/sandboxes/{sb['id']}/exec",
    headers=headers,
    json={"command": "node -v"},
).json()
print(r["exitCode"], r["result"])  # 0 "v22.x\n"

~95s synchronous limit

The gateway runs on Cloudflare, which caps an outbound request at ~100s. A synchronous exec that runs longer is aborted at ~95s with an exec_timeout error (HTTP 504). Use an async command for anything that might run that long, such as installs, test suites, builds, or clones.

Long-running commands (async)

Pass "async": true and the command launches detached inside the sandbox; the call returns immediately with an execId. You then poll GET /v1/sandboxes/:id/exec/:execId with short calls that never approach the cap. The job keeps running across polls and client disconnects until it finishes or the sandbox stops.

ts
// Launch: returns { execId, state: "running" } right away
const { execId } = await fetch(`${GATEWAY}/v1/sandboxes/${sb.id}/exec`, {
  method: "POST",
  headers,
  body: JSON.stringify({ command: "npm ci && npm test", async: true }),
}).then((r) => r.json());

// Poll until done; each call is well under a second
let job;
do {
  await new Promise((r) => setTimeout(r, 1000));
  job = await fetch(
    `${GATEWAY}/v1/sandboxes/${sb.id}/exec/${execId}?full=1`,
    { headers },
  ).then((r) => r.json());
} while (job.state !== "completed");

console.log(job.output, "→ exit", job.exitCode);
python
import time

launch = requests.post(
    f"{GATEWAY}/v1/sandboxes/{sb['id']}/exec",
    headers=headers,
    json={"command": "npm ci && npm test", "async": True},
).json()
exec_id = launch["execId"]

while True:
    job = requests.get(
        f"{GATEWAY}/v1/sandboxes/{sb['id']}/exec/{exec_id}",
        headers=headers,
        params={"full": 1},
    ).json()
    if job["state"] == "completed":
        break
    time.sleep(1)

print(job["output"], "-> exit", job["exitCode"])

A poll returns state (running or completed), exitCode (once done), and output, which contains the last 64 KB of stdout/stderr by default, or pass ?tail=N for N bytes / ?full=1 for everything. To stop a job early, DELETE /v1/sandboxes/:id/exec/:execId kills it and cleans up.

Files

Move files in and out with base64-encoded bodies:

CallBody → Response
POST .../upload{ path, contentBase64 }{ ok: true }
POST .../download{ path }{ contentBase64 }
bash
curl -X POST https://gateway.codegraff.com/v1/sandboxes/$ID/upload \
  -H "Authorization: Bearer cg_sk_your_key" -H "Content-Type: application/json" \
  -d "{\"path\":\"/home/user/app.js\",\"contentBase64\":\"$(base64 < app.js)\"}"

Idle stop and garbage collection

Idle sandboxes stop automatically; they do not delete themselves

A running sandbox auto-stops after its autoStopMinutes inactivity window (30 minutes by default). Auto-stop ends compute billing and preserves the filesystem, but the stopped sandbox remains in your account until you explicitly call DELETE /v1/sandboxes/:id.

The inactivity timer tracks external sandbox activity. Gateway interactions such as exec and file/toolbox calls refresh activity; a process merely running in the background does not. For an unattended build, server, or async job, choose an autoStopMinutes value longer than the expected runtime. Setting it to 0 disables idle auto-stop, but compute metering continues and the gateway still force-stops the sandbox if the account runs out of credit.

ConditionWhat Codegraff does
Running but idleAuto-stops after autoStopMinutes. Memory and processes are cleared; disk files are preserved.
Running without creditThe billing janitor force-stops it on its next successful pass (scheduled once per minute) so more compute debt cannot accrue.
Stopped or archivedThe janitor settles residual compute and releases the reservation. Compute no longer accrues; retained storage is billed only above the free 5 GiB allowance. The sandbox is not automatically deleted by Codegraff.
DELETESettles any final usage, removes the billing meter, and permanently destroys the sandbox and its files.

For disposable workloads, call DELETE /v1/sandboxes/:id in a cleanup or finally block so a failed task does not leave a stopped sandbox behind.

Lifecycle

CallEffect
GET /v1/sandboxesList your sandboxes.
GET /v1/sandboxes/:idState + spec for one sandbox.
POST .../stopStop the VM and settle compute billing. Disk state is preserved.
POST .../startResume a stopped sandbox.
DELETE /v1/sandboxes/:idSettle final billing and destroy it for good.

Each sandbox is private to the key's owner. Calls against a sandbox you do not own return 404.

Billing

Compute is billed per second of running time and debited from your credit balance. The reservation for a fresh sandbox covers its full autoStopMinutes window; a per-minute meter then charges actual uptime (so a long-running or restarted box is billed for exactly the seconds it ran, never the wall-clock gap while stopped).

ResourceRate
vCPU19.6 µUSD/s each
Memory6.3 µUSD/s per GiB
Storage0.042 µUSD/s per GiB (first 5 GiB free)

What it costs

The default spec (2 vCPU, 4 GiB RAM, 3 GiB disk) runs at 65 µUSD/s, about $0.0039/min or $0.23/hour while running. A stopped sandbox bills only storage (free under 5 GiB) until you delete it. Hit GET /v1/sandboxes/:id/meter for live cost, or watch the dashboard. Costs are in micro-USD (1,000,000 µUSD = $1).

Using the SDK

If you embed the agent with the TypeScript or Python SDK, the same sandboxes are wrapped in a higher-level client: graff.createSandbox({...}) / graff.create_sandbox(...), then box.exec(...), box.upload(...) / download, and box.stop() / start() / destroy(). They authenticate with the same cg_sk_ key and bill identically. Reach for the raw HTTP API above when you're not in Node or Python, or when you want background exec + polling.

Color theme
Theme follows your system