The sandbox API
Nimbus.fromEnv uses a colocated DO binding. Nimbus.connect uses the
authenticated remote API. Both return the same typed sandbox.
const box = nimbus.sandbox('job-123', { profile: 'default', // sandbox profile from your NimbusConfig tenant: 'acme', // namespaces the underlying DO: tenant:subject:id subject: 'agent',});
await box.ready(); // headless boot + profile preinstalls; memoizedawait box.destroy({ reason: 'job done' });Every call awaits ready(); call it directly to pay the boot cost first.
Running things
Section titled “Running things”Run one command, run source code, or start a process.
const result = await box.exec('npm test', { cwd: '/home/user/example-app', env: { CI: 'true' }, timeoutMs: 60_000, stdin: 'y\n',});// { command, exitCode, success, stdout, stderr, duration, timestamp }
await box.runCode('print(2 + 2)', { language: 'python', install: 'ifMissing' });
const proc = await box.startProcess('node --watch server.cjs');// NimbusExecResult & { pid, process, ports }exec holds the whole output until the command exits. For large or
long-running output, use execStream: it returns stdout and stderr as bytes
while the command writes them.
const run = await box.execStream('npm test', { timeoutMs: 600_000 });for await (const chunk of run.output) { await spill.write(chunk.stream, chunk.data); // chunk: { stream: 'stdout' | 'stderr', data: Uint8Array }}const { exitCode } = await run.exit; // settles after the last chunkThe stream applies backpressure. When the reader stops, the command waits at
its next write, so unread output does not pile up in the sandbox: it holds
about 64 KiB, plus what the RPC or HTTP transport has in flight. Cancelling run.output kills the command, and run.exit then rejects
with the cancel reason. exec reads this same stream to the end.
Read and change files through the sandbox VFS.
await box.files.write('/home/user/example-app/server.cjs', code); // string | Uint8Arrayconst text = await box.files.read('/home/user/example-app/server.cjs');const bytes = await box.files.readBytes('/home/user/data.bin');const list = await box.files.list('/home/user'); // { name, type }[]await box.files.stat(path); await box.files.mkdir(path);await box.files.exists(path); await box.files.delete(path, { recursive: true });Runtimes
Section titled “Runtimes”Install allowed runtimes when a task needs them.
await box.runtimes.ensure(['python', 'clang']); // install if missingawait box.runtimes.list(); // { installed, available }Nimbus checks runtimes.allow before the install RPC.
Processes
Section titled “Processes”List processes and control their input, signals, size, and logs.
const procs = await box.processes.list(); // pid, command, state, exitCode…await box.processes.kill(pid);await box.processes.write(pid, 'input\n'); // stdinawait box.processes.signal(pid, 'SIGINT');await box.processes.resize(pid, { columns: 120, rows: 32 });const logs = await box.processes.logs(pid, { lines: 100 });attach returns an async iterable for live input and output.
const attachment = box.processes.attach(pid);await attachment.write('q');for await (const chunk of attachment) { // { seq, ts, stream: 'stdout' | 'stderr', data, binary? }}Ports and apps
Section titled “Ports and apps”Every port gets a private preview URL. A server you start is durable: after a platform reset, the next request to its URL restarts it and waits for it.
await box.startProcess('npx vite --host --port 5173', { cwd: 'app' });box.ports.url(5173); // https://5173--<id>.<suffix>/ owner onlyawait box.ports.list();Sharing is explicit. apps.expose with visibility: 'public' mints a link
anyone can open; the link is bound to the program (its command and working
directory), not to the port.
const web = await box.apps.expose(5173, { visibility: 'public', name: 'web' });web.url; // https://<capability>--web--<id>.<suffix>/await box.apps.list(); // owner, name, port, pid, status, visibility, urlawait box.apps.rotateLink('web'); // new link, old one revokedawait box.apps.expose('web', { visibility: 'scoped' }); // back to owner onlyawait box.apps.remove('web'); // stop it, release port and storageA target is a port, a pid, a name, or an owner. startProcess(cmd, { restart: 'on-failure' }) also restarts a server after a crash; by default only a reset
does.
Each (session, port) gets one host, <port>--<id>.<suffix>, where the
suffix is the deployment’s NIMBUS_PREVIEW_HOST_SUFFIX binding; a named app
also answers at <name>--<id>.<suffix>. The app mounts at that host’s root,
so root-absolute paths resolve without rewriting. A deployment with no suffix
bound falls back to /s/<id>/port/<port>/ on the endpoint, and public host
links need the NIMBUS_PUBLIC_DIRECTORY binding
(nimbusPublicDirectory: true in @nimbus-sh/config).
The private URL carries no credential. Where auth is enforced,
GET /s/<id>/api/preview-url?port=<n> mints a single-use
session:preview token for it (tokens & auth). A public
link carries its capability in the hostname; rotating or removing the app
revokes it.
Agent tools
Section titled “Agent tools”Wrap the sandbox as a Proteus-style tool provider.
const provider = box.tools({ namespace: 'sandbox' });It includes files, exec, processes, ports, apps (exposeApp, listApps),
and runtimes.
Errors
Section titled “Errors”Remote calls throw NimbusRemoteError with status, optional code,
and the response body. The client validates wire payloads with zod, so a
drifted server fails instead of returning malformed shapes.