# 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.