# Isorun Source: / --- title: Isorun description: General-purpose execution runtime for AI agents. Sandboxes in milliseconds. --- Isorun gives you isolated Linux microVMs that boot in milliseconds. Each sandbox is a real Linux box with its own kernel, built for AI agents, untrusted code execution, and anything that needs a clean machine without container security tradeoffs. ```ts title="TypeScript" import { Isorun } from 'isorun' const isorun = new Isorun() // reads ISORUN_API_KEY from env const sandbox = await isorun.create({ image: 'node:22-slim' }) const { stdout } = await sandbox.exec('node -v') console.log(stdout) // v22.x.x await sandbox.destroy() ``` ## What makes Isorun different Most runtimes stop at "boot fast, run code, destroy." Isorun adds primitives that change what an agent can do inside a sandbox: - **Hardware-isolated sandboxes.** Each microVM gets its own kernel. - **Fork a running sandbox.** Clone its filesystem, memory, and processes so an agent can branch, try something, and roll back. - **Hibernate / resume.** Keep a workspace around at zero idle cost. - **Snapshot / restore.** Capture state, then restart anywhere in the same region. - **Port forwarding.** Reach any in-sandbox port over HTTPS with `sandbox.url(port)`. - **Per-second billing.** $0.025/vCPU-hr + $0.015/GiB-hr. About four cents an hour for a 1 vCPU + 1 GiB sandbox. New accounts get **$50 in free credit**, no card required. That's roughly 1,250 hours of a default 1 vCPU + 1 GiB sandbox. ## Next steps - [Quickstart](/getting-started/quickstart/) runs code in a sandbox in a few lines, then introduces the primitives. - [TypeScript SDK reference](/sdks/typescript/) covers the full API surface. - [Pricing](/getting-started/pricing/) has per-second rates with worked examples. --- # Quickstart Source: /getting-started/quickstart --- title: Quickstart description: Run code in a sandbox in three lines, then learn the primitives that make Isorun different. --- You can run code in a fresh microVM sandbox in a few lines: install the SDK, set your API key, then create a sandbox and exec inside it. ## Install ```bash title="Terminal" npm install isorun ``` Requires Node.js 20 or newer. ## API key Set `ISORUN_API_KEY` in your environment. Get one from the [dashboard](https://app.isorun.ai). ```bash title="Terminal" export ISORUN_API_KEY=isorun_live_... ``` ## Run your first sandbox ```ts title="TypeScript" import { Isorun } from 'isorun' const isorun = new Isorun() // reads ISORUN_API_KEY from env const sandbox = await isorun.create({ image: 'node:22-slim' }) const { stdout } = await sandbox.exec('node -v') console.log(stdout) // v22.x.x await sandbox.destroy() ``` Each `isorun.create()` boots a microVM with its own kernel and defaults to 1 vCPU + 1 GiB RAM + a 4 GiB disk. Call `sandbox.destroy()` to free it, or let the `timeoutSec` timer auto-destroy it. ## Run any image The `image` can be **any OCI image**: a Docker Hub tag like `node:22` or `python:3.12-slim`, a GHCR or private-registry reference, or one you built yourself. The first time a runner sees an image it pulls and prepares it; every boot after that is instant. ```ts title="TypeScript" await isorun.create({ image: 'node:22' }) await isorun.create({ image: 'ghcr.io/your-org/agent-runtime:1.0' }) ``` Pin a real tag or digest. Floating tags like `latest` and `lts` are rejected at create time, because they'd let an upstream republish silently change what runs. [Bring your own image](/sandboxes/any-oci-image) ## The primitives that make Isorun different Most runtimes stop at "boot fast, run code, destroy." Isorun adds primitives that change what an agent can do inside a sandbox. ### 1. Forking: clone a running sandbox ```ts title="TypeScript" const parent = await isorun.create({ image: 'python:3.12-slim' }) await parent.exec('pip install transformers torch') // setup once // 5 forks inherit the loaded packages AND the model weights in // memory, with no re-install and no re-load. const children = await parent.fork(5) await Promise.all(children.map((c) => c.exec('python3 evaluate_hypothesis.py'))) ``` Each child inherits the parent's filesystem state and running process state. This is the backtracking primitive an agent uses to explore a branch and roll back. [Read more](/sandboxes/checkpoints-and-rollback/) ### 2. Public URLs: expose any in-sandbox port ```ts title="TypeScript" await sandbox.exec( "nohup setsid jupyter notebook --port 8888 " + "--no-browser --ip 0.0.0.0 --NotebookApp.token='' >/dev/null 2>&1 &" ) console.log(sandbox.url(8888)) // → an HTTPS proxy URL for port 8888 (fetch with your API key) ``` You get a public, WebSocket-aware URL. Vite HMR, Jupyter, and gradio all work. [Read more](/sandboxes/public-urls/) ### 3. Hibernate / resume: persistent workspaces at zero idle cost ```ts title="TypeScript" await sandbox.hibernate() // pause + release active runtime resources // … hours later … await sandbox.resume() // filesystem, memory, and processes intact ``` To the user it looks like a workspace that's always there, while the runner only spends resources when it's actively running. Filesystem, memory, and running processes all survive. [Read more](/sandboxes/lifecycle/) ### 4. Snapshot / restore: capture state, restart anywhere ```ts title="TypeScript" const snap = await sandbox.snapshot() await sandbox.destroy() // Later, on any runner in the same region: const restored = await isorun.restore(snap.id) await restored.exec('node -v') await restored.destroy() ``` Use this to amortise expensive setup across many sandboxes. [Read more](/sandboxes/checkpoints-and-rollback/) ## Next steps - [Create & execute](/sandboxes/create-and-execute/) covers the default create-run-destroy loop. - [TypeScript SDK reference](/sdks/typescript/) lists every method and option. - [Authentication](/getting-started/authentication/) explains keys and regions. --- # Authentication Source: /getting-started/authentication --- title: Authentication description: API keys and how the SDK reaches the right region. --- Isorun authenticates with a single API key. Set it in your environment and the SDK handles everything else, including routing requests to the right region. ```ts title="TypeScript" import { Isorun } from 'isorun' // Reads ISORUN_API_KEY from environment const isorun = new Isorun() // Or pass it directly const explicit = new Isorun({ apiKey: 'isorun_live_...' }) ``` ## How regions work When you sign up you create an **organization**, and the organization is scoped to a region (US or EU). Every API key belongs to that organization, and the SDK routes to that region's runner automatically. There's no endpoint to configure and nothing to override. ## Environment variable ```bash title="Terminal" export ISORUN_API_KEY=isorun_live_... ``` The SDK reads `ISORUN_API_KEY` automatically. That's the only variable you need. Treat your key like a password. Keep it in an environment variable or secrets manager, never commit it, and never paste it into a sandbox. If a key leaks, revoke it from the dashboard. ## Get a key Create one in the [dashboard](https://app.isorun.ai). A key is bound to your organization and its region. You can revoke or rotate keys at any time, and an organization can hold as many keys as you need. ## Next steps - [Quickstart](/getting-started/quickstart/) runs your first sandbox. - [Pricing](/getting-started/pricing/) explains what a sandbox costs. --- # Pricing Source: /getting-started/pricing --- title: Pricing description: Per-second billing with runtime-first economics. --- You pay for what a sandbox actually uses, vCPU and RAM, by the second, with no minimums and no idle charge while it's hibernated. ## Rate Sandboxes are billed by the second based on vCPU and RAM: | Resource | Rate | |---|---| | vCPU | $0.025 / hour | | RAM | $0.015 / GiB / hour | So a 1 vCPU + 1 GiB sandbox is **$0.040 / hour**, which rounds to four cents. Common shapes: | Profile | Price | |---|---| | 1 vCPU + 1 GiB (default) | $0.040 / hour | | 2 vCPU + 4 GiB | $0.110 / hour | | 4 vCPU + 8 GiB | $0.220 / hour | | 8 vCPU + 16 GiB | $0.440 / hour | Every new account gets **$50 in free credit**, about 1,250 hours of a default sandbox. No card required to start. ## What's billed - **Running sandboxes:** every second from `isorun.create()` to `sandbox.destroy()` (or the auto-destroy `timeoutSec` timer). - **Hibernated sandboxes:** not billed for active runtime; you pay nothing while the sandbox is paused. - **Destroyed sandboxes:** billing stops immediately. `sandbox.destroy()` returns the exact billed amount in `costCents`: ```ts title="TypeScript" const stats = await sandbox.destroy() console.log(stats.costCents) // e.g. 0.00111, fractional cents are normal ``` ## Example A 1 vCPU + 1 GiB sandbox running for 90 seconds: ``` 0.040 USD/hour × (90 / 3600) hours = $0.001 ``` A tenth of a cent. ## Next steps - [Lifecycle](/sandboxes/lifecycle/), hibernate to stop paying for idle time. - [Quickstart](/getting-started/quickstart/), spend your first credit. --- # Create & Execute Source: /sandboxes/create-and-execute --- title: Create & Execute description: The default runtime loop for agents and jobs, create, execute, destroy. --- Create a sandbox, run commands inside it, then destroy it. This is the default runtime loop for agents and short jobs. Use this pattern for one-shot tasks and short workflows. For anything that needs to outlive a single request, see [Lifecycle and hibernation](/sandboxes/lifecycle/). ## Quick example ```ts title="TypeScript" import { Isorun } from 'isorun' const isorun = new Isorun() const sandbox = await isorun.create({ image: 'python:3.12-slim' }) try { const result = await sandbox.exec("python3 -c 'print(2**100)'") console.log(result.stdout.trim()) console.log(result.exitCode) } finally { await sandbox.destroy() } ``` ## Streaming output while a command runs `sandbox.exec()` buffers: it resolves once the command exits, so a long build prints nothing until the end. To watch output live, call the streaming endpoint directly — `POST /v1/runs/{id}/exec/stream` returns Server-Sent Events, one JSON frame per `data:` line. | Approach | Best for | Tradeoff | |---|---|---| | `sandbox.exec(cmd, timeoutSec)` | Short bounded commands | No incremental output while running | | `POST /v1/runs/{id}/exec/stream` | Long builds / tests / training | A few lines of parsing; you handle the frames yourself | Each frame is `{ "type": "stdout" \| "stderr" \| "exit", "data": "..." }`: ```ts title="TypeScript" const res = await fetch(`${isorun.apiUrl}/v1/runs/${sandbox.id}/exec/stream`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.ISORUN_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ command: 'python3 long_training_loop.py', timeout: 600 }), }) for await (const chunk of res.body) { for (const line of Buffer.from(chunk).toString().split('\n')) { if (!line.startsWith('data: ')) continue const frame = JSON.parse(line.slice(6)) if (frame.type === 'stdout') process.stdout.write(frame.data) if (frame.type === 'stderr') process.stderr.write(frame.data) } } ``` `GET /v1/runs/{id}/exec/ws` carries the identical frames over a WebSocket if you would rather not parse SSE. ## Handle failures A non-zero exit code is a normal result, not an exception, check `exitCode` yourself. ```ts title="TypeScript" const result = await sandbox.exec('pytest -q', 180) if (result.exitCode !== 0) { console.error(result.stderr) throw new Error('tests failed') } ``` ## Clean up and control cost Always destroy or close sandboxes in `finally` paths. - Prevents leaked runtime costs. - Keeps capacity available under load. - Makes behavior predictable in production. A sandbox you forget to destroy keeps billing until its idle timeout fires. Put `destroy()` in a `finally` block so it runs even when the command throws. ## Next steps - [Lifecycle and hibernation](/sandboxes/lifecycle/), pause a sandbox without paying for idle time. - [Checkpoints and rollback](/sandboxes/checkpoints-and-rollback/), snapshot expensive setup and fork it. - [TypeScript SDK](/sdks/typescript/), full method reference. --- # Lifecycle & Hibernation Source: /sandboxes/lifecycle --- title: Lifecycle & Hibernation description: Create, run, hibernate, resume, and destroy a sandbox, and the cost model behind each transition. --- A sandbox moves through five states, and the transitions between them are the only operations that cost money. An idle hibernated sandbox costs nothing on the runner side. This page walks each transition and the cost model behind it. ``` create() exec() ┌──────────────► running ◄────────┐ │ │ │ │ hibernate() │ │ ▼ │ │ hibernated │ │ │ │ │ resume() │ │ ▼ │ │ running ──────────┘ │ └─ destroy() ─► destroyed ``` ## Create a sandbox It boots a microVM with its own kernel. Defaults: 1 vCPU, 1 GiB RAM, 4 GiB writable scratch disk that grows on write. ```ts title="TypeScript" import { Isorun } from 'isorun' const isorun = new Isorun() const sandbox = await isorun.create({ image: 'python:3.12-slim' }) console.log(sandbox.id) // run<...> console.log(sandbox.createMs) // server-side create duration in ms ``` Wrap the work in `try { … } finally { destroy() }` so the sandbox is always freed: ```ts title="TypeScript" const sandbox = await isorun.create({ image: 'python:3.12-slim' }) try { await sandbox.exec("python3 -c 'print(42)'") } finally { await sandbox.destroy() } ``` ## Hibernate a sandbox Hibernation pauses the sandbox, persists its state, and releases active runtime resources. The runID stays valid, so the user sees a paused workspace they can come back to. The runner only spends resources when the sandbox is actively running, so a hibernated sandbox is essentially free. ```ts title="TypeScript" const sandbox = await isorun.create({ image: 'python:3.12-slim' }) await sandbox.exec('pip install numpy pandas') // setup work // User closes their laptop. Hibernate the workspace. await sandbox.hibernate() // Hours later, the user comes back… await sandbox.resume() // State is identical to what it was at hibernate time: // the pip-installed packages are still there, the cwd is preserved, // and any background processes the user started are still running. await sandbox.exec("python3 -c 'import numpy; print(numpy.__version__)'") ``` What survives a hibernate/resume cycle: - **Filesystem state.** Anything you wrote to the scratch disk is intact. - **Memory state.** Every page the VM had loaded is preserved, including in-memory caches, JIT-compiled bytecode, and database working sets. - **Process state.** Background processes the user started before hibernation are still running at the same PIDs after resume. A long-running `node server.js`, a `jupyter notebook` kernel, an SSH agent, they all survive. - **Open file descriptors and listen sockets.** The VM's network stack is re-attached to a fresh network interface with a new guest IP. Listen sockets and the kernel's TCP stack survive cleanly. The VM gets a new guest IP on resume, so any ESTABLISHED outbound TCP connection from before hibernation is reset. Code that holds a long-lived connection (a database pool, a streaming HTTP client) must reconnect after `resume()`. ## Resume a sandbox Resume restores a hibernated sandbox. The same runID is reused across hibernate/resume cycles, so client code that holds a reference to a sandbox doesn't need to track a new identity. ## Destroy a sandbox Destroy frees the sandbox permanently, including any hibernation snapshot on disk. It returns measured resource usage, these are the numbers you're billed on. ```ts title="TypeScript" const stats = await sandbox.destroy() console.log(stats.cpuMs) // CPU time used console.log(stats.memPeakBytes) // peak RSS console.log(stats.uptimeMs) // wall-clock from create to destroy console.log(stats.costCents) // exact billed amount in cents ``` ## Auto-destroy timeout Every sandbox has an idle timeout (default 300 seconds, configurable per-create via `timeoutSec`). When the timer fires, the sandbox is destroyed. Use `sandbox.setTimeout(seconds)` to extend it during a long-running workflow, pass `0` to disable. Reach for hibernation when you want the *workspace* to persist but the *VM resources* released. The user-facing experience is "always on"; the runner-facing cost is "only when you're actively running." ## Next steps - [Create and execute](/sandboxes/create-and-execute/), the basic create/exec/destroy loop. - [Checkpoints and rollback](/sandboxes/checkpoints-and-rollback/), persist state across regions with snapshots. - [Pricing](/getting-started/pricing/), the per-second billing model. --- # Persistent Storage Source: /sandboxes/persistent-storage --- title: Persistent Storage description: Attach a durable persistent disk to a sandbox. --- By default a sandbox's `/workspace` is ephemeral — gone when the sandbox is destroyed. Attach a disk and `/workspace` becomes durable: it survives the sandbox, snapshots, and forks. Isorun disks are backed by [high-performance Tigris Cloud Disk](https://www.tigrisdata.com/docs/cloud-disk/) — blocks live in object storage behind a local NVMe write-back cache: a real block device, not FUSE, not a network filesystem. ## Attach a disk Pass `disk` to `create`. `/workspace` *is* the disk — formatted ext4 on first use and mounted for you. ```ts const sandbox = await isorun.create({ image: 'node:22-slim', disk: 'api-server' }) // install dependencies into the disk-backed /workspace await sandbox.exec('npm install') // node_modules lands on the disk await sandbox.destroy() // the disk survives const again = await isorun.create({ image: 'node:22-slim', disk: 'api-server' }) // /workspace is exactly as you left it — npm install already done ``` One sandbox holds a disk at a time. Tigris enforces it with a lease; a concurrent attach fails until the first detaches. ## Disk snapshots and forks A snapshot is a point-in-time checkpoint. A fork clones a snapshot into a new, independent disk — copy-on-write, so nothing is copied and only divergent writes cost space. **Restore** is a fork from a known-good snapshot — non-destructive, so the broken disk is untouched: ```ts await isorun.snapshotDisk('project', { name: 'green' }) // checkpoint a working state // ... a migration corrupts the disk ... const restored = await isorun.forkDisk('project', { snapshot: 'green', name: 'project-fix' }) await isorun.create({ image: 'node:22-slim', disk: 'project-fix' }) // back to 'green' ``` **Fan-out** is the same primitive at scale — prepare once, fork N isolated copies: ```ts const disk = await isorun.createDisk({ name: 'rl-env', sizeGb: 500 }) const prep = await isorun.create({ image: 'python:3.12-slim', disk: 'rl-env' }) await prep.exec('pip install torch gymnasium && git clone https://github.com/org/env /workspace/env') await prep.destroy() await isorun.snapshotDisk('rl-env', { name: 'ready' }) const runs = await Promise.all( Array.from({ length: 100 }, (_, seed) => isorun.create({ image: 'python:3.12-slim', forkFromDisk: 'rl-env', diskSnapshot: 'ready' }) .then((sb) => sb.exec(`python /workspace/env/train.py --seed=${seed}`)), ), ) ``` Each fork gets its own `/workspace`; writes in one are invisible to the others. Forking is metadata-only — instant, regardless of disk size. ## Manage disks Disks live independently of sandboxes. ```ts await isorun.createDisk({ name: 'datasets', sizeGb: 250 }) await isorun.listDisks() // your disks await isorun.snapshotDisk('datasets', { name: 'v1' }) await isorun.forkDisk('datasets', { snapshot: 'v1', name: 'datasets-v2' }) await isorun.deleteDisk('datasets-v2') // deletes the disk and its data ``` Disk names are lowercase letters, digits, and hyphens (≤ 30 chars). You can't delete a disk while other disks forked from it still exist — delete those forked disks first. ## How it behaves - **Real ext4.** The guest kernel mounts the disk directly — `node_modules`, git, SQLite, `mmap`, and file locks behave exactly as on local disk. No userspace filesystem layer. - **POSIX.** Fully POSIX-compliant disk. - **Sized like a real disk.** `df -h /workspace` shows the size you provisioned; a full disk returns `ENOSPC`. You pay for data stored, not size provisioned. - **Durable on `fsync`.** A successful `fsync` is committed. Writes hit the local NVMe write-log at NVMe latency and drain to Tigris in the background; a clean `destroy` or `hibernate` flushes the log before detaching. A host crash loses only the last few un-drained seconds — the filesystem recovers by journal replay, like a power cut. - **Attach is free.** A disk-backed sandbox starts as fast as one without. ## Billing $0.02 per GB-month for data stored — you pay for actual data, not provisioned size — plus the usual per-second compute while a disk is attached. No egress fees, no separate Tigris account; it's a line item on your bill. Forks add nothing beyond their divergent writes. ## Regions Disks are global Tigris buckets — a disk's blocks live in Tigris's globally distributed storage, so it attaches from a sandbox in any region. Destroy a sandbox and the disk detaches automatically; attach it to a new sandbox whenever you like, on any machine. --- # Checkpoints & Rollback Source: /sandboxes/checkpoints-and-rollback --- title: Checkpoints & Rollback description: Capture sandbox state, restore quickly, and fork prepared runtimes for parallel work. --- A checkpoint captures a sandbox's full state so you can restore it later or fork it into parallel copies. Use checkpoints when setup is expensive and you want repeatable branching. ## Snapshot and restore ```ts title="TypeScript" import { Isorun } from 'isorun' const isorun = new Isorun() const sandbox = await isorun.create({ image: 'python:3.12-slim' }) await sandbox.exec('pip install numpy pandas') const snap = await sandbox.snapshot() await sandbox.destroy() // Later, anywhere in the same region: const restored = await isorun.restore(snap.id) try { const r = await restored.exec("python3 -c 'import numpy; print(numpy.__version__)'") console.log(r.stdout.trim()) } finally { await restored.destroy() } ``` ## Fork for parallel hypotheses Prepare once, then fork multiple children for concurrent experiments. ```ts title="TypeScript" const parent = await isorun.create({ image: 'python:3.12-slim' }) await parent.exec('pip install transformers torch') const children = await parent.fork(5) try { await Promise.all( children.map((child, i) => child.exec(`python3 evaluate.py --variant=${i}`)) ) } finally { await Promise.all(children.map((c) => c.destroy())) await parent.destroy() } ``` ## What carries over - Filesystem state at fork/snapshot time. - Loaded runtime state (so setup usually does not need to re-run). - Environment configuration applied before checkpoint/fork. ## What resets - Active outbound network sessions. - In-flight operations at the time of checkpoint/fork. - Per-sandbox runtime control channel. ## Roll back to a snapshot ```ts title="TypeScript" const sandbox = await isorun.create({ image: 'python:3.12-slim' }) try { await sandbox.exec("echo 'state-ok' > /tmp/state.txt") const snap = await sandbox.snapshot() const failed = await sandbox.exec('rm /tmp/state.txt && exit 1') if (failed.exitCode !== 0) { await sandbox.destroy() const rolled = await isorun.restore(snap.id) const r = await rolled.exec('cat /tmp/state.txt') console.log(r.stdout.trim()) await rolled.destroy() } } finally { // sandbox may already be destroyed above; destroy() throws on a destroyed // sandbox, so guard for production code. } ``` `destroy()` throws if the sandbox is already destroyed. When a code path may destroy a sandbox before the `finally` block runs, guard the cleanup call. ## Operational checklist - Store snapshot IDs alongside job metadata. - Destroy restored/forked children in `finally` paths. - Set explicit `exec()` timeouts in child runs. - Call `isorun.deleteSnapshot(id)` when a snapshot is no longer needed. ## Next steps - [Lifecycle and hibernation](/sandboxes/lifecycle/), pause a single sandbox instead of snapshotting it. - [TypeScript SDK](/sdks/typescript/), `snapshot()`, `restore()`, and `fork()` reference. --- # Shell Sessions Source: /sandboxes/detachable-sessions --- title: Shell Sessions description: Long-lived bash sessions inside a sandbox. State persists across commands. --- A shell session is a single long-lived `bash` process inside the sandbox that survives across many commands. `cd`, `export`, shell variables, and background jobs all persist between calls, so the per-command fork cost a regular `sandbox.exec()` pays disappears. Reach for this primitive when an AI agent runs many small commands in sequence, `cd /repo && git diff && pytest && grep ...`, and for interactive workflows where the user expects state to stick. ## Why it's fast A normal `sandbox.exec("ls")` does this every call: 1. HTTP round-trip from your client to the runner. 2. A connection from the runner to the in-sandbox agent. 3. The agent forks `sh -c "ls"`. The shell forks `ls`. 4. `ls` runs. 5. The agent reads stdout, sends it back over the runtime channel. 6. HTTP response back to your client. For 100 commands you pay all of that 100 times. Inside a shell session: 1. **One** WebSocket connection for the whole session. 2. **One** persistent runtime channel between the runner and the agent. 3. **One** long-running `bash` process inside the VM. Commands stream in over its stdin; output streams out over stdout/stderr. 4. The agent injects a sentinel after each command so it knows where one command's output ends and the next begins. Exit codes ride along on the sentinel. Result: each command is a write to the already-running bash, you skip the per-call setup a separate exec pays every time. ## Open a session over WebSocket The TypeScript SDK ships a `sandbox.shell()` helper in an upcoming release. Until it lands, talk to the WebSocket endpoint directly using your client of choice. The protocol below is the wire format the helper will speak. `sandbox.shell()` isn't in the SDK yet. The WebSocket protocol shown here is stable and is what the helper will wrap once it ships. ```ts title="TypeScript" import { WebSocket } from 'ws' import { Isorun } from 'isorun' const isorun = new Isorun() const sandbox = await isorun.create({ image: 'python:3.12-slim' }) const ws = new WebSocket( `${isorun.apiUrl.replace(/^http/, 'ws')}/v1/runs/${sandbox.id}/shell`, { headers: { Authorization: `Bearer ${process.env.ISORUN_API_KEY}` } }, ) await new Promise((resolve, reject) => { ws.once('open', () => resolve()) ws.once('error', reject) }) function run(id: string, command: string): Promise<{ exitCode: number; stdout: string; stderr: string }> { return new Promise((resolve) => { let stdout = '', stderr = '' const handler = (raw: any) => { const msg = JSON.parse(raw.toString()) if (msg.id !== id) return if (msg.type === 'shell_out') stdout += msg.data else if (msg.type === 'shell_err') stderr += msg.data else if (msg.type === 'shell_exit') { ws.off('message', handler) resolve({ exitCode: msg.code, stdout, stderr }) } } ws.on('message', handler) ws.send(JSON.stringify({ type: 'shell_run', id, command })) }) } try { await run('r1', 'cd /tmp') // cwd persists await run('r2', 'export FOO=bar') // env persists const r = await run('r3', 'echo $FOO') console.log(r.stdout) // bar } finally { ws.close() await sandbox.destroy() } ``` ## What persists across commands - **cwd** (`cd` works) - **environment variables** (`export FOO=bar`) - **shell variables** (`X=42; echo $X`) - **shell functions and aliases** - **background jobs** (`./long-running &` keeps running) - **shell history** (in-memory) What doesn't persist: anything that requires re-launching the shell itself (`exec` to replace the shell, `exit`). If your command kills the shell, the session ends and the client receives a `shell_closed` frame. ## Streaming output The shell streams stdout and stderr separately and incrementally, you don't have to wait for the command to exit to see partial output. The WebSocket frames are `shell_out` and `shell_err` chunks that arrive in real time. The helper class will surface this as a callback API; the raw WebSocket above already gives you the chunks. ## Limits | Constraint | Value | |---|---| | Sessions per sandbox | unbounded; each is one `bash` process | | Session timeout | sandbox lifetime (or until you close the WebSocket) | | Per-command overhead | minimal: one write to the persistent bash | | Bash death detection | `shell_closed` frame surfaces immediately | ## Choose between exec and shell | Use `sandbox.exec()` | Use the shell session | |---|---| | One-off command per sandbox | Many commands in sequence | | You want command isolation | You want shared cwd / env / state | | Don't care about per-call latency | Latency matters (agent loop, REPL) | | Stateless tools | Stateful workflows | ## Next steps - [Create and execute](/sandboxes/create-and-execute/), the stateless `exec` path. - [Computer use (CDP + VNC)](/sandboxes/computer-use-vnc/), another long-lived in-sandbox service over WebSocket. - [TypeScript SDK](/sdks/typescript/), full method reference. --- # Port forwarding Source: /sandboxes/public-urls --- title: Port forwarding description: Reach any port your in-sandbox code is listening on over HTTPS. Authenticated and WebSocket-aware. --- `sandbox.url(port)` returns a URL that proxies to whatever your sandbox is listening on at that port. Start a `vite` dev server, a `jupyter notebook`, a `gradio` app, or any HTTP server inside the sandbox and reach it over HTTPS. The URL is **authenticated, not public**. It lives under `/v1`, so every request needs your `Authorization: Bearer ` header — a browser opening it directly gets `401`. Proxy it from your own backend, or attach the header from your client, rather than handing the raw URL to an end user. ## Preview a dev server ```ts title="TypeScript" import { Isorun } from 'isorun' const isorun = new Isorun() const sandbox = await isorun.create({ image: 'node:22-slim' }) await sandbox.exec('git clone https://github.com/me/my-app && cd my-app && npm install') // Start vite in the background. nohup + setsid + redirect so the exec // call returns immediately and the server keeps running. await sandbox.exec('cd my-app && nohup setsid npm run dev >/dev/null 2>&1 &') // Reach the dev server through the runner. const url = sandbox.url(5173) console.log(url) // → https://run-.isorun.ai/v1/runs//proxy/5173/ const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.ISORUN_API_KEY}` }, }) ``` ## URL format The URL is a path on the runner that already serves your API: ``` /v1/runs//proxy//? ``` | Component | Meaning | |---|---| | `` | Runner base URL, `isorun.apiUrl` — derived from your key's region | | `` | Sandbox ID returned by `sandbox.id` | | `` | TCP port your in-sandbox server is listening on | | `` | Forwarded verbatim to the in-sandbox server | | `` | Forwarded verbatim | So `.../v1/runs/run0123456789abcdef/proxy/8000/foo/bar?q=1` becomes a request to `localhost:8000/foo/bar?q=1` inside the sandbox. `sandbox.url(port, path)` takes an optional second argument for the path, so `sandbox.url(8000, '/foo/bar')` builds the same URL for you. ## What passes through the proxy - **All HTTP methods.** GET, POST, PUT, DELETE, PATCH, anything. - **Streaming responses.** SSE, chunked encoding, etc. pass through without buffering. - **WebSocket upgrades.** Vite HMR, Jupyter kernel WS, hot-reload, notebook websockets, gradio's queue WS, langchain streaming, all work end-to-end. - **Cookies.** Forwarded both directions. - **Large request and response bodies.** No size cap from the proxy. ## Authentication model The URL is anonymous: anyone with the URL can hit it. The sandbox ID in the host is high-entropy, so it's effectively unguessable unless you share it. The sandbox ID is the only credential on a public URL. Treat any URL you generate as a bearer secret: sharing it grants access to that port. If you need stronger auth in front of your in-sandbox service, run it behind a reverse proxy with auth inside the sandbox itself, or ship a per-port allowlist via your own gateway. What gets added to the forwarded request: - `X-Forwarded-Host` - `X-Forwarded-Proto: https` - `X-Real-IP: ` ## Performance Most of the round-trip is network distance between the client and the runner. For traffic that needs the lowest latency, hold a persistent connection to the URL: HTTP keep-alive, WebSocket, or streaming SSE. ## Limits | Constraint | Value | |---|---| | Ports per sandbox | any TCP port your code listens on | | Method support | all HTTP methods including PATCH and CUSTOM | | WebSocket support | yes, end-to-end | | Auth | none; the sandbox ID is the credential | ## Next steps - [Computer use (CDP + VNC)](/sandboxes/computer-use-vnc/), expose Chrome DevTools and noVNC through the same proxy. - [TypeScript SDK](/sdks/typescript/), the `url(port, path?)` reference. --- # Any OCI Image Source: /sandboxes/any-oci-image --- title: Any OCI Image description: Use any Docker Hub / GHCR / private-registry image as a sandbox base. First boot pulls and builds; every subsequent boot is fast. --- Pass any OCI image reference to `isorun.create({ image })` and the runner pulls it on first use, prepares an optimized cache, and reuses that cache for subsequent boots of the same image. The cache build is one-time per `(image, runner)` pair, after that, every `isorun.create()` for that image returns to the standard low-latency startup path. ## Examples ```ts title="TypeScript" import { Isorun } from 'isorun' const isorun = new Isorun() // Standard library images, no separate setup needed await isorun.create({ image: 'python:3.12-slim' }) await isorun.create({ image: 'node:22-slim' }) await isorun.create({ image: 'ubuntu:24.04' }) await isorun.create({ image: 'alpine:3.20' }) await isorun.create({ image: 'ruby:3.3' }) await isorun.create({ image: 'golang:1.23' }) await isorun.create({ image: 'rust:1.83' }) // Specialized agent images await isorun.create({ image: 'mcr.microsoft.com/playwright:v1.48.0' }) await isorun.create({ image: 'jupyter/scipy-notebook:python-3.12' }) // Your own image (build + push, then reference by ref) await isorun.create({ image: 'ghcr.io/your-org/agent-runtime:1.0' }) await isorun.create({ image: 'docker.io/youraccount/data-pipeline:v3' }) // Private registries, same syntax, configure the runner with creds await isorun.create({ image: 'registry.internal.example.com/team/agent:latest' }) ``` ## How it works 1. The first time a runner sees an image reference, it fetches and validates the image. 2. It prepares an optimized runtime artifact for that image so startup paths stay predictable. 3. The runtime injects the control agent needed for `exec`, hibernate, networking, and observability APIs. 4. That optimized artifact is cached per `(image, runner)` so subsequent `isorun.create()` calls reuse it. 5. After the first build, sandbox startup returns to the normal low-latency path for that image. The first build takes 10 to 60 seconds depending on image size and network. Every boot after that is the standard cold-start. ## Pin your image tags `latest`, `lts`, and `stable` tags are rejected at create time. They change without notice and would silently invalidate the cached artifact when the upstream tag is republished. Use a pinned tag (`python:3.12`, `node:22-slim`) or a digest reference (`python@sha256:...`). The `latest`, `lts`, and `stable` tags are rejected at create time. Pin an explicit tag or digest so the cached artifact stays valid. ## Pre-build an image To amortize the first-boot pull (for example, in CI before a launch), call the build endpoint. It runs the same prep work the first `create()` would do: ```bash title="Terminal" curl -X POST https://run-us.isorun.ai/v1/images/build \ -H "Authorization: Bearer $ISORUN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"image": "your-image:1.0"}' ``` ## Use custom image content Inside the sandbox, your image is mounted as an immutable base with a writable scratch layer on top. Anything user code writes to `/`, `/tmp`, `/home`, etc. lands on the scratch layer and is preserved for the sandbox's lifetime (including across hibernate/resume cycles). The image's `ENTRYPOINT` and `CMD` are not run, the init script replaces them with the isorun guest agent. If your image starts a daemon on boot, wrap it in a script and call it after `isorun.create()`. If your image has a startup hook (e.g. a daemon that should launch on boot), wrap it in a script and call it after `isorun.create()`: ```ts title="TypeScript" const sandbox = await isorun.create({ image: 'docker.io/your-team/agent:1.0' }) await sandbox.exec('/usr/local/bin/start-services.sh & disown', 2) ``` The image's environment variables (`PATH`, `NODE_VERSION`, etc.) *are* applied, the runner reads the image config at build time and injects them via the agent during boot. ## Next steps - [Lifecycle and hibernation](/sandboxes/lifecycle/), how the scratch layer survives hibernate/resume. - [Computer use (CDP + VNC)](/sandboxes/computer-use-vnc/), a custom browser image in action. - [TypeScript SDK](/sdks/typescript/), the `create({ image })` reference. --- # Browser & computer use Source: /sandboxes/computer-use-vnc --- title: Browser & computer use description: Chromium with CDP and a live noVNC desktop inside a microVM. Drive it with Playwright or browser-use, watch it live, and run agent loops. --- Run Chromium with the Chrome DevTools Protocol (CDP) and a live noVNC desktop inside an Isorun microVM. Point a CDP client at it (Playwright, pyppeteer, browser-use), and open the noVNC URL to watch the session in real time. The image ships Xvfb (a virtual display), a window manager, Chromium started with `--remote-debugging-port=9222`, x11vnc, and noVNC. The port proxy exposes CDP and the noVNC web UI through the same authenticated `sandbox.url(port)` addresses as any other in-sandbox service. You don't need to handle bind-address quirks. Services that default to localhost-only, including Chromium's CDP, are reached automatically when you call `sandbox.url(port)`. What you get: - **CDP on port 9222.** Connect Playwright, pyppeteer, browser-use, or any client that speaks the Chrome DevTools Protocol. - **noVNC web UI on port 6080.** Open it in any browser to watch the agent operate Chromium in real time. WebSocket pass-through is end-to-end, so the live screen updates without extra setup. ## Start the sandbox ```ts title="TypeScript" import { Isorun } from 'isorun' const isorun = new Isorun() const sandbox = await isorun.create({ image: 'docker.io/isorun/computer-use:1', vcpus: 2, memMiB: 4096, }) try { // Start the desktop. & disown so exec returns immediately and the // server keeps running. await sandbox.exec('/usr/local/bin/start-vnc.sh & disown', 2) await new Promise((r) => setTimeout(r, 2000)) // wait for binds console.log('CDP :', sandbox.url(9222)) // /v1/runs//proxy/9222/ console.log('noVNC :', sandbox.url(6080)) // /v1/runs//proxy/6080/ } finally { await sandbox.destroy() } ``` ## Drive it with Playwright ```ts title="TypeScript" import { chromium } from 'playwright' import { Isorun } from 'isorun' const isorun = new Isorun() const sandbox = await isorun.create({ image: 'docker.io/isorun/computer-use:1', vcpus: 2, memMiB: 4096, }) try { await sandbox.exec('/usr/local/bin/start-vnc.sh & disown', 2) await new Promise((r) => setTimeout(r, 2000)) const browser = await chromium.connectOverCDP(sandbox.url(9222)) const ctx = browser.contexts()[0] ?? (await browser.newContext()) const page = await ctx.newPage() await page.goto('https://example.com') console.log(await page.title()) await page.screenshot({ path: '/tmp/screenshot.png' }) await browser.close() } finally { await sandbox.destroy() } ``` The same CDP endpoint works with `browser-use`, pyppeteer, or anything else that connects over the DevTools Protocol. ## Watch the agent in real time Open `sandbox.url(6080)` in any browser. You get the noVNC web UI showing the live Chromium session: mouse moves, page loads, typed input, all of it. It's useful for debugging agent loops and for building supervised flows where a human watches the agent. The page connects to a WebSocket VNC endpoint inside the sandbox. Isorun handles the protocol bridge, so the desktop session streams without custom network setup. ## Use it with Anthropic or OpenAI computer use Anthropic's computer use tool and OpenAI's computer-use model both work as a screenshot-and-action loop: the model looks at a screenshot, returns an action (click, type, key, scroll), your code executes that action, and you send back a fresh screenshot. **The model never connects to the browser, and there is no CDP URL to hand it.** That is the key difference from the Playwright path above, where your code drives Chromium directly over CDP. To run that loop against an Isorun sandbox, the sandbox provides the desktop (the Xvfb display you can watch over noVNC), and your agent loop captures screenshots and injects mouse and keyboard events inside the sandbox through `sandbox.exec`. For that you need an input tool such as `xdotool` in your image. Anthropic's [reference implementation](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo) (Xvfb plus a window manager plus xdotool) is the simplest starting point. The current tool version is `computer_20251124` with the `computer-use-2025-11-24` beta header. For most browser tasks the CDP path above (Playwright or browser-use) is faster, cheaper, and more reliable than pixel-level computer use. Reach for computer use when there is no clean DOM to drive, such as a native desktop app or a canvas UI. ## Size the sandbox - **vCPUs:** 2 minimum. Chromium is slow when CPU-starved. - **Memory:** 4 GiB minimum, more if you'll have many tabs open. - **Disk:** the default 4 GiB scratch is enough. Bump it via `diskMiB` if you expect downloads or large session storage. A browser sandbox costs the same as any sandbox at the same vCPU + RAM (`$0.025/vCPU-hr + $0.015/GiB-hr`, see [pricing](/getting-started/pricing/)), and like every other Isorun sandbox you can `sandbox.hibernate()` it when idle and pay nothing until the user comes back. ## Next steps - [Public URLs](/sandboxes/public-urls/) shows how the CDP and noVNC ports are exposed. - [Lifecycle and hibernation](/sandboxes/lifecycle/) covers hibernating an idle browser session. - [Any OCI image](/sandboxes/any-oci-image/) explains how to build your own browser image. --- # Network Filtering Source: /security/network-filtering --- title: Network Filtering description: Per-sandbox egress allow/deny lists. Block by default, allow what you trust. --- Control which hosts a sandbox can reach with a per-sandbox egress policy. Every sandbox has its own egress policy. By default the policy is "unrestricted", the VM can reach any host on the public internet. For agent code that runs untrusted scripts or that should only talk to specific upstreams, pass `network: { allow, deny }` at create time. ## Allow only what you trust The most common pattern: deny everything, then allow only the hosts you need. ```ts title="TypeScript" import { Isorun } from 'isorun' const isorun = new Isorun() const sandbox = await isorun.create({ image: 'python:3.12-slim', network: { deny: ['0.0.0.0/0'], // block everything allow: ['api.openai.com', 'pypi.org'], // …except these }, }) try { await sandbox.exec('pip install openai') // works const r = await sandbox.exec('curl -m 2 https://example.com') console.log(r.exitCode !== 0) // true (blocked) } finally { await sandbox.destroy() } ``` ## Block specific hosts For policies where you trust most of the internet but want to block specific upstreams: ```ts title="TypeScript" const sandbox = await isorun.create({ image: 'python:3.12-slim', network: { deny: ['169.254.169.254', 'metadata.google.internal'] }, }) try { await sandbox.exec('curl https://example.com') // works const r = await sandbox.exec('curl -m 2 http://169.254.169.254/') console.log(r.exitCode !== 0) // true } finally { await sandbox.destroy() } ``` ## Rule shape Rules are matched against the destination of every outbound TCP connection from the sandbox's network interface. - **CIDR ranges**: `0.0.0.0/0`, `10.0.0.0/8`, `192.168.1.0/24` - **IP literals**: `1.2.3.4` - **Hostnames**: `api.openai.com`, matched literally against the destination's SNI - **Wildcards**: `*.openai.com` matches `api.openai.com`, `cdn.openai.com`, etc. A wildcard like `*.openai.com` does not match the bare apex `openai.com`. List the apex separately if you need it. The order of evaluation: deny rules first, then allow. A connection is permitted iff it matches an allow rule and does not match any deny rule. ## How it works The runner applies egress policy at the host boundary before traffic leaves the sandbox. Requests are checked against the sandbox policy and either allowed or blocked. DNS and hostname checks are kept in sync so domain allowlists stay accurate. Policy is enforced outside guest code execution. Changes made inside the VM only affect the guest's view and cannot disable runtime egress enforcement. Enforcement lives at the host boundary, not inside the guest. Code running in the sandbox can't edit iptables, swap the resolver, or otherwise disable the policy. ## Combine with named profiles If your allow list is one of the common patterns (Anthropic, OpenAI, HuggingFace, etc.), pass `networkProfile` instead, it's the same machinery with a friendlier name. See [Network Profiles](/security/network-profiles/). ## Per-endpoint filtering For finer-grained control (allow `POST /v1/chat/completions` but deny `DELETE /v1/admin/...`), use [Endpoint Rules](/security/endpoint-rules/) together with [credential injection](/security/credential-injection/). ## Next steps - [Network profiles](/security/network-profiles/), named allow/deny templates for common workloads. - [Endpoint rules](/security/endpoint-rules/), narrow the credential proxy to specific methods and paths. - [Audit trail](/security/audit-trail/), every blocked egress is logged. --- # Network Profiles Source: /security/network-profiles --- title: Network Profiles description: Named egress policy templates for common agent workloads. --- Apply a named egress policy template instead of assembling rules by hand. Network profiles are pre-built allow/deny lists for common configurations. Pass `networkProfile` at sandbox creation to apply one without assembling the rules yourself. ```ts title="TypeScript" import { Isorun } from 'isorun' const isorun = new Isorun() // All available profiles: const profiles = await isorun.networkProfiles() console.log(profiles.map((p) => p.name)) // → ['unrestricted', 'locked-down', 'claude-code', 'openai', ...] ``` ## Available profiles | Profile | Description | Mode | |---|---|---| | `unrestricted` | No filtering, sandbox can reach any host | Allow-all | | `locked-down` | Air-gapped, no external network at all | Deny-all | | `claude-code` | Anthropic API + PyPI + npm + GitHub + crates.io | Allow-list | | `openai` | OpenAI API + PyPI + npm + GitHub | Allow-list | `isorun.networkProfiles()` returns the live catalogue from the runner, new profiles show up there without an SDK update. ```ts title="TypeScript" // Locked-down, sandbox can't reach anything outside the runner. const a = await isorun.create({ image: 'python:3.12-slim', networkProfile: 'locked-down' }) try { const r = await a.exec('curl -m 2 https://example.com') console.log(r.exitCode !== 0) // true, blocked } finally { await a.destroy() } // claude-code profile, agent can reach Anthropic + package registries. const b = await isorun.create({ image: 'python:3.12-slim', networkProfile: 'claude-code' }) try { await b.exec('pip install anthropic') // works const r = await b.exec('curl -m 2 https://random-site.com') console.log(r.exitCode !== 0) // true } finally { await b.destroy() } ``` ## Mix with explicit allow/deny `networkProfile` and the explicit `network: { allow, deny }` option are mutually exclusive, pick one. If a profile is *almost* what you want, list the rules directly and tweak. You can't pass both `networkProfile` and `network: { allow, deny }` on the same create call. Choose one. ## Profile internals Profiles are server-side; the SDK just passes the name. The runtime enforcer blocks outbound traffic that does not match the profile allowlist. DNS and hostname checks are maintained alongside policy updates. See [Network Filtering](/security/network-filtering/) for the raw allow/deny list shape and [Endpoint Rules](/security/endpoint-rules/) for per-method/per-path filtering. ## Next steps - [Network filtering](/security/network-filtering/), the raw allow/deny rule shape. - [Endpoint rules](/security/endpoint-rules/), per-method and per-path controls. - [Credential injection](/security/credential-injection/), keep API keys out of the guest. --- # Endpoint Rules Source: /security/endpoint-rules --- title: Endpoint Rules description: Restrict the host credential proxy to specific HTTP method + path combinations. --- Endpoint rules narrow what the host credential proxy is willing to forward. The base feature ([Credential Injection](/security/credential-injection/)) lets the agent talk to a named service through the proxy. Endpoint rules add method + path controls on top, so the agent's `OPENAI_API_KEY` can only call `POST /v1/chat/completions`, not `DELETE /v1/admin/*`. The TypeScript SDK currently exposes the simple `credentials: { svc: key }` form. The structured shape below is supported by the server and the REST API today; first-class TS SDK helpers ship in a future release. Use the REST API or hand-roll the request until then. ## Wire shape (`credentials_v2`) ```json title="JSON" { "image": "python:3.12-slim", "credentials_v2": { "openai": { "key": "sk-real-openai-key", "endpoints": [ { "methods": ["POST"], "paths": ["/v1/chat/completions"] }, { "methods": ["GET"], "paths": ["/v1/models"] } ] } } } ``` A request through the credential proxy is forwarded only if its `(method, path)` matches one of the entries. Anything else is rejected at the proxy with a 4xx. ## Rule fields | Field | Required | Description | |---|---|---| | `methods` | Yes | One or more HTTP methods. Empty array means "any method." | | `paths` | Yes | Path prefixes or globs (e.g. `/repos/myorg/*`). | ## Common policies ### Read-only GitHub access ```json title="JSON" { "credentials_v2": { "github": { "key": "ghp_...", "endpoints": [ { "methods": ["GET"], "paths": ["/repos/*"] }, { "methods": ["GET"], "paths": ["/user"] } ] } } } ``` ### LLM inference only ```json title="JSON" { "credentials_v2": { "openai": { "key": "sk-...", "endpoints": [ { "methods": ["POST"], "paths": ["/v1/chat/completions"] }, { "methods": ["POST"], "paths": ["/v1/embeddings"] }, { "methods": ["GET"], "paths": ["/v1/models"] } ] } } } ``` ## Combination model A request must pass both filters: 1. The sandbox's network policy (allow-list / deny-list / named profile) has to permit the destination host. 2. Each `(method, path)` has to match an endpoint rule for the credential that authenticates it. Use this alongside [Network Profiles](/security/network-profiles/) for reusable policy baselines. ## Next steps - [Credential injection](/security/credential-injection/), the base proxy these rules narrow. - [Network profiles](/security/network-profiles/), reusable host-level egress baselines. - [Audit trail](/security/audit-trail/), proxied requests are logged without the credential. --- # Credential Injection Source: /security/credential-injection --- title: Credential Injection description: Inject API keys into a sandbox without exposing the raw secret to the agent's environment. --- The agent inside a sandbox needs to call third-party APIs (OpenAI, Anthropic, GitHub, HuggingFace, etc.). The naive way is to set `OPENAI_API_KEY=sk-...` as a sandbox env var, but then the agent's process can read it from `process.env`, dump it to a file, exfiltrate it via DNS, or include it in an LLM prompt by accident. Credential injection solves this by holding the real secret on the **host** and never letting it cross into the guest VM. The host runs a reverse proxy that watches outbound HTTPS connections from the sandbox; when one matches a known service URL, it inserts the matching credential into the request before forwarding upstream. ## Inject a credential ```ts title="TypeScript" import { Isorun } from 'isorun' const isorun = new Isorun() const sandbox = await isorun.create({ image: 'python:3.12-slim', credentials: { openai: process.env.OPENAI_API_KEY!, anthropic: process.env.ANTHROPIC_API_KEY!, github: process.env.GITHUB_TOKEN!, }, }) try { // The agent's environment has placeholder values: await sandbox.exec("env | grep -E 'OPENAI|ANTHROPIC|GITHUB'") // OPENAI_API_KEY=sk-isorun-proxy-managed // OPENAI_BASE_URL=http://169.254.169.254:/proxy/openai // ANTHROPIC_API_KEY=sk-ant-isorun-proxy-managed // … // And the agent's HTTPS calls work, SDKs that honor // *_BASE_URL pick up the proxy automatically. await sandbox.exec("python3 -c 'import openai; print(openai.models.list())'") } finally { await sandbox.destroy() } ``` The agent never sees the real secret. Printing `process.env.OPENAI_API_KEY` or reading `/proc/self/environ` returns the placeholder. The only thing the agent can do with it is make HTTPS calls through the proxy, which is exactly what you want. ## Recognised service keys The proxy maps each credential map key to a known service. Pass the service short-name as the map key: | Key | Service hostname | |---|---| | `openai` | `api.openai.com` | | `anthropic` | `api.anthropic.com` | | `google` | `generativelanguage.googleapis.com` | | `github` | `api.github.com` | | `huggingface` | `api-inference.huggingface.co` | For each known service the proxy sets the right env vars inside the guest (`OPENAI_API_KEY` + `OPENAI_BASE_URL`, etc.) and injects the matching auth header on outbound requests through the proxy. ## Per-endpoint filtering For tighter control, allow `POST /v1/chat/completions` but reject `DELETE /v1/admin/...`, use [Endpoint Rules](/security/endpoint-rules/) to specify which methods + paths the proxy is willing to forward for each credential. ## Combine with network profiles Credential injection composes cleanly with [network profiles](/security/network-profiles/). The profile blocks all egress except the API hosts you trust; credential injection ensures the agent's calls to those hosts go out with the right key, without ever putting the key in the guest's address space. ## Next steps - [Endpoint rules](/security/endpoint-rules/), restrict the proxy to specific methods and paths. - [Network profiles](/security/network-profiles/), block egress to everything but trusted hosts. - [Audit trail](/security/audit-trail/), proxied requests are logged, never the credential. --- # Audit Trail Source: /security/audit-trail --- title: Audit Trail description: Tamper-evident NDJSON log of every event in a sandbox's lifetime. Per-tenant. --- Every sandbox has a tamper-evident audit log that records lifecycle, command, file-transfer, credential-proxy, and blocked-egress events with a chained HMAC, so any change to a past entry is detectable. The chain is keyed with a per-team secret. ## Read the log ```ts title="TypeScript" import { Isorun } from 'isorun' const isorun = new Isorun() const sandbox = await isorun.create({ image: 'python:3.12-slim' }) try { await sandbox.exec('echo hello') await sandbox.exec("python3 -c 'print(2 + 2)'") for (const entry of await sandbox.auditLog()) { console.log(entry.timestamp, entry.event, entry.data) } // 2026-05-26T… sandbox.created { image: 'python:3.12-slim', vcpus: 1, ... } // 2026-05-26T… exec.start { command: 'echo hello' } // 2026-05-26T… exec.exit { command: 'echo hello', exit_code: 0, duration_ms: 12 } // 2026-05-26T… exec.start { command: "python3 -c 'print(2 + 2)'" } // 2026-05-26T… exec.exit { command: "python3 -c 'print(2 + 2)'", exit_code: 0, duration_ms: 41 } } finally { await sandbox.destroy() } ``` Each entry is a JSON object with at least: ```json title="JSON" { "seq": 3, "ts": "2026-05-26T14:23:01.412Z", "sandbox_id": "run...", "event": "exec.exit", "data": { "command": "echo hello", "exit_code": 0 }, "hmac": "sha256:abc123..." } ``` ## Verify the chain Each entry is chained to the previous one with a keyed hash, so tampering with any entry breaks the chain at that point and every later entry. You can verify the chain locally without round-tripping to the runner: pull your team secret from the dashboard once and run the verifier against the log. ## What's logged | Event | When | Data | |---|---|---| | `sandbox.created` | Create returns | `image`, `vcpus`, `mem_mib` | | `sandbox.forked` | A child is forked from this run | `parent`, `image` | | `exec.start` | Each `exec()` | `command` | | `exec.exit` | Same call returns | `command`, `exit_code`, `duration_ms` | | `file.upload` | A file is written into the sandbox | `path`, `size` | | `file.download` | A file is read out of the sandbox | `path`, `size` | | `credential.proxied` | A proxied credentialed request is made | `service`, `method`, `path`, `status` (never the credential) | | `network.blocked` | An egress connection is denied by policy | `domain` or `ip` + `port`, `reason` | | `sandbox.destroyed` | `destroy()` or auto-destroy | `cpu_ms`, `mem_peak_bytes`, `uptime_ms` | Command output (stdout/stderr) is never recorded, only the command line, its exit code, and duration. File events record path and size, not contents; credential-proxy events never include the credential. The log is flushed to object storage (R2) on destroy (best-effort) so you have a permanent audit trail even after the runner is torn down. ## Guarantees 1. **Append-only verification.** Anyone with the team secret can verify any prefix of the log without needing the later entries, useful for streaming verification during long-running sessions. 2. **Per-tenant isolation.** Each team has its own secret, so a leaked verification key only affects that one team's audit trail. There is no API to mutate or delete entries. The only way to break the chain is to tamper with the stored file, which you would detect on the next verification. The flush to object storage on destroy is best-effort. Read the log before you destroy a sandbox if you need a guaranteed copy in hand. ## Next steps - [Network filtering](/security/network-filtering/), blocked egress shows up as `network.blocked` entries. - [Credential injection](/security/credential-injection/), proxied calls are logged without the secret. - [Endpoint rules](/security/endpoint-rules/), narrow what the credential proxy will forward. --- # TypeScript SDK Source: /sdks/typescript --- title: TypeScript SDK description: Official Node.js client for sandbox lifecycle, command execution, file I/O, snapshots, fork, hibernate, and port exposure. --- The `isorun` package is the canonical client for the Isorun API. ESM only, Node 20+. ## Install ```bash title="Terminal" npm install isorun ``` ## Authenticate ```bash title="Terminal" export ISORUN_API_KEY="isorun_live_..." ``` The runner endpoint is derived from the region segment in your key (`isorun_live_us_...` → `https://run-us.isorun.ai`), so there's nothing to configure. ## Quickstart ```ts title="TypeScript" import { Isorun } from 'isorun' const isorun = new Isorun() const sandbox = await isorun.create({ image: 'node:22-slim' }) try { const { stdout } = await sandbox.exec('node -v') console.log(stdout) // v22.x.x } finally { await sandbox.destroy() } ``` --- ## `Isorun` ```ts title="TypeScript" new Isorun({ apiKey?: string }) ``` | Option | Default | Notes | |----------|------------------------------------------------------|-------| | `apiKey` | `ISORUN_API_KEY` env | Throws if neither is set. | ### Properties - `apiUrl: string`, the resolved runner endpoint, useful when building external URLs. ### Methods | Method | Returns | Description | |---|---|---| | `create(options?)` | `Promise` | Boot a new sandbox. | | `get(id)` | `Promise` | Look up an existing sandbox; `null` on 404. | | `list()` | `Promise` | All active sandboxes for the API key. | | `restore(snapshotId)` | `Promise` | Restore from a snapshot. | | `listSnapshots()` | `Promise` | List your snapshots. | | `deleteSnapshot(id)` | `Promise` | Delete one snapshot. | | `networkProfiles()` | `Promise` | List the named egress profiles available on the runner. | | `usage()` | `Promise` | Live usage and cost summary for the account. | | `history()` | `Promise` | Past (destroyed) sandbox runs, newest first. | | `close()` | `void` | Release the connection pool so the process can exit cleanly. | ### `create(options?)` ```ts title="TypeScript" const sandbox = await isorun.create({ image: 'python:3.12-slim', // any supported OCI image; default: 'node:22-slim' vcpus: 1, // default 1 memMiB: 1024, // default 1024 diskMiB: 4096, // scratch disk; wiped on destroy timeoutSec: 300, // auto-destroy after N idle seconds }) ``` ### `networkProfiles()` Lists the named egress profiles you can pass as `networkProfile` on `create()`. ```ts title="TypeScript" const profiles = await isorun.networkProfiles() // [{ name: 'openai', description?, allow: string[], deny: string[] }, ...] ``` ### `usage()` Live usage and cost summary for the account. ```ts title="TypeScript" const u = await isorun.usage() // { activeSandboxes, historySandboxes, activeCostCents, historyCostCents, // totalCostCents, cpuSeconds, memSeconds, asOf } ``` ### `history()` Past (destroyed) sandbox runs, newest first. ```ts title="TypeScript" const runs = await isorun.history() // [{ id, image, vcpus, memMiB, createMs, createdAt, destroyedAt, // uptimeMs, cpuMs, memPeakBytes, costCents }, ...] ``` --- ## `Sandbox` A `Sandbox` is the handle to a running microVM. ### Properties - `id: string`, unique sandbox ID (`run...`). - `createMs: number`, server-side create duration in ms. - `image: string`, resolved image name. ### Methods #### `exec(command, timeoutSec?)` ```ts title="TypeScript" const { exitCode, stdout, stderr } = await sandbox.exec('pytest -q', 180) if (exitCode !== 0) console.error(stderr) ``` Runs `command` in the guest. `timeoutSec` defaults to 30. Stdout and stderr are buffered and returned together. #### `writeFile(path, content)` · `readFile(path)` · `readdir(path)` ```ts title="TypeScript" await sandbox.writeFile('/app/config.json', JSON.stringify({ env: 'prod' })) const text = await sandbox.readFile('/app/config.json') const entries = await sandbox.readdir('/app') ``` `content` accepts a string or `Buffer`. `readFile` returns UTF-8. #### `info()` ```ts title="TypeScript" const meta = await sandbox.info() // { id, status, image, vcpus, memMiB, diskMiB, createMs, createdAt } ``` #### `snapshot()` · `isorun.restore(id)` ```ts title="TypeScript" const snap = await sandbox.snapshot() // later, or elsewhere: const restored = await isorun.restore(snap.id) ``` `snap` is `{ id, runId, sizeBytes, createdAt }`. Snapshots persist until `deleteSnapshot()`. #### `fork(count?)` ```ts title="TypeScript" const children = await sandbox.fork(5) // 5 clones of the running sandbox await Promise.all(children.map((c) => c.exec('python3 evaluate.py'))) ``` Each child inherits filesystem + memory state at fork time and runs independently. Default count is 1. #### `hibernate()` · `resume()` ```ts title="TypeScript" await sandbox.hibernate() // pauses; releases active runtime resources // ... later ... await sandbox.resume() // back to running, same id, same state ``` Filesystem, memory, and processes survive. Established outbound TCP connections don't survive hibernate. Reconnect any long-lived connection after `resume()`. #### `setTimeout(seconds)` ```ts title="TypeScript" await sandbox.setTimeout(900) // extend auto-destroy timer ``` Use as a keep-alive when handing the sandbox to a long-running workflow. #### `url(port, path?)` ```ts title="TypeScript" const cdp = sandbox.url(9222) const api = sandbox.url(8080, '/health') ``` Returns the URL that proxies to a port your in-sandbox service is listening on. Synchronous, no network call. `url()` builds `/v1/runs//proxy//` and does no network I/O. The URL is **authenticated, not a capability**: it sits under `/v1`, so requests still need `Authorization: Bearer ` and it returns `401` without one. Do not hand it to a browser or an end user expecting it to load. #### `destroy()` ```ts title="TypeScript" const stats = await sandbox.destroy() // { status, cpuMs, memPeakBytes, uptimeMs, costCents } ``` Frees the sandbox, returns measured usage. Billing is computed from `uptimeMs`. --- ## Claude Managed Agents The SDK ships a self-hosted adapter for [Anthropic Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes). Each Anthropic session is bound to its own sandbox for the lifetime of the session. ```ts title="TypeScript" import { Isorun } from 'isorun' import { runOrchestrator } from 'isorun/claude-agents/orchestrator' await runOrchestrator({ isorun: new Isorun(), environmentId: 'env_...', // from Anthropic Console environmentKey: 'sk-ant-oat01-...', // self-hosted environment key image: 'docker.io/isorun/claude-agents:0.4.3', vcpus: 2, memMiB: 4096, }) ``` `@anthropic-ai/sdk` is an optional peer dependency, install it if you use this entry point: ```bash title="Terminal" npm install @anthropic-ai/sdk ``` --- ## Environment variable | Variable | Purpose | |------------------|-----------------------------------------------------| | `ISORUN_API_KEY` | API key (used when not passed to the constructor). | ## Errors All methods throw on non-2xx responses. The error message includes the HTTP status and the first 200 bytes of the response body, so you can distinguish `401` from `429` from `500` without extra plumbing. ## Production checklist - Always call `destroy()` in `finally`. - Set explicit `exec()` timeouts. - Log sandbox IDs alongside your request / job IDs for correlation. ## Next steps - [Create and execute](/sandboxes/create-and-execute/), the default runtime loop. - [Lifecycle and hibernation](/sandboxes/lifecycle/), the full state model behind these methods. - [API reference](/api-reference/), the underlying REST endpoints. --- # Claude Managed Agents Source: /integrations/claude-managed-agents --- title: Claude Managed Agents description: Self-hosted sandbox runtime for Anthropic Claude Managed Agents. One microVM per session, isolated for the lifetime of the session. --- [Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes) let you run Anthropic-orchestrated agents inside your own sandbox infrastructure. The Isorun SDK ships a self-hosted adapter that provisions one Isorun microVM per Anthropic session and binds every work item in that session to the same VM. ## What you get - **One sandbox per Anthropic session.** Successive turns in the same session share filesystem and process state. - **Per-sandbox isolation.** Sessions never see each other's data, every session is its own microVM with its own kernel. - **Hibernate-on-idle.** Server-side TTL reclaims unused sandboxes without you tracking them. - **Zero orchestration plumbing.** A single `runOrchestrator()` call polls Anthropic's work queue, allocates sandboxes, runs work items to completion, and reaps on shutdown. ## Install ```bash title="Terminal" npm install isorun @anthropic-ai/sdk ``` `@anthropic-ai/sdk` is an optional peer dependency of the SDK, only needed by this entry point. ## Configure Get an environment ID and key from the [Anthropic Console](https://console.anthropic.com) → Managed Agents → Self-hosted environment. ```bash title="Terminal" export ISORUN_API_KEY=isorun_live_... export ANTHROPIC_ENVIRONMENT_ID=env_... export ANTHROPIC_ENVIRONMENT_KEY=sk-ant-oat01-... ``` ## Run the orchestrator ```ts title="TypeScript" import { Isorun } from 'isorun' import { runOrchestrator } from 'isorun/claude-agents/orchestrator' await runOrchestrator({ isorun: new Isorun(), environmentId: process.env.ANTHROPIC_ENVIRONMENT_ID!, environmentKey: process.env.ANTHROPIC_ENVIRONMENT_KEY!, image: 'docker.io/isorun/claude-agents:0.4.3', vcpus: 2, memMiB: 4096, }) ``` `runOrchestrator()` runs until you abort it. It will: 1. Poll Anthropic's environment work queue. 2. For each new work item, allocate (or re-use) an Isorun sandbox for the session. 3. Stream the work item into the sandbox via a runner inside the VM. 4. Post results back to Anthropic. 5. Refresh the sandbox's auto-destroy timer between turns; let the server reap idle sandboxes when the TTL fires. ## Custom image `docker.io/isorun/claude-agents:0.4.3` ships: - Node 22 + `@anthropic-ai/sdk` pre-installed at `/opt/isorun-claude-runner` - `bash`, `unzip`, `tar`, `git`, `curl`, plus standard build tools - `/mnt/session` as the runner's working directory; the agent's final output files go under `/mnt/session/outputs` Bring your own image if you need extra tooling. Any OCI image works, just install Node 22 and `@anthropic-ai/sdk` and copy the runner from the published one. See `src/claude-agents/Dockerfile.base` in `isorun-sdk` for a starting point. ## Session → sandbox binding rules Override the default allocation per session by setting metadata on the Anthropic session before it starts: | Metadata key | Effect | |---|---| | `isorun.image` | Use a different image for this session | | `isorun.sandbox_id` | Attach to a pre-existing sandbox (BYOS) | When `isorun.sandbox_id` is set, the orchestrator never destroys that sandbox. You own its lifecycle, including cleanup. ## Shutdown `runOrchestrator()` honours an `AbortSignal`: ```ts title="TypeScript" const ac = new AbortController() process.on('SIGTERM', () => ac.abort()) await runOrchestrator({ isorun: new Isorun(), environmentId: process.env.ANTHROPIC_ENVIRONMENT_ID!, environmentKey: process.env.ANTHROPIC_ENVIRONMENT_KEY!, image: 'docker.io/isorun/claude-agents:0.4.3', vcpus: 2, memMiB: 4096, signal: ac.signal, }) ``` Drains in-flight work items, stops polling, and exits. Sandboxes allocated by the orchestrator are not destroyed on shutdown, the server-side TTL reaps them. ## Next steps - [TypeScript SDK](/sdks/typescript/), the `Isorun` client the orchestrator wraps. - [Any OCI image](/sandboxes/any-oci-image/), build a custom agent image. - [Lifecycle and hibernation](/sandboxes/lifecycle/), the TTL and idle-reclaim model. --- # OpenAI Agents Source: /integrations/openai-agents --- title: OpenAI Agents description: Run agent code inside Isorun sandboxes from the OpenAI Agents SDK. --- The `isorun/openai-agents` entry point gives you OpenAI Agents function tools backed by a single shared Isorun sandbox: a Python `code_interpreter` and a `shell`. Both run in one microVM, so multi-turn state (cwd, env vars, installed packages, files) persists across tool calls. ## Install ```bash title="Terminal" npm install isorun @openai/agents zod ``` ## Example agent ```ts title="TypeScript" import { Agent, run } from '@openai/agents' import { isorunTools } from 'isorun/openai-agents' const { tools, close } = await isorunTools() try { const coder = new Agent({ name: 'coder', instructions: 'You are a senior developer. Use the tools to write and run code that solves the task.', tools, }) const result = await run(coder, 'Compute 2**100 and explain the result.') console.log(result.finalOutput) } finally { await close() } ``` `isorunTools()` returns `{ tools, close }`. Pass `tools` to `new Agent({ tools })` and call `close()` when the agent loop is done to destroy the sandbox. ## What gets exposed | Tool | What it does | |---|---| | `code_interpreter(code)` | Runs Python in a microVM with internet, pip, and a full Python environment. Returns stdout/stderr. | | `shell(command)` | Runs a shell command in the same sandbox. Returns stdout/stderr. | Both tools share one sandbox, so state created by `shell` (`git clone`, `pip install`) is visible to subsequent `code_interpreter` calls and vice versa. ## Options ```ts title="TypeScript" const { tools, close } = await isorunTools({ image: 'python:3.12-slim', // any OCI image; default python:3.12-slim timeoutSec: 900, // idle auto-destroy, default 600 // apiKey defaults to ISORUN_API_KEY; pass `isorun: new Isorun()` to reuse a client }) ``` ## Next steps - [LangChain](/integrations/langchain/) for the same sandbox tools in LangChain. - [MCP server](/integrations/mcp-server/) to drive sandboxes from Claude Desktop, Cursor, or Zed. - [TypeScript SDK](/sdks/typescript/), the client behind these tools. --- # LangChain Source: /integrations/langchain --- title: LangChain description: Use Isorun sandboxes as LangChain.js tools. --- The `isorun/langchain` entry point returns LangChain.js tools backed by a single shared Isorun sandbox: a Python `code_interpreter` and a `shell`. Both run in one microVM, so state persists across agent turns. ## Install ```bash title="Terminal" npm install isorun @langchain/core zod ``` ## With LangGraph ```ts title="TypeScript" import { ChatOpenAI } from '@langchain/openai' import { createReactAgent } from '@langchain/langgraph/prebuilt' import { isorunTools } from 'isorun/langchain' const { tools, close } = await isorunTools() try { const agent = createReactAgent({ llm: new ChatOpenAI({ model: 'gpt-4o' }), tools, }) const res = await agent.invoke({ messages: [{ role: 'user', content: 'Compute the first 100 primes and print them.' }], }) console.log(res.messages.at(-1)?.content) } finally { await close() } ``` `isorunTools()` returns `{ tools, close }`. The tools share one sandbox and refresh its idle timer on every call, so it stays alive across turns. Call `close()` when you're done to destroy it. ## What gets exposed | Tool | What it does | |---|---| | `code_interpreter(code)` | Runs Python in a microVM with internet, pip, and a full Python environment. | | `shell(command)` | Runs a shell command in the same sandbox. | ## Options ```ts title="TypeScript" const { tools, close } = await isorunTools({ image: 'python:3.12-slim', // any OCI image timeoutSec: 600, // idle auto-destroy }) ``` `apiKey` defaults to `ISORUN_API_KEY`. The underlying sandbox supports the full SDK surface (`fork`, `url`, `hibernate`, snapshots); reach for the [TypeScript SDK](/sdks/typescript/) directly when you need it. ## Next steps - [OpenAI Agents](/integrations/openai-agents/) for the same tools in the OpenAI Agents SDK. - [MCP server](/integrations/mcp-server/) to drive sandboxes from an MCP client. - [Any OCI image](/sandboxes/any-oci-image/) to use a custom base image. --- # CrewAI Source: /integrations/crewai --- title: CrewAI description: Run agent code inside Isorun sandboxes from CrewAI tasks. --- Preview. CrewAI is a Python framework, and the drop-in tools below will ship with the Isorun Python SDK (in development). ```bash title="Terminal" pip install isorun[crewai] # coming soon ``` The integration ships two CrewAI-native tools: - `IsorunCodeInterpreterTool` - runs Python in a microVM - `IsorunShellTool` (alias `IsorunSandboxTool`) - runs shell commands Both tools follow the standard CrewAI `BaseTool` shape and cache one sandbox per tool instance. ## Example crew ```python title="Python" from crewai import Agent, Task, Crew from isorun.integrations.crewai import IsorunCodeInterpreterTool, IsorunShellTool code = IsorunCodeInterpreterTool() shell = IsorunShellTool() researcher = Agent( role="Data scientist", goal="Analyze the user's data and produce findings.", backstory="You're an experienced analyst who runs everything in a sandbox.", tools=[code, shell], ) task = Task( description=( "Download the latest IMDB top-100 dataset, compute the average rating " "by decade, and report the decade with the highest average." ), expected_output="A short paragraph with the decade and the average rating.", agent=researcher, ) try: Crew(agents=[researcher], tasks=[task]).kickoff() finally: code.close() shell.close() ``` ## Tool lifecycle Each tool instance owns one sandbox. The sandbox is created lazily on first call and destroyed when you call `tool.close()` (or use the tool as a context manager). The tool resets the sandbox's idle timer on every call so it stays alive across multiple agent turns. If your crew's agents need to share state, files, installed packages, in-memory caches, give them the *same* tool instance. For isolation, give each agent its own tool instance. ## Next steps - [LangChain](/integrations/langchain/), the same sandbox tools for LangChain. - [OpenAI Agents](/integrations/openai-agents/), sandbox tools for the OpenAI Agents SDK. - [Lifecycle and hibernation](/sandboxes/lifecycle/), how the per-tool sandbox idle timer works. --- # MCP Server Source: /integrations/mcp-server --- title: MCP Server description: Drive Isorun sandboxes from any MCP-compatible client (Claude Desktop, Cursor, Zed). Speaks JSON-RPC over stdio. --- The `isorun` package ships an MCP server CLI, `isorun-mcp`, that any [Model Context Protocol](https://modelcontextprotocol.io) client can drive: Claude Desktop, Cursor, Zed, or anything else that speaks MCP. It exposes the canonical Isorun primitives as MCP tools over stdio. ## Install ```bash title="Terminal" npm install -g isorun @modelcontextprotocol/sdk zod ``` This puts `isorun-mcp` on your PATH. (Install locally instead and reference the binary by path if you prefer.) ## Tools | Tool | What it does | |---|---| | `create_sandbox` | Boot a fresh sandbox, return its ID | | `exec` | Run a command in a sandbox | | `url` | Public URL for an in-sandbox port | | `hibernate` | Pause a sandbox to disk, free runner resources | | `resume` | Bring a hibernated sandbox back | | `checkpoint` | Snapshot a running sandbox, return a snapshot ID | | `restore` | Create a new sandbox from a snapshot | | `list_sandboxes` | List live sandboxes for the API key | | `destroy_sandbox` | Free a sandbox and return usage stats | Tools take a `sandboxId`, so the model can `create_sandbox` once, run several `exec` calls against that ID, then `destroy_sandbox` when done. ## Wire into Claude Desktop Add the server to `claude_desktop_config.json`: ```json title="JSON" { "mcpServers": { "isorun": { "command": "isorun-mcp", "env": { "ISORUN_API_KEY": "isorun_live_..." } } } } ``` Restart Claude. The model now has `isorun:create_sandbox`, `isorun:exec`, `isorun:hibernate`, and the rest as native tools. ## Wire into Cursor Add to `~/.cursor/mcp.json` (or your project's `.cursor/mcp.json`): ```json title="JSON" { "mcpServers": { "isorun": { "command": "isorun-mcp", "env": { "ISORUN_API_KEY": "isorun_live_..." } } } } ``` ## Wire into Zed Add to `settings.json`: ```json title="JSON" { "context_servers": { "isorun": { "source": "custom", "command": { "path": "isorun-mcp", "env": { "ISORUN_API_KEY": "isorun_live_..." } } } } } ``` ## Embed the server in your own host `buildServer()` returns a configured MCP server without binding it to stdio, so you can compose Isorun's tools with your own inside one process. ```ts title="TypeScript" import { buildServer } from 'isorun/mcp' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' const server = await buildServer() // register your own tools on `server` here, then: await server.connect(new StdioServerTransport()) ``` ## Next steps - [TypeScript SDK](/sdks/typescript/), the client the server wraps. - [Create and execute](/sandboxes/create-and-execute/), what `create_sandbox` and `exec` do under the hood. - [Lifecycle and hibernation](/sandboxes/lifecycle/), the `hibernate` and `resume` tools. --- # API Reference Source: /api-reference --- title: API Reference description: REST API for sandbox lifecycle, execution, files, snapshots, fork, hibernate, audit, and network policy. --- Base URL: `https://run-.isorun.ai` (the region segment matches the one embedded in your API key). All `/v1/*` endpoints require `Authorization: Bearer `. The base URL's region segment is fixed by your API key, `isorun_live_us_...` resolves to `https://run-us.isorun.ai`. You don't choose it per request. ## Create a sandbox ```bash title="Terminal" curl -X POST https://run-us.isorun.ai/v1/runs \ -H "Authorization: Bearer $ISORUN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"image": "python:3.12-slim", "vcpus": 1, "mem_mib": 1024}' ``` ```json title="JSON" { "id": "run78cbccbab85efc28", "status": "running", "image": "python:3.12-slim", "region": "us", "vcpus": 1, "mem_mib": 1024, "disk_mib": 4096, "create_ms": 9, "created_at": "2026-05-26T12:00:00Z" } ``` Optional fields: `disk_mib`, `timeout` (auto-destroy seconds), `network: { allow, deny }`, `network_profile`, `credentials`. ## Execute a command ```bash title="Terminal" curl -X POST https://run-us.isorun.ai/v1/runs/run78cbccbab85efc28/exec \ -H "Authorization: Bearer $ISORUN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command": "python3 -c \"print(1+1)\"", "timeout": 30}' ``` ```json title="JSON" { "exit_code": 0, "stdout": "2\n", "stderr": "" } ``` ## Destroy a sandbox ```bash title="Terminal" curl -X DELETE https://run-us.isorun.ai/v1/runs/run78cbccbab85efc28 \ -H "Authorization: Bearer $ISORUN_API_KEY" ``` ```json title="JSON" { "status": "destroyed", "cpu_ms": 48, "mem_peak_bytes": 48926720, "uptime_ms": 59000, "cost_cents": 0.0007 } ``` ## All endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/healthz` | Health check (no auth) | | `POST` | `/v1/runs` | Create a sandbox | | `GET` | `/v1/runs` | List active sandboxes | | `GET` | `/v1/runs/{id}` | Get sandbox status | | `DELETE` | `/v1/runs/{id}` | Destroy a sandbox | | `PATCH` | `/v1/runs/{id}` | Reset the auto-destroy timer | | `POST` | `/v1/runs/{id}/exec` | Execute command (JSON response) | | `POST` | `/v1/runs/{id}/exec/stream` | Execute command (SSE stream) | | `GET` | `/v1/runs/{id}/terminal?token=KEY` | WebSocket PTY terminal | | `GET` | `/v1/runs/{id}/shell` | WebSocket long-lived bash session | | `POST` | `/v1/runs/{id}/snapshot` | Snapshot a running sandbox | | `POST` | `/v1/runs/restore` | Restore from snapshot | | `POST` | `/v1/runs/{id}/fork` | Fork the running sandbox | | `POST` | `/v1/runs/{id}/hibernate` | Pause + persist to disk | | `POST` | `/v1/runs/{id}/resume` | Restore a hibernated sandbox | | `POST` | `/v1/runs/{id}/files/upload?path=PATH` | Upload file | | `GET` | `/v1/runs/{id}/files/download?path=PATH` | Download file | | `GET` | `/v1/runs/{id}/files?path=PATH` | List directory | | `GET` | `/v1/runs/{id}/audit` | NDJSON audit trail | | `GET` | `/v1/runs/{id}/proxy/{port}/*` | Reverse-proxy a guest port | | `GET` | `/v1/snapshots` | List snapshots | | `DELETE` | `/v1/snapshots/{id}` | Delete a snapshot | | `GET` | `/v1/runs/history` | Past (destroyed) sandboxes | | `GET` | `/v1/usage` | Usage / cost rollup | | `GET` | `/v1/network-profiles` | List named egress profiles | | `GET` | `/v1/images` | List supported OCI images | | `POST` | `/v1/images/build` | Build an image from a registry | ## Next steps - [TypeScript SDK](/sdks/typescript/), the typed client over these endpoints. - [Authentication](/getting-started/authentication/), how API keys and regions work. - [Network filtering](/security/network-filtering/), the `network` and `network_profile` create fields. --- # Troubleshooting Source: /troubleshooting --- title: Troubleshooting description: Fixes for the errors and surprises you're most likely to hit. --- Common symptoms and what to do about them. Every SDK method throws on a non-2xx response, and the error message includes the HTTP status plus the first 200 bytes of the response body, so the status code below is usually visible in the thrown error. ## 401 Unauthorized Your API key is missing, malformed, or revoked. **What to do:** - Confirm `ISORUN_API_KEY` is set in the environment the SDK runs in (`echo $ISORUN_API_KEY`). - Check the key still exists in the [dashboard](https://app.isorun.ai). Revoked keys stop working immediately. - Use the full key, including its region segment. The SDK derives the endpoint from it, so a truncated or edited key won't route. ## 429 Too Many Requests You've hit a rate or capacity limit. **What to do:** - Back off and retry, transient capacity limits clear quickly. - Destroy sandboxes you're no longer using; concurrent sandboxes count against your limits. - If you're launching a burst, stagger creates rather than firing them all at once. ## My command times out `exec` has a default timeout of 30 seconds. **What to do:** - Pass a longer timeout: `sandbox.exec('pytest -q', 180)`. - For long-running builds, tests, or training, use `execStream` so you get incremental output instead of waiting for a single buffered result. ## The sandbox disappeared on its own Sandboxes auto-destroy when their `timeoutSec` idle timer fires. **What to do:** - Set a longer `timeoutSec` at create time. - Call `sandbox.setTimeout(seconds)` to extend the timer when you hand the sandbox to a long-running workflow. ## My public URL doesn't respond A public URL is anonymous: the unguessable run ID in the host is the credential, so a missing header isn't the cause. **What to do:** - Confirm the sandbox is still alive (not destroyed or past its `timeoutSec`) and your service is listening on that port. Localhost-only servers are exposed automatically; the first hit to a new port can be slightly slower. - Treat the URL as a secret. If it leaked, destroy the sandbox. ## The first boot of an image is slow The first time a runner sees an image, it pulls and prepares it. Every boot after that is instant. **What to do:** - Pin a real tag or digest, `latest` and `lts` are rejected at create time. - Warm the image once at startup if your first request is latency-sensitive. ## Outbound connections drop after resume Hibernate preserves filesystem, memory, and processes, but established outbound TCP connections don't survive. **What to do:** - Re-open connections (DB clients, websockets, streams) after `resume()`. ## My code can't reach the internet Egress is filtered at the host. If a sandbox uses a locked-down network profile, outbound traffic to unlisted hosts is dropped. **What to do:** - Pick or author a [network profile](/security/network-profiles/) that allows the hosts you need. - Use [credential injection](/security/credential-injection/) for third-party APIs so the key never enters the guest. ## Next steps - [Network profiles](/security/network-profiles/), control egress per sandbox. - [Lifecycle](/sandboxes/lifecycle/), how hibernate, resume, and timeouts work. - [TypeScript SDK reference](/sdks/typescript/), method signatures and errors. --- # Changelog Source: /changelog --- title: Changelog description: What shipped and when. --- ## v1.0.0, June 16, 2026 Public launch. - **MicroVM sandboxes**, fast cold-create on our optimized boot pipeline. - **Any OCI image**, run any registry image (Docker Hub, GHCR, private). First use pulls and prepares it; every boot after is instant. [How it works →](/sandboxes/any-oci-image) - **Snapshot & restore**, checkpoint a running VM and resume from the snapshot. - **Fork**, clone a running sandbox into N children that inherit filesystem + memory state. - **Hibernate & resume**, pause without paying for active runtime; resume preserves processes. - **Port forwarding**, reach any in-sandbox port with `sandbox.url(port)`. - **Network egress filtering**, allow/deny lists per sandbox + named profiles (`unrestricted`, `locked-down`, `claude-code`, `openai`, …). - **Credential injection**, host-side reverse proxy keeps API keys out of the guest's environment. - **File upload/download/list**, via the runtime protocol. - **WebSocket terminal**, full PTY with tab completion, vi, Ctrl-C. - **WebSocket shell**, long-lived bash session, state persists across commands. - **SSE streaming exec**, stream stdout/stderr in real time. - **Audit trail**, tamper-evident NDJSON log of every sandbox event. - **TypeScript SDK**, `npm install isorun`, ESM-only, Node 20+. - **Claude Managed Agents adapter**, `isorun/claude-agents/orchestrator` sub-export. - **REST API**, OpenAPI 3.1 spec, Bearer auth. - **Per-second billing**, $0.025/vCPU-hr + $0.015/GiB-hr. $50 free credit on signup.