sandbox-as-a-service
v0.1.0
Published
Zero-dependency JavaScript/TypeScript client for Sandbox as a Service — secure, disposable cloud sandboxes for AI agents and code execution
Maintainers
Readme
sandbox-as-a-service
A zero-dependency JavaScript/TypeScript client for Sandbox as a Service — secure, disposable cloud sandboxes for AI agents and code execution. Each sandbox is a dedicated virtual machine, created with one call and destroyed when you are done.
- Node.js 18+ (uses the built-in
fetch; nothing to install alongside it) - ESM and CommonJS, with hand-written TypeScript definitions
- Streaming command output via Server-Sent Events
- Typed errors for every failure category
Install
npm install sandbox-as-a-serviceQuickstart
ESM:
import { Client } from 'sandbox-as-a-service';
const client = new Client(); // reads AAS_API_KEY from the environment
const sandbox = await client.createSandbox({ size: 'small', timeoutMinutes: 10 });
try {
const result = await sandbox.exec('python3 -c "print(6 * 7)"');
console.log(result.stdout); // 42
await sandbox.writeFile('/workspace/app.py', "print('hello from the sandbox')");
console.log((await sandbox.readFile('/workspace/app.py')).content);
} finally {
await sandbox.destroy();
}TypeScript:
import { Client, ExecutionFailed } from 'sandbox-as-a-service';
const client = new Client(); // reads AAS_API_KEY from the environment
const sandbox = await client.createSandbox({ size: 'small', timeoutMinutes: 10 });
try {
const result = await sandbox.exec('python3 -m pytest -q', { timeoutMs: 300000, cwd: '/workspace' });
result.check(); // throws ExecutionFailed on a non-zero exit code
} catch (err) {
if (err instanceof ExecutionFailed) console.error(err.execution.stderr);
} finally {
await sandbox.destroy();
}CommonJS works the same way: const { Client } = require('sandbox-as-a-service');
On Node.js 22+ you can let the scope destroy the sandbox for you:
const client = new Client();
await using sandbox = await client.createSandbox({ size: 'small', timeoutMinutes: 10 });
const result = await sandbox.exec('echo hello');
// the sandbox is destroyed hereSet AAS_API_KEY from Dashboard → API keys and
the client picks it up. Pass new Client({ apiKey, baseUrl, timeoutMs }) to override; a bare
baseUrl gets /v1 appended. Creating a sandbox blocks until the machine is ready.
Streaming output
Pass onStdout or onStderr and the same call streams: the callbacks fire
as the sandbox produces output, and the return value is the same Execution
a blocking call returns.
const result = await sandbox.exec(
'for i in 1 2 3; do echo tick $i; sleep 1; done',
{
onStdout: (chunk) => process.stdout.write(chunk),
onStderr: (chunk) => process.stderr.write(chunk),
},
);
// callbacks fire as output arrives; result is the usual Execution objectClosing the connection mid-stream — the process exiting, Ctrl-C — kills the
remote command and records it as cancelled.
Files
await sandbox.writeFile('/workspace/app.py', "print('hello')");
await sandbox.writeFile('/workspace/blob.bin', base64, { encoding: 'base64' });
const file = await sandbox.readFile('/workspace/app.py');
console.log(file.content, file.sizeBytes); // .text and .bytes are aliases
const listing = await sandbox.listFiles('/workspace'); // entries: name, type, size_bytes
console.log(listing.names());
await sandbox.deleteFile('/workspace/app.py'); // { recursive: true } for a directory treePorts
const preview = await sandbox.exposePort(8000); // { url, port, ... }
const open = await sandbox.listPorts();
await sandbox.closePort(8000);Errors
Non-2xx responses raise a specific error, so a caller can react to the reason
rather than parse a status code. Every SandboxApiError carries status,
type, requestId and the decoded responseBody; quote the request id in a
support request.
import { Client, NotFoundError, RateLimitError } from 'sandbox-as-a-service';
const client = new Client();
try {
const sandbox = await client.getSandbox('sbx_does_not_exist');
} catch (err) {
if (err instanceof NotFoundError) console.log('gone');
if (err instanceof RateLimitError) console.log('slow down, retry after', err.retryAfter, 'seconds');
}| Error | Raised when |
| --- | --- |
| AuthenticationError | The key is missing, malformed or revoked (401). |
| PermissionDeniedError | The key is valid but not allowed to do this (403). |
| NotFoundError | No such sandbox, file or execution (404). |
| InvalidRequestError | The request body or parameters were rejected (400); a 422 maps to the base SandboxApiError. |
| ConflictError | The sandbox is in a state that forbids the operation (409). |
| PaymentRequiredError | The account has no credit left (402). |
| RateLimitError | A rate limit was hit (429); retryAfter is set when the header is present. |
| ServiceUnavailableError | A transient server or upstream failure (503). |
| SandboxConnectionError | The request never reached the API — DNS, TLS or timeout. |
| SandboxConfigurationError | The client was constructed with something it cannot use. |
What the client covers
client.createSandbox({ size, name, timeoutMinutes, idempotencyKey })— creates a sandbox and returns it ready to use.client.getSandbox(id),client.listSandboxes({ limit, startingAfter, includeDeleted }),client.iterSandboxes()client.getAccount(),client.getUsage({ days })sandbox.refresh(),sandbox.extend(additionalMinutes),sandbox.destroy()sandbox.exec(command, { timeoutMs, cwd, env, onStdout, onStderr }),sandbox.getExecution(id)sandbox.writeFile(path, content, { encoding }),sandbox.readFile(path, { encoding })sandbox.listFiles(path, { recursive }),sandbox.deleteFile(path, { recursive })sandbox.exposePort(port),sandbox.listPorts(),sandbox.closePort(port)
Snapshots are available over the REST API only for now.
Full API reference: https://sandbox-as-a-service.com/docs/api
