@sapiom/sandbox
🕗 Deprecated for new projects. @sapiom/tools
now ships sandbox support built in — use its sapiom.sandboxes.* surface
instead. This standalone package remains published and supported for existing
production use, but new projects should not adopt it.
Sandbox environment management for the Sapiom SDK. Create isolated execution environments, manage files, run commands, and stream output in real time.
Installation
npm install @sapiom/sandbox
pnpm add @sapiom/sandbox
Quickstart
import { SapiomSandbox } from "@sapiom/sandbox";
const sandbox = await SapiomSandbox.create({ name: "my-sandbox" });
await sandbox.writeFile("hello.py", 'print("Hello from the sandbox!")');
const result = await sandbox.exec("python hello.py");
console.log(result.stdout);
const proc = await sandbox.execStream("node runner.js");
for await (const line of proc.output) {
console.log(`[${line.stream}] ${line.data}`);
}
console.log("exit code:", proc.exitCode);
await sandbox.destroy();
API
SapiomSandbox.create(opts)
Creates a new sandbox and returns a handle for interacting with it.
const sandbox = await SapiomSandbox.create({
name: "my-sandbox",
tier: "m",
ttl: "1h",
image: "python:3.12",
envs: { NODE_ENV: "production" },
});
Options:
name | string | Yes | Sandbox name (lowercase alphanumeric + hyphens, 2-63 chars) |
apiKey | string | No | Sapiom API key. Falls back to SAPIOM_API_KEY env var |
baseUrl | string | No | Override the sandbox service URL |
fetch | typeof fetch | No | Pre-configured fetch function (overrides apiKey) |
tier | SandboxTier | No | Memory tier: 'xs', 's', 'm', 'l', 'xl' (default 's') |
ttl | string | No | Time-to-live (e.g. '1h', '24h', '7d') |
envs | Record<string, string> | No | Environment variables |
port | number | No | Single port to expose (mutually exclusive with ports) |
ports | PortSpec[] | No | Array of port specs to expose (mutually exclusive with port) |
image | string | No | Pre-built Docker image for instant creation |
sandbox.writeFile(path, content)
Writes a file relative to the sandbox's workspace root.
await sandbox.writeFile("src/index.ts", 'console.log("hi")');
sandbox.uploadFile(path, content, opts?)
Uploads a file using multipart upload. Handles the full initiate → upload parts → complete lifecycle, with parallel part uploads and automatic abort on any failure. Prefer this over writeFile for binary content or files over a few MB.
import { openAsBlob } from "node:fs";
await sandbox.uploadFile("data/snapshot.bin", new Uint8Array(bytes));
const blob = await openAsBlob("./huge.parquet");
await sandbox.uploadFile("datasets/huge.parquet", blob, {
partSize: 5 * 1024 * 1024,
concurrency: 4,
onPartUploaded: (part, progress) => {
const pct = ((progress.bytesUploaded / progress.totalBytes) * 100).toFixed(1);
console.log(`part ${part.partNumber} ok — ${pct}%`);
},
});
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 10_000);
await sandbox.uploadFile("big.bin", blob, { signal: ctrl.signal });
Options:
partSize | number | 5 * 1024 * 1024 | Part size in bytes. The Sapiom ingress rejects uploads over ~8 MiB, so keep this ≤ 7 MiB. |
concurrency | number | 4 | Number of parallel part uploads. Blaxel recommends 3–5. |
permissions | string | "0644" | Unix file permissions. |
maxRetries | number | 3 | Retries per failed part on 408/425/429/5xx and network errors. 0 disables. |
retryBaseDelayMs | number | 50 | Initial backoff in ms. Doubles per attempt with jitter; honors Retry-After. |
signal | AbortSignal | — | Cancel the upload. Triggers an auto-abort of the server-side session. |
onPartUploaded | (part, progress) => void | — | Fired after each part finishes (completion order — not partNumber order). |
The server accepts at most 10,000 parts per upload. If content.size / partSize > 10_000, uploadFile throws before making any request and suggests a larger partSize.
Low-level multipart API
For resumable uploads or custom retry logic, drive the multipart lifecycle directly:
const { uploadId } = await sandbox.initiateMultipartUpload("large.bin");
try {
const part1 = await sandbox.uploadPart(uploadId, 1, chunk1);
const part2 = await sandbox.uploadPart(uploadId, 2, chunk2);
const uploaded = await sandbox.listMultipartParts(uploadId);
await sandbox.completeMultipartUpload(uploadId, [
{ partNumber: part1.partNumber, etag: part1.etag },
{ partNumber: part2.partNumber, etag: part2.etag },
]);
} catch (err) {
await sandbox.abortMultipartUpload(uploadId);
throw err;
}
initiateMultipartUpload(path, opts?) | Start a session. Returns { uploadId, path }. |
uploadPart(uploadId, partNumber, bytes, opts?) | Upload one part (1-indexed, 1–10000). |
listMultipartParts(uploadId, opts?) | List parts already uploaded. |
completeMultipartUpload(uploadId, parts, opts?) | Commit the upload. |
abortMultipartUpload(uploadId, opts?) | Discard the session and clean up parts. |
sandbox.readFile(path)
Reads a file relative to the sandbox's workspace root and returns its content as a string.
const content = await sandbox.readFile("src/index.ts");
sandbox.exec(command, opts?)
Executes a shell command inside the sandbox. By default waits for the process to finish.
const result = await sandbox.exec("npm install");
console.log(result.exitCode);
console.log(result.stdout);
console.log(result.stderr);
const bg = await sandbox.exec("npm start", { waitForCompletion: false });
console.log(bg.pid);
const status = await sandbox.getProcess(bg.pid);
console.log(status.completed, status.exitCode);
const final = await sandbox.waitForProcess(bg.pid);
console.log(final.exitCode);
Options:
cwd | string | — | Working directory (resolved relative to workspaceRoot) |
env | Record<string, string> | — | Environment variables for the process |
waitForCompletion | boolean | true | Wait for the process to finish |
pollInterval | number | 1000 | Polling interval in ms |
timeout | number | 60000 | Timeout in ms when waiting |
signal | AbortSignal | — | Signal to cancel the operation |
sandbox.execStream(command, opts?)
Executes a command and streams output in real time via an async iterable. Ideal for long-running processes like AI agent runs.
const proc = await sandbox.execStream("node agent.js");
for await (const line of proc.output) {
process.stdout.write(line.data);
}
console.log("exit code:", proc.exitCode);
Supports cancellation via AbortSignal:
const controller = new AbortController();
const proc = await sandbox.execStream("long-task", {
signal: controller.signal,
});
setTimeout(() => controller.abort(), 30_000);
for await (const line of proc.output) {
console.log(line.data);
}
Options:
cwd | string | — | Working directory (resolved relative to workspaceRoot) |
env | Record<string, string> | — | Environment variables for the process |
signal | AbortSignal | — | Signal to cancel the operation |
sandbox.getProcess(pid)
Gets the current status of a process by PID.
const status = await sandbox.getProcess(pid);
console.log(status.completed, status.exitCode);
sandbox.waitForProcess(pid, opts?)
Waits for a process to complete by polling its status. Returns the same ExecResult as exec().
const result = await sandbox.waitForProcess(pid, { timeout: 120_000 });
console.log(result.exitCode, result.stdout);
sandbox.destroy()
Destroys the sandbox and releases all associated resources.
await sandbox.destroy();
Properties
sandbox.name | string | Sandbox identifier |
sandbox.workspaceRoot | string | Absolute workspace root path in the sandbox |
License
MIT