@generativex/sandbox-sdk
v0.3.1
Published
TypeScript SDK for GX Sandbox — isolated microVM sandboxes, app serving, and managed databases via one typed client.
Maintainers
Readme
GX Sandbox — TypeScript SDK
Typed TypeScript/JavaScript client for GX Sandbox — spin up isolated microVM sandboxes, run commands and stream logs, read/write files, deploy app-serving services, and provision managed databases (MySQL / Valkey), all behind one client.
Ships ESM and CommonJS with full .d.ts types. Types and request/response
shapes are derived from the server's OpenAPI schema (the single source of
truth — see ADR 70-devex/sdk/000), so the SDK can't drift from the API.
Install
npm install @generativex/sandbox-sdkRequires Node.js 18+ (uses the global fetch/AbortSignal).
Authentication
Every request authenticates with a project API key (sb_…) or a
personal access token (sbp_…) as a Bearer token. Get one from the Console,
the sandbox CLI, or the admin API (see
authentication docs).
Pass it explicitly, or set environment variables and let the client pick them up:
export SANDBOX_API_KEY="sb_…"
export SANDBOX_API_URL="https://api.your-gx-sandbox.example" # defaults to http://localhost:3000Quick start
import { SandboxClient } from "@generativex/sandbox-sdk";
// apiKey / baseUrl fall back to SANDBOX_API_KEY / SANDBOX_API_URL
const client = new SandboxClient({
apiKey: "sb_…",
baseUrl: "https://api.your-gx-sandbox.example",
});
const sb = await client.sandboxes.create({ alias: "demo" }); // auto-starts
const out = await sb.run("echo hello");
console.log(out.stdout); // "hello\n"
// Files are base64-on-the-wire; the SDK encodes/decodes for you.
// write() accepts a string or Uint8Array; read() returns bytes (Uint8Array).
await sb.files.write("/tmp/note.txt", "hi there");
const bytes = await sb.files.read("/tmp/note.txt");
console.log(new TextDecoder().decode(bytes)); // "hi there"
// Stream logs as an async iterable.
for await (const line of sb.logs({ follow: true })) {
console.log(line.line);
}
await sb.destroy();Ergonomics
await using — never leak a microVM. The sandbox is destroyed when it leaves
scope, even if your code throws:
{
await using sb = await client.sandboxes.create({ alias: "ci" });
await sb.run("pytest");
} // sb.destroy() runs here automaticallyStream output live and still get the exit code. Pass onStdout/onStderr
to run() — output is delivered as it's produced, and the resolved result
(exit code included) is returned at the end. Ideal for agent loops:
const { exitCode } = await sb.run("npm test", {
onStdout: (line) => process.stdout.write(line + "\n"),
onStderr: (line) => process.stderr.write(line + "\n"),
});Prefer an async iterable? for await (const line of sb.stream("npm test")) ….
Background processes return a handle — poll, wait, or kill without juggling PIDs:
const proc = await sb.runBackground("python -m http.server 8000");
const result = await proc.wait(); // polls until exit; result.exitCode
await proc.kill();Read files in the shape you want; write many at once:
const text = await sb.files.read("/app/log.txt", { format: "text" });
await sb.files.writeMany([
{ path: "/app/main.py", content: "print('hi')" },
{ path: "/app/data.bin", content: bytes },
]);Interactive terminal (PTY) over WebSocket — no BYO WS client:
await using term = await sb.terminal();
term.onData((bytes) => process.stdout.write(bytes));
term.write("ls -la\n");
term.resize(120, 40);
// or stream output: for await (const chunk of term) process.stdout.write(chunk);Run the OpenAI Codex agent inside the sandbox in one call. sb.codex() starts
the codex app-server in the VM and opens an initialize-d JSON-RPC session over
the passthrough WebSocket:
await using codex = await sb.codex({ version: "0.125.0", apiKey: process.env.OPENAI_API_KEY });
for await (const ev of codex.turn("Refactor src/parse.ts and add tests")) {
if (ev.method === "item/completed") console.log(ev.params);
}turn() is the opinionated convenience (thread/start → turn/start → stream to
turn/completed). For full control, every codex RPC is typed: codex.request
autocompletes the method name and types params per method (over codex's
ClientRequest union), notifications stream as the typed ServerNotification
union, and the approvals/elicitations codex raises mid-turn are answered via
onServerRequest:
codex.onServerRequest((req) => {
if (req.method === "item/commandExecution/requestApproval") return { decision: "accept" };
if (req.method === "item/fileChange/requestApproval") return { decision: "accept" };
return { decision: "decline" };
});
await codex.request("model/list", {}); // typed method + params
for await (const n of codex) console.log(n.method); // typed ServerNotificationThe codex message types are owned by codex — committed under src/generated/codex/
and regenerated with npm run generate:codex (runs codex app-server generate-ts)
to match the codex version your platform runs.
Codex config is fully, type-safely controllable. Pass a typed CodexConfig
(codex's own config.toml schema — enum-enforced) and the SDK serializes it to
TOML and sends it verbatim, so what you write is exactly codex's config:
await using codex = await sb.codex({
version: "0.139.0",
apiKey: process.env.OPENAI_API_KEY,
config: {
model_provider: "openai",
approval_policy: "untrusted", // typed as AskForApproval
sandbox_mode: "read-only", // typed as SandboxMode
model_reasoning_effort: "high", // typed as ReasoningEffort
},
});Every event the agent emits is captured — reasoning ("thinking"), tool/command
execution, file changes, agent messages — as typed ServerNotifications
(item/started/item/completed carry a typed item), and approvals arrive via
onServerRequest. (Verified live against codex 0.139.)
Control the idle auto-stop window (the server-side setTimeout analog — in
seconds), or pin a sandbox up:
await sb.setIdleTimeout(900); // auto-stop after 15 min of inactivity
await sb.setAlwaysOn(); // disable idle auto-stop entirelyReconnect to a sandbox by id/alias from anywhere (the client is stateless):
const sb = await client.sandboxes.connect(sandboxId); // alias for .get()Correlate failures with server logs. Every API error carries the server request id:
catch (err) {
if (err instanceof SandboxHttpStatusError) {
console.error(err.code, err.requestId); // → scripts/logs.sh request <id>
}
}What you can do
| Namespace | What it covers |
|---|---|
| client.sandboxes | Create / list / search / get / handle; lifecycle (start/stop/destroy/waitFor); run ({ timeoutSecs }), stream, background processes (runBackground → .follow() incremental output, .wait()); files (read/write/writeMany/patch/list/upload/download/stream); ports, forwards & exposures ({ signal }); snapshots; commit({ metadata, requireClean })/clone; env, runtime secrets & metadata; resize; idle policy; desktop (desktopInfo/resizeDesktop/clipboard) & terminal/VNC; startVscode/vscodeStatus; logs (SSE); egress audit |
| client.services | App-serving (Cloud Run-like): deploy (image / build-from-git / source), scale, autoscale, rename, domains, secrets, volumes (+ backup/restore), instances, logs, deploy-events, builds, rollback — via services.handle(idOrName) |
| client.mysql / client.valkey | Managed databases via handle(id): create/get/list; SQL console (query/databases/schema/tableDetail/browseData); users; backups; metrics; lifecycle |
| client.apps | App manifests + ephemeral per-session app instances; TeamRun for multi-turn WebSocket sessions |
| client.images | Read-only image catalog (catalog), named templates (templates), and image store (store, committed snapshots) |
| client.networkPolicies | Project L3/L4 egress/ingress firewall policies |
| client.secrets / client.principals | IAM: project secrets and first-class identities |
| client.whoami() / client.egressProfiles() | Identify the calling credential; list egress profiles |
Errors
All API failures throw a typed error. Branch on the structured fields, not strings:
import {
SandboxHttpStatusError,
SandboxPayloadTooLargeError,
isRetryable,
} from "@generativex/sandbox-sdk";
try {
await sb.files.write("/tmp/big", huge);
} catch (err) {
if (err instanceof SandboxPayloadTooLargeError) {
console.error(err.body?.hint);
} else if (err instanceof SandboxHttpStatusError) {
console.error(err.status, err.body?.code, err.body?.hint);
}
if (isRetryable(err)) {
// 429 / 5xx / network — the client already retried with backoff
}
}Pagination
List methods return a Paginated<T> that transparently follows cursors:
for await (const policy of client.networkPolicies.list()) {
console.log(policy.name);
}
const all = await client.images.catalog().all(); // collect to an arrayConfiguration
new SandboxClient({
apiKey: "sb_…", // or SANDBOX_API_KEY
baseUrl: "https://…", // or SANDBOX_API_URL (default http://localhost:3000)
timeout: 60_000, // ms; unary calls only — streaming is never timed out
maxRetries: 2, // 429/5xx/network, exponential backoff + Retry-After
autoStartOnStopped: true, // a data-plane op (run/files/…) on a stopped/idle
// sandbox transparently starts it, waits for running,
// and retries once — long-lived sandboxes "just work".
// Override per call: run(cmd, { autoStart: false }).
headers: { "x-admin-key": "…" }, // non-Bearer auth (apiKey optional when set)
fetch: customFetch, // override the fetch implementation (proxy, polyfill)
});Zero-round-trip handles. When you already hold an id,
sandboxes.handle(id),mysql.handle(id),valkey.handle(id), andservices.handle(idOrName)return a handle with no network call — call ops (run,query,deploy, …) directly.
Development
npm install
npm run typecheck
npm run build # ESM + CJS + .d.ts via tsup
npm test # vitest (offline; fetch is mocked)
npm run test:e2e # live e2e (needs SANDBOX_API_URL + SANDBOX_API_KEY)
npm run generate # regenerate src/generated/api.ts from ../../openapi.jsonLicense
Apache-2.0. See LICENSE.
