> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vpod.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Sandbox

> Create sandboxes, run commands, and execute Python code from TypeScript.

The TypeScript SDK runs the same sandbox in Node and in a browser tab. The API
below is identical in both; what differs is where snapshots are cached and what
the guest's network can reach. See [Browser](/sdk/typescript/browser) for that.

## Install

```bash theme={null}
npm install @capsule-run/vpod
```

```ts theme={null}
import { Sandbox } from "@capsule-run/vpod";
```

In a browser, import from `@capsule-run/vpod/browser` instead. The package root
resolves by export condition, which some bundlers get wrong for client code.

## Overview

| Method                                   | Description                                          |
| :--------------------------------------- | :--------------------------------------------------- |
| `Sandbox.create(options?)`               | Create a new sandbox                                 |
| `sandbox.commands.run(cmd, options?)`    | Run a shell command                                  |
| `sandbox.commands.interrupt()`           | Stop the command that is running                     |
| `sandbox.code.run(source, options?)`     | Run Python code                                      |
| `sandbox.close()`                        | Shut down the running sandbox                        |
| `sandbox.suspend()`                      | Suspend and return the delta bytes                   |
| `sandbox.suspendToOpfs()`                | Suspend into browser storage, returns an instance id |
| `Sandbox.resume(idOrInstance, options?)` | Resume a suspended instance                          |
| `Sandbox.listInstances()`                | List instances held in browser storage               |
| `Sandbox.destroy(id)`                    | Delete a stored instance                             |

Everything is async, because loading the engine and fetching a snapshot are.

## Persistent sessions

All calls share one running sandbox, so state carries across them. Declaring it
with `await using` closes it when the scope ends:

```ts theme={null}
import { Sandbox } from "@capsule-run/vpod";

await using sandbox = await Sandbox.create();

await sandbox.commands.run("export API_KEY=secret");
const result = await sandbox.commands.run("echo $API_KEY");
console.log(result.stdout); // secret
```

If your toolchain does not support `await using`, call `await sandbox.close()`
yourself.

Shell commands and Python code share one filesystem:

```ts theme={null}
await sandbox.commands.run("echo 'from shell' > /tmp/shared.txt");
await sandbox.code.run("print(open('/tmp/shared.txt').read().strip())");
```

<Warning>
  Environment variables do not cross between the two. `commands.run("export FOO=bar")`
  is invisible to `code.run(...)`. Use the filesystem to pass data between them.
</Warning>

## Return values

### `sandbox.commands.run(cmd)`

| Field      | Type      | Description                 |
| :--------- | :-------- | :-------------------------- |
| `stdout`   | `string`  | Standard output             |
| `stderr`   | `string`  | Standard error              |
| `exitCode` | `number`  | Exit code                   |
| `success`  | `boolean` | True when `exitCode` is `0` |

```ts theme={null}
const result = await sandbox.commands.run("whoami");
console.log(result.stdout);   // root
console.log(result.exitCode); // 0
```

### `sandbox.code.run(source)`

| Field     | Type             | Description                               |
| :-------- | :--------------- | :---------------------------------------- |
| `text`    | `string`         | Output of the executed code               |
| `error`   | `string \| null` | Error message when execution failed       |
| `logs`    | `string[]`       | Log lines produced during execution       |
| `stderr`  | `string`         | Anything the code wrote to standard error |
| `success` | `boolean`        | True when `error` is `null`               |

```ts theme={null}
await sandbox.code.run("import json");
await sandbox.code.run("data = [1, 2, 3]");

const result = await sandbox.code.run("print(sum(data))");
console.log(result.text);  // 6
console.log(result.error); // null
```

Variables and imports live for the lifetime of the session, so a REPL built on
this behaves the way people expect.

## Timeouts

Both methods take a `timeout` in seconds. When it is reached the guest work is
interrupted and the call returns rather than waiting.

```ts theme={null}
await sandbox.code.run("import time; time.sleep(30)", { timeout: 3 });

const result = await sandbox.commands.run("sleep 30", { timeout: 3 });
console.log(result.exitCode); // 124
```

## Interrupting a command

A command no longer has to run to its timeout. `interrupt()` stops whatever is in
the foreground, and `commands.run()` takes an `AbortSignal` for callers who would
rather set the deadline up front.

```ts theme={null}
const running = sandbox.commands.run("sleep 300");
await sandbox.commands.interrupt();

const result = await running;
console.log(result.exitCode); // 130
```

```ts theme={null}
const controller = new AbortController();
const promise = sandbox.commands.run("sleep 300", { signal: controller.signal });
controller.abort();

// or set a deadline without wiring up a controller
await sandbox.commands.run("sleep 300", { signal: AbortSignal.timeout(5000) });
```

The two report differently, on purpose. `interrupt()` resolves with an ordinary
result carrying exit code `130`, because a terminal wants the exit code. A
`signal` rejects with `signal.reason`, so your own controller throws `AbortError`
and `AbortSignal.timeout()` throws `TimeoutError`.

The command really stops. This is not the caller walking away while the guest
keeps working, so the sandbox is yours again immediately.

<Note>
  `code.run()` cannot be interrupted yet. It accepts `signal` because it shares an
  options type with `commands.run()`, but ignores it, so use `timeout` there.

  A command that ignores the interrupt runs to its own deadline and is recovered
  the same way a timeout is.
</Note>

## `Sandbox.create()` options

| Option             | Type                                    | Description                                                                                       |
| :----------------- | :-------------------------------------- | :------------------------------------------------------------------------------------------------ |
| `snapshot`         | `string \| { path } \| { bytes, name }` | What to boot from. Defaults to `vsnap-base:latest`                                                |
| `network`          | `boolean`                               | Force the guest's network on or off                                                               |
| `corsProxy`        | `string`                                | A relay for hosts that send no CORS headers. Browser only, see [Browser](/sdk/typescript/browser) |
| `registryUrl`      | `string`                                | Where to resolve snapshot names                                                                   |
| `workerUrl`        | `string \| URL`                         | Where the emulator worker is served from                                                          |
| `componentUrl`     | `string \| URL`                         | Where the wasm component is served from                                                           |
| `networkWorkerUrl` | `string \| URL`                         | Where the network worker is served from                                                           |

### `snapshot`

A registry name, or a snapshot you built yourself:

```ts theme={null}
await Sandbox.create({ snapshot: "vsnap-data" });
await Sandbox.create({ snapshot: { path: "./alpine-3.23.0-256mb.snap" } });
await Sandbox.create({ snapshot: { bytes, name: "alpine-3.23.0-256mb.snap" } });
```

Keep the RAM size in the file name, because the emulator reads it from there.

<Info>
  The first `Sandbox.create()` downloads the snapshot and caches it, on disk in Node
  and in origin-private storage in a browser. Later runs use the cache. See
  [Snapshots](/sdk/typescript/snapshots).
</Info>

### `network`

The guest's network is on by default wherever it can be. Pass `false` to run
fully offline, or `true` to fail loudly instead of silently starting without it:

```ts theme={null}
const offline = await Sandbox.create({ network: false });
```

`sandbox.network` reports what the guest can actually reach:

```ts theme={null}
const { backend, rawTcp, corsRestricted } = sandbox.network;
```

The three asset URL options only matter in a browser, where the worker and the
wasm are fetched at runtime rather than imported.

## Differences from the Python SDK

<Note>
  Three things differ, and all three surprise people who move between the two:

  * The default snapshot is `vsnap-base:latest` here and `alpine` in Python.
  * `sandbox.suspend()` returns the delta **bytes**, not an instance id. Browsers
    have nowhere to put a file, so where the state goes is your decision. See
    [Suspend & resume](/sdk/typescript/suspend-resume).
  * There is no `mounts` option. Mounting a host directory has no meaning in a
    browser tab, so the TypeScript SDK does not offer it anywhere.
</Note>
