# OpenComputer Docs (v1)

> Archived documentation for the v1 sandbox runtime

This file contains the full content of all documentation pages. For a compact index, see [llms.txt](https://docs-v1.opencomputer.dev/llms.txt). To download all pages as a zip, use [docs.zip](https://docs-v1.opencomputer.dev/docs.zip).

---
title: Introduction
url: "https://docs-v1.opencomputer.dev/introduction.md"
description: Cloud sandboxes for AI apps
---

OpenComputer gives you a full Linux VM in the cloud. Each sandbox is an isolated virtual machine with its own filesystem, network, and process space — persistent, long-running, and capable of hibernating when idle. Write files, install packages, run commands, and build complete projects inside secure, isolated environments.

<CardGroup cols={2}>
  <Card title="Full Linux VM" icon="server">
    Not a container. Real VM with its own kernel, memory, and disk. Hardware-level isolation via KVM.
  </Card>

  <Card title="Long-running" icon="clock">
    Hours or days, not minutes. Install packages, build projects, run test suites, iterate — no cold starts between steps.
  </Card>

  <Card title="Checkpoint & fork" icon="code-branch">
    Named snapshots you can fork from. Try five approaches in parallel from the same starting point — like git branches for VMs.
  </Card>

  <Card title="Elastic compute" icon="gauge-high">
    Scale memory and CPU at runtime. Request more resources for heavy tasks and release them after.
  </Card>
</CardGroup>

## Install

<Tabs items={["npm", "pip", "CLI"]}>
  <Tab title="npm">
    ```bash
    npm install @opencomputer/sdk
    ```
  </Tab>

  <Tab title="pip">
    ```bash
    pip install opencomputer-sdk
    ```
  </Tab>

  <Tab title="CLI">
    ```bash
    # Installs to ~/.local/bin (no sudo). See cli/overview for manual install.
    curl -fsSL https://raw.githubusercontent.com/diggerhq/opencomputer/main/scripts/install.sh | bash
    ```
  </Tab>
</Tabs>

```bash
export OPENCOMPUTER_API_KEY=your-api-key
```

<Tip>Grab your API key from [app.opencomputer.dev](https://app.opencomputer.dev)</Tip>

## Quick Example

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    import { Sandbox } from "@opencomputer/sdk";

    const sandbox = await Sandbox.create();
    const result = await sandbox.commands.run("echo 'Hello World from OpenSandbox!'");
    console.log(result.stdout);
    await sandbox.kill();
    ```
  </Tab>

  <Tab title="Python">
    ```python
    import asyncio
    from opencomputer import Sandbox

    async def main():
        sandbox = await Sandbox.create()
        result = await sandbox.commands.run("echo 'Hello World from OpenSandbox!'")
        print(result.stdout)
        await sandbox.kill()

    asyncio.run(main())
    ```
  </Tab>
</Tabs>

## Next Steps

<CardGroup cols={2}>
  <Card title="Quickstart" icon="rocket" href="/quickstart">
    Create your first sandbox in 2 minutes
  </Card>

  <Card title="How It Works" icon="microchip" href="/how-it-works">
    Architecture: VMs, hibernation, checkpoints
  </Card>

  <Card title="Sandboxes" icon="box" href="/sandboxes/overview">
    The compute primitive
  </Card>

  <Card title="Working with Files" icon="file" href="/sandboxes/working-with-files">
    Upload, download, and manage files
  </Card>
</CardGroup>


---
title: Quickstart
url: "https://docs-v1.opencomputer.dev/quickstart.md"
description: Create your first sandbox in 2 minutes
---

## Prerequisites

An OpenComputer API key and the SDK installed. See [Introduction](/introduction) for install instructions.

```bash
export OPENCOMPUTER_API_KEY=your-api-key
```

<Tip>Grab your API key from [app.opencomputer.dev](https://app.opencomputer.dev)</Tip>

## Create Your First Sandbox

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    import { Sandbox } from "@opencomputer/sdk";

    const sandbox = await Sandbox.create();

    // Run a command inside the sandbox
    const result = await sandbox.commands.run("echo 'Hello World from OpenSandbox!'");
    console.log(result.stdout);

    await sandbox.kill();
    ```
  </Tab>

  <Tab title="Python">
    ```python
    import asyncio
    from opencomputer import Sandbox

    async def main():
        sandbox = await Sandbox.create()

        # Run a command inside the sandbox
        result = await sandbox.commands.run("echo 'Hello World from OpenSandbox!'")
        print(result.stdout)

        await sandbox.kill()

    asyncio.run(main())
    ```
  </Tab>
</Tabs>

Running this will print:

```
Hello World from OpenSandbox!
```

<Note>
  Your sandboxes stay alive as long as you need them. They don't shut down after a command finishes — you can keep running commands, installing packages, and building projects for hours or days. Sandboxes only stop when you explicitly kill them or when the idle timeout expires (default 5 minutes, configurable).
</Note>

## Next Steps

<CardGroup cols={2}>
  <Card title="Elastic Compute" icon="gauge-high" href="/sandboxes/elasticity">
    Scale memory and CPU at runtime based on workload
  </Card>

  <Card title="Upload & Download Files" icon="file-arrow-up" href="/sandboxes/working-with-files">
    Read, write, and transfer files to and from sandboxes
  </Card>

  <Card title="Custom Sandboxes" icon="palette" href="/sandboxes/templates">
    Build custom environments with your own packages and tools
  </Card>

  <Card title="Checkpoints" icon="code-branch" href="/sandboxes/checkpoints">
    Snapshot and fork VMs for parallel experimentation
  </Card>
</CardGroup>


---
title: How It Works
url: "https://docs-v1.opencomputer.dev/how-it-works.md"
description: Architecture and key technical decisions
---

Your code talks to a **control plane** API, which provisions a virtual machine on a worker node. Commands and file operations run inside that VM — I/O streams back over WebSocket. The control plane transparently proxies data-plane requests to the correct worker.

SDK users only need an API key. The SDK handles routing automatically.

<Steps>
  <Step title="Create">
    Your code calls the control plane (`X-API-Key` auth). A VM boots from a golden snapshot in \~300ms.
  </Step>

  <Step title="Operate">
    Run commands, read/write files — all execution happens inside the VM. The control plane routes requests to the correct worker automatically.
  </Step>
</Steps>

## Virtual Machines

Each sandbox is a real virtual machine with its own Linux kernel, memory, and disk. Isolation is hardware-level (KVM), not namespace-based like containers.

Why not containers? Containers share a kernel. A kernel exploit in one container compromises every other container on the host. KVM-based VMs eliminate that attack surface entirely.

## Hibernation

`hibernate()` snapshots the VM's memory and disk state, then stops it. No compute costs while hibernated. `wake()` restores the sandbox — the platform attempts a fast snapshot-based resume and falls back to a cold boot from the saved disk state if needed.

The sandbox keeps the same ID across hibernate/wake cycles. A rolling idle timeout (default 300s) auto-hibernates sandboxes that go unused.

## Checkpoints & Forking

A checkpoint is a named snapshot of a running sandbox. You can:

* **Restore** — revert the sandbox in-place (all changes since the checkpoint are lost)
* **Fork** — create a new sandbox from the checkpoint

Forking preserves the filesystem and installed packages. The new sandbox starts with a fresh boot from that disk state — don't assume running processes carry over.

```bash
# Checkpoint, then fork two independent sandboxes
oc checkpoint create sb-abc --name ready-state
oc checkpoint spawn cp-xyz   # → sandbox A
oc checkpoint spawn cp-xyz   # → sandbox B
```

Maximum 10 full checkpoints or 100 disk-only checkpoints per sandbox.

## Templates

Templates define the sandbox environment. The default template includes Ubuntu, Python 3, Node.js, and common CLI tools.

For custom environments, use the **Image builder** to define dependencies programmatically, or create **pre-built snapshots** for instant reuse:

```typescript
// On-demand image (built + cached by content hash)
const image = Image.base().aptInstall(['curl']).pipInstall(['pandas']);
const sandbox = await Sandbox.create({ image });

// Pre-built snapshot (instant boot)
const sandbox = await Sandbox.create({ snapshot: 'my-custom-stack' });
```

See [Templates](/sandboxes/templates) for the full guide.

## Elasticity

Sandboxes can scale memory at runtime via an internal metadata endpoint (`http://169.254.169.254/v1/scale`) accessible from inside the VM. CPU scales proportionally. This lets workloads request more resources before heavy tasks and release them after. See [Elasticity](/sandboxes/elasticity) for details.

## Networking

Every sandbox gets full outbound internet access via a TAP device with NAT. No firewall or allowlist to configure.

For inbound access, create a **preview URL** that reverse-proxies public traffic to a port inside the sandbox:

```bash
oc preview create sb-abc --port 3000
```

See [Preview URLs](/sandboxes/preview-urls) for custom domains and auth.

<Tip>
  API details: [HTTP API Reference](/reference/api#base-url--authentication).
</Tip>


---
title: Sandboxes
url: "https://docs-v1.opencomputer.dev/sandboxes/overview.md"
description: The compute primitive — full Linux VMs in the cloud
---

A sandbox is a full Linux virtual machine in the cloud. Each one has its own filesystem, network stack, and process space — completely isolated from other sandboxes via hardware-level virtualization. Think of it as a disposable laptop that starts in milliseconds and sleeps when idle.

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    import { Sandbox } from "@opencomputer/sdk";

    const sandbox = await Sandbox.create();
    const result = await sandbox.exec.run("echo Hello from $(uname -a)");
    console.log(result.stdout);
    await sandbox.kill();
    ```
  </Tab>

  <Tab title="Python">
    ```python
    from opencomputer import Sandbox

    async with await Sandbox.create() as sandbox:
        result = await sandbox.exec.run("echo Hello from $(uname -a)")
        print(result.stdout)
    # sandbox is killed automatically on exit
    ```
  </Tab>
</Tabs>

## Creating a Sandbox

<Tabs items={["TypeScript", "Python", "curl"]}>
  <Tab title="TypeScript">
    ```typescript
    const sandbox = await Sandbox.create({
      template: "my-custom-template",
      timeout: 600,
      cpuCount: 2,
      memoryMB: 1024,
      diskMB: 20480, // 20GB workspace (default; larger sizes in closed beta)
      envs: { NODE_ENV: "production" },
      metadata: { project: "my-app" },
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    sandbox = await Sandbox.create(
        template="my-custom-template",
        timeout=600,
        disk_mb=20480,  # 20GB workspace (default; larger sizes in closed beta)
        envs={"NODE_ENV": "production"},
        metadata={"project": "my-app"},
    )
    ```
  </Tab>

  <Tab title="curl">
    ```bash
    curl -X POST https://app.opencomputer.dev/api/sandboxes \
      -H "X-API-Key: $OPENCOMPUTER_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "templateID": "my-custom-template",
        "timeout": 600,
        "cpuCount": 2,
        "memoryMB": 1024,
        "diskMB": 20480,
        "envs": {"NODE_ENV": "production"},
        "metadata": {"project": "my-app"}
      }'
    ```
  </Tab>
</Tabs>

### Parameters

| Parameter     | Type   | Default  | Description                                                          |
| ------------- | ------ | -------- | -------------------------------------------------------------------- |
| `template`    | string | `"base"` | Template for the VM root filesystem                                  |
| `timeout`     | number | `300`    | Idle timeout in seconds (resets on every operation)                  |
| `envs`        | object | —        | Environment variables set inside the VM                              |
| `metadata`    | object | —        | Arbitrary key-value pairs stored with the sandbox                    |
| `cpuCount`    | number | —        | CPU cores. TypeScript and HTTP API only                              |
| `memoryMB`    | number | —        | Memory in MB. TypeScript and HTTP API only                           |
| `diskMB`      | number | `20480`  | Workspace disk size in MB (20GB–256GB). See disk sizing note below.  |
| `secretStore` | string | —        | Secret store name — resolves encrypted secrets and egress allowlist  |
| `image`       | Image  | —        | Declarative image definition (see [Templates](/sandboxes/templates)) |
| `snapshot`    | string | —        | Name of a pre-built snapshot for instant boot                        |
| `apiKey`      | string | —        | Defaults to `OPENCOMPUTER_API_KEY` env var                           |
| `apiUrl`      | string | —        | Defaults to `OPENCOMPUTER_API_URL` env var                           |

<Note>
  `cpuCount`, `memoryMB`, and `secretStore` are not available in the Python SDK. Use the [HTTP API](/reference/api#sandbox-lifecycle) directly for custom resources from Python.
</Note>

<Info>
  **Disk sizing (closed beta).** Every sandbox ships with a 20GB workspace on
  `/home/sandbox`. You can request up to 256GB via `diskMB`; any GB above the
  20GB baseline is metered per-second at a rate comparable to AWS EBS gp3.
  Larger disk limits are gated per-organization during the beta — [book a
  call](https://cal.com/team/digger/opencomputer-founder-chat) to have your
  org's ceiling raised.
</Info>

## Connecting to an Existing Sandbox

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    const sandbox = await Sandbox.connect("sb-abc123");
    ```
  </Tab>

  <Tab title="Python">
    ```python
    sandbox = await Sandbox.connect("sb-abc123")
    ```
  </Tab>
</Tabs>

## Lifecycle

Sandboxes have four states:

| Status       | Description                  |
| ------------ | ---------------------------- |
| `running`    | Active, accepting operations |
| `hibernated` | State saved, no compute cost |
| `stopped`    | Terminated                   |
| `error`      | Failed                       |

A rolling idle timeout (default 300s) resets on every operation — exec, file access, agent activity. When the timeout expires, the sandbox auto-hibernates if possible, otherwise stops.

## Hibernation & Wake

Hibernation snapshots the VM state and stops it — no cost while hibernated. Wake restores the sandbox, typically fast, with a cold-boot fallback if snapshot restore is unavailable.

<Tabs items={["TypeScript", "HTTP"]}>
  <Tab title="TypeScript">
    ```typescript
    await sandbox.hibernate();
    // ... later ...
    await sandbox.wake({ timeout: 600 });
    ```
  </Tab>

  <Tab title="HTTP">
    ```http
    POST /api/sandboxes/sb-abc123/hibernate

    POST /api/sandboxes/sb-abc123/wake
    Content-Type: application/json

    {"timeout": 600}
    ```
  </Tab>
</Tabs>

<Note>
  `hibernate()` and `wake()` are available in the TypeScript SDK and [CLI](/cli/sandbox). Python SDK support is planned — use the [HTTP API](/reference/api#sandbox-lifecycle) directly.
</Note>

## Methods

| Method       | TypeScript                      | Python                           | Description                   |
| ------------ | ------------------------------- | -------------------------------- | ----------------------------- |
| Kill         | `await sandbox.kill()`          | `await sandbox.kill()`           | Terminate the sandbox         |
| Check status | `await sandbox.isRunning()`     | `await sandbox.is_running()`     | Returns boolean               |
| Set timeout  | `await sandbox.setTimeout(600)` | `await sandbox.set_timeout(600)` | Update idle timeout (seconds) |
| Hibernate    | `await sandbox.hibernate()`     | —                                | Snapshot and stop             |
| Wake         | `await sandbox.wake()`          | —                                | Resume from hibernation       |

## Properties

| Property   | TypeScript          | Python               | Description                                               |
| ---------- | ------------------- | -------------------- | --------------------------------------------------------- |
| Sandbox ID | `sandbox.sandboxId` | `sandbox.sandbox_id` | Unique identifier                                         |
| Status     | `sandbox.status`    | `sandbox.status`     | Current lifecycle state                                   |
| Domain     | `sandbox.domain`    | —                    | Preview URL domain for port 80                            |
| Agent      | `sandbox.agent`     | `sandbox.agent`      | [Agent sessions](/agent-sessions/overview)                |
| Exec       | `sandbox.exec`      | `sandbox.exec`       | [Command execution](/sandboxes/running-commands)          |
| Files      | `sandbox.files`     | `sandbox.files`      | [Filesystem](/sandboxes/working-with-files)               |
| PTY        | `sandbox.pty`       | `sandbox.pty`        | [Interactive terminals](/sandboxes/interactive-terminals) |

<Warning>
  `sandbox.commands` is a deprecated alias for `sandbox.exec`. Use `sandbox.exec` in all new code.
</Warning>

### Python Context Manager

The Python SDK supports `async with` for automatic cleanup:

```python
async with await Sandbox.create() as sandbox:
    result = await sandbox.exec.run("echo hello")
    print(result.stdout)
# sandbox.kill() called automatically on exit
```

## Forking from Checkpoints

Create a sandbox from a saved checkpoint — useful for parallel testing from a known-good state:

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    const forked = await Sandbox.createFromCheckpoint("cp-abc123", { timeout: 600 });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    forked = await Sandbox.create_from_checkpoint("cp-abc123", timeout=600)
    ```
  </Tab>
</Tabs>

See [Checkpoints](/sandboxes/checkpoints) for the full workflow.

## Related

<CardGroup cols={2}>
  <Card title="Running Commands" href="/sandboxes/running-commands">
    Execute shell commands
  </Card>

  <Card title="Working with Files" href="/sandboxes/working-with-files">
    Read, write, and manage files
  </Card>

  <Card title="Interactive Terminals" href="/sandboxes/interactive-terminals">
    PTY sessions for interactive work
  </Card>

  <Card title="Checkpoints" href="/sandboxes/checkpoints">
    Snapshot, fork, and restore
  </Card>

  <Card title="Templates" href="/sandboxes/templates">
    Custom VM images
  </Card>

  <Card title="Preview URLs" href="/sandboxes/preview-urls">
    Expose ports to the internet
  </Card>
</CardGroup>

<Tip>
  CLI: [`oc sandbox`](/cli/sandbox) for lifecycle management. Full reference: [TypeScript SDK](/reference/typescript-sdk#sandbox) · [Python SDK](/reference/python-sdk#sandbox) · [HTTP API](/reference/api#sandbox-lifecycle).
</Tip>


---
title: Running Commands
url: "https://docs-v1.opencomputer.dev/sandboxes/running-commands.md"
description: Execute shell commands inside a sandbox
---

Three primitives, each tuned for a different shape of work:

| Use                                     | Primitive                            | Why                                                                                                                                                                                           |
| --------------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| One-shot command, wait for the result   | `exec.run()`                         | Simplest call. Returns once the command exits. Each call is isolated — no state carries between calls.                                                                                        |
| Long-running / streaming process        | `exec.start()` / `exec.background()` | Returns a session handle you can stream from, send stdin to, or reattach to later. Use for servers, builds, and any job you need to observe live.                                             |
| Multi-step workflow, state must persist | `exec.shell()`                       | Opens a long-lived `bash` whose `cwd`, exported env, and shell functions persist across `.run()` calls — the ergonomics of a terminal tab. Reconnectable via `exec.reattachShell(sessionId)`. |

Rules of thumb:

* Need a return value and don't care about streaming? → **`run()`**.
* Starting something that outlives the call (a server, a log tailer, a slow build)? → **`start()` / `background()`**.
* Doing a sequence of commands where one step's state (directory, env var, activated venv) affects the next? → **`shell()`**.

Under the hood `shell()` *is* an exec session — its `sessionId` shows up in `exec.list()` next to anything started via `start()`. The difference is purely how the SDK frames commands on top: a sentinel protocol per `.run()` call so you can treat one long-lived bash as many discrete calls.

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    import { Sandbox } from "@opencomputer/sdk";

    const sandbox = await Sandbox.create();
    const result = await sandbox.exec.run("echo Hello, World!");
    console.log(result.stdout); // "Hello, World!\n"
    await sandbox.kill();
    ```
  </Tab>

  <Tab title="Python">
    ```python
    from opencomputer import Sandbox

    async with await Sandbox.create() as sandbox:
        result = await sandbox.exec.run("echo Hello, World!")
        print(result.stdout)  # "Hello, World!\n"
    ```
  </Tab>
</Tabs>

## Quick Commands: `exec.run()`

Run a shell command and wait for the result. The command runs via `sh -c`, so pipes, redirects, and shell expansion work.

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    // Working directory and environment variables
    const result = await sandbox.exec.run("npm run build", {
      cwd: "/app",
      env: { NODE_ENV: "production" },
      timeout: 120,
    });

    if (result.exitCode !== 0) {
      console.error("Build failed:", result.stderr);
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python
    result = await sandbox.exec.run(
        "npm run build",
        cwd="/app",
        env={"NODE_ENV": "production"},
        timeout=120,
    )

    if result.exit_code != 0:
        print("Build failed:", result.stderr)
    ```
  </Tab>
</Tabs>

### Parameters

| Parameter | Type   | Default | Description                         |
| --------- | ------ | ------- | ----------------------------------- |
| `command` | string | —       | Shell command to run (**required**) |
| `timeout` | number | `60`    | Timeout in seconds                  |
| `env`     | object | —       | Environment variables               |
| `cwd`     | string | —       | Working directory                   |

### ProcessResult

| Field           | TypeScript | Python      | Type   |
| --------------- | ---------- | ----------- | ------ |
| Exit code       | `exitCode` | `exit_code` | number |
| Standard output | `stdout`   | `stdout`    | string |
| Standard error  | `stderr`   | `stderr`    | string |

## Async Commands: `exec.start()` / `exec.background()`

Start a command as an exec session for long-running processes or streaming output. Returns an `ExecSession` with callbacks for stdout, stderr, and exit. `exec.background()` is an alias for `exec.start()` — same options, same return type.

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    const session = await sandbox.exec.start("node server.js", {
      cwd: "/app",
      env: { PORT: "3000" },
      onStdout: (data) => process.stdout.write(data),
      onStderr: (data) => process.stderr.write(data),
      onExit: (code) => console.log("Server exited:", code),
      maxRunAfterDisconnect: 300, // keep running 5min after disconnect
    });

    // Send input
    session.sendStdin("some input\n");

    // Wait for completion (or kill)
    await session.kill();
    ```
  </Tab>

  <Tab title="Python">
    ```python
    session = await sandbox.exec.start(
        "node", args=["server.js"],
        cwd="/app",
        env={"PORT": "3000"},
        on_stdout=lambda b: print(b.decode(), end=""),
        on_stderr=lambda b: print(b.decode(), end=""),
        on_exit=lambda code: print(f"Server exited: {code}"),
        max_run_after_disconnect=300,
    )

    # Send input
    await session.send_stdin("some input\n")

    # Wait for completion (or kill)
    await session.kill()
    ```
  </Tab>
</Tabs>

### ExecStartOpts

| Parameter   | TypeScript              | Python                     | Description                              |
| ----------- | ----------------------- | -------------------------- | ---------------------------------------- |
| Arguments   | `args: string[]`        | `args: list[str]`          | Command arguments                        |
| Env         | `env: object`           | `env: dict`                | Environment variables                    |
| Working dir | `cwd: string`           | `cwd: str`                 | Working directory                        |
| Timeout     | `timeout: number`       | `timeout: int`             | Timeout in seconds                       |
| Keep-alive  | `maxRunAfterDisconnect` | `max_run_after_disconnect` | Seconds to keep running after disconnect |
| Stdout      | `onStdout`              | `on_stdout`                | Callback receiving raw bytes             |
| Stderr      | `onStderr`              | `on_stderr`                | Callback receiving raw bytes             |
| Exit        | `onExit`                | `on_exit`                  | Callback receiving the exit code         |

### ExecSession

| Member     | TypeScript              | Python                      | Description                                 |
| ---------- | ----------------------- | --------------------------- | ------------------------------------------- |
| Session ID | `sessionId`             | `session_id`                | Session identifier                          |
| Done       | `done: Promise<number>` | `done: asyncio.Future[int]` | Resolves with exit code                     |
| Send stdin | `sendStdin(data)`       | `send_stdin(data)`          | Write to the process                        |
| Kill       | `kill(signal?)`         | `kill(signal=9)`            | Kill the process                            |
| Detach     | `close()`               | `close()`                   | Close the WebSocket (process keeps running) |

## Stateful Shell: `exec.shell()`

Open a long-lived `bash` session whose state (`cwd`, exported env vars, shell functions) persists across `.run()` calls. Foreground-only: concurrent `.run()` rejects with `ShellBusyError`.

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    const sh = await sandbox.exec.shell({ cwd: "/app" });

    await sh.run("npm install");
    await sh.run("export NODE_ENV=test");
    const r = await sh.run("npm test", {
      onStdout: (b) => process.stdout.write(b),
    });
    console.log(r.exitCode);

    await sh.close();
    ```
  </Tab>

  <Tab title="Python">
    ```python
    sh = await sandbox.exec.shell(cwd="/app")

    await sh.run("npm install")
    await sh.run("export NODE_ENV=test")
    r = await sh.run("npm test", on_stdout=lambda b: print(b.decode(), end=""))
    print(r.exit_code)

    await sh.close()
    ```
  </Tab>
</Tabs>

### Reattaching to an open shell

The shell is just an exec session, so its `sessionId` is stable across SDK invocations. Keep the id and revisit the same shell later — cwd, env, and shell functions are still there because the bash process never went anywhere.

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    const sh = await sandbox.exec.shell();
    await sh.run("cd /srv && export DEPLOY=canary");
    const id = sh.sessionId; // persist this somewhere

    // ...later, possibly a different process...

    const sh2 = await sandbox.exec.reattachShell(id);
    const r = await sh2.run("pwd; echo $DEPLOY");
    // → "/srv\ncanary\n"
    ```
  </Tab>

  <Tab title="Python">
    ```python
    sh = await sandbox.exec.shell()
    await sh.run("cd /srv && export DEPLOY=canary")
    sid = sh.session_id  # persist this somewhere

    # ...later, possibly a different process...

    sh2 = await sandbox.exec.reattach_shell(sid)
    r = await sh2.run("pwd; echo $DEPLOY")
    # → "/srv\ncanary\n"
    ```
  </Tab>
</Tabs>

<Note>
  Reattach assumes the shell is idle (no in-flight `.run()` from another client). If two clients try to drive the same shell concurrently, their output will interleave — coordinate at the application level.
</Note>

### Terminal-tab semantics

Running `exit` (or `exit N`) inside `sh.run()` closes the shell — same as closing a terminal tab. The pending `.run()` rejects with `ShellClosedError` and any subsequent `.run()` on the same `Shell` also rejects. Start a fresh `shell()` if you need another one.

### Streaming output

Pass `onStdout`/`onStderr` to `sh.run()` and they fire as bytes arrive, before the promise resolves. Two caveats worth knowing:

* **bash builtin output is block-buffered.** `echo`, `printf`, and other builtins go through glibc `stdio`, which only flushes when the buffer fills (\~4 KB) or bash exits. If you want live output from a simple loop, use an external binary (`/bin/echo`) or a tool that flushes explicitly (`python -u`, `stdbuf -oL <cmd>`).
* **Upstream hops may coalesce small frames.** Chunks travel bash → agent → worker (gRPC) → WS → CDN → client. Any of those hops is allowed to combine small frames under light load, so a short command may arrive as one chunk even if its output was produced incrementally. Real workloads (builds, installs, servers) produce enough output that streaming is visible in practice.

See `sdks/typescript/examples/stream-demo.ts` / `sdks/python/examples/stream_demo.py` for a \~6-second apt-install simulation that prints per-chunk arrival timestamps — a good way to eyeball streaming behavior against your deployment.

<Note>
  Per-call `cwd`, `env`, and `timeout` are intentionally not supported in v1. Use inline shell syntax (`cd /x && cmd`, `FOO=bar cmd`) — the shell state carries across calls. Use `exec.background()` for fire-and-forget processes.
</Note>

## Managing Sessions

### List Sessions

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    const sessions = await sandbox.exec.list();
    for (const s of sessions) {
      console.log(s.sessionID, s.running ? "running" : `exited (${s.exitCode})`);
      console.log(`  command: ${s.command}, clients: ${s.attachedClients}`);
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python
    sessions = await sandbox.exec.list()
    for s in sessions:
        status = "running" if s.running else f"exited ({s.exit_code})"
        print(s.session_id, status, f"clients: {s.attached_clients}")
    ```
  </Tab>
</Tabs>

### Attach to Running Session

Reconnect to a running exec session to resume streaming output:

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    const session = await sandbox.exec.attach(sessionId, {
      onStdout: (data) => process.stdout.write(data),
      onStderr: (data) => process.stderr.write(data),
      onExit: (code) => console.log("Exited:", code),
      onScrollbackEnd: () => console.log("--- live output ---"),
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    session = await sandbox.exec.attach(
        session_id,
        on_stdout=lambda b: print(b.decode(), end=""),
        on_stderr=lambda b: print(b.decode(), end=""),
        on_exit=lambda code: print(f"Exited: {code}"),
        on_scrollback_end=lambda: print("--- live output ---"),
    )
    ```
  </Tab>
</Tabs>

On attach, the server replays the scrollback buffer (historical output), sends a scrollback-end marker, then streams live output.

### Kill a Session

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    await sandbox.exec.kill(sessionId);
    await sandbox.exec.kill(sessionId, 15); // SIGTERM
    ```
  </Tab>

  <Tab title="Python">
    ```python
    await sandbox.exec.kill(session_id)
    await sandbox.exec.kill(session_id, signal=15)  # SIGTERM
    ```
  </Tab>
</Tabs>

<Warning>
  `sandbox.commands` is a deprecated alias for `sandbox.exec`. Use `sandbox.exec` in all new code.
</Warning>

<Tip>
  CLI equivalent: [`oc exec`](/cli/exec). Full reference: [TypeScript SDK](/reference/typescript-sdk#exec) · [Python SDK](/reference/python-sdk#exec) · [HTTP API](/reference/api#commands-exec).
</Tip>


---
title: Working with Files
url: "https://docs-v1.opencomputer.dev/sandboxes/working-with-files.md"
description: "Read, write, and manage files inside a sandbox"
---

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    import { Sandbox } from "@opencomputer/sdk";

    const sandbox = await Sandbox.create();

    await sandbox.files.write("/app/hello.txt", "Hello, World!");
    const content = await sandbox.files.read("/app/hello.txt");
    console.log(content); // "Hello, World!"

    await sandbox.kill();
    ```
  </Tab>

  <Tab title="Python">
    ```python
    from opencomputer import Sandbox

    async with await Sandbox.create() as sandbox:
        await sandbox.files.write("/app/hello.txt", "Hello, World!")
        content = await sandbox.files.read("/app/hello.txt")
        print(content)  # "Hello, World!"
    ```
  </Tab>
</Tabs>

## Reading Files

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    // Read as string
    const text = await sandbox.files.read("/app/config.json");

    // Read as bytes
    const bytes = await sandbox.files.readBytes("/app/image.png");
    ```
  </Tab>

  <Tab title="Python">
    ```python
    # Read as string
    text = await sandbox.files.read("/app/config.json")

    # Read as bytes
    data = await sandbox.files.read_bytes("/app/image.png")
    ```
  </Tab>
</Tabs>

## Writing Files

Content can be a string or binary data:

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    // Write text
    await sandbox.files.write("/app/config.json", JSON.stringify({ port: 3000 }));

    // Write binary
    const imageData = new Uint8Array([...]);
    await sandbox.files.write("/app/logo.png", imageData);
    ```
  </Tab>

  <Tab title="Python">
    ```python
    # Write text
    await sandbox.files.write("/app/config.json", '{"port": 3000}')

    # Write binary
    await sandbox.files.write("/app/logo.png", image_bytes)
    ```
  </Tab>
</Tabs>

## Listing Directories

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    const entries = await sandbox.files.list("/app");
    for (const entry of entries) {
      console.log(entry.isDir ? "📁" : "📄", entry.name, entry.size);
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python
    entries = await sandbox.files.list("/app")
    for entry in entries:
        kind = "dir" if entry.is_dir else "file"
        print(f"{kind}: {entry.name} ({entry.size} bytes)")
    ```
  </Tab>
</Tabs>

### EntryInfo

| Field         | TypeScript | Python   | Type    |
| ------------- | ---------- | -------- | ------- |
| Name          | `name`     | `name`   | string  |
| Is directory  | `isDir`    | `is_dir` | boolean |
| Full path     | `path`     | `path`   | string  |
| Size in bytes | `size`     | `size`   | number  |

## Managing Files

### Create Directory

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    await sandbox.files.makeDir("/app/data");
    ```
  </Tab>

  <Tab title="Python">
    ```python
    await sandbox.files.make_dir("/app/data")
    ```
  </Tab>
</Tabs>

### Remove

Deletes a file or directory:

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    await sandbox.files.remove("/app/temp");
    ```
  </Tab>

  <Tab title="Python">
    ```python
    await sandbox.files.remove("/app/temp")
    ```
  </Tab>
</Tabs>

### Check Existence

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    if (await sandbox.files.exists("/app/config.json")) {
      const config = await sandbox.files.read("/app/config.json");
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python
    if await sandbox.files.exists("/app/config.json"):
        config = await sandbox.files.read("/app/config.json")
    ```
  </Tab>
</Tabs>

<Note>
  `exists()` is a client-side convenience — it attempts a file read and returns `false` on error. There is no dedicated HTTP endpoint for existence checks.
</Note>

## Large Files

All file operations are streamed end-to-end using 256KB chunks — there is no file size limit beyond available disk space. Files up to 200MB+ transfer reliably at \~20 MB/s.

The standard `read()`, `readBytes()`, and `write()` methods work for large files, but they buffer the full content in memory on the client side. For very large files, use the streaming methods to avoid high memory usage:

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    // Stream a large file download — returns a ReadableStream, not a buffer
    const stream = await sandbox.files.readStream("/data/model.bin");

    // Pipe to a file, or process chunks incrementally
    const reader = stream.getReader();
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      // process each Uint8Array chunk...
    }

    // Stream a large file upload — accepts ReadableStream or Uint8Array
    const fileStream = someReadableStream; // e.g. from fs.createReadStream
    await sandbox.files.writeStream("/data/model.bin", fileStream);

    // Uint8Array also works (sent without buffering a copy)
    await sandbox.files.writeStream("/data/output.bin", largeUint8Array);
    ```
  </Tab>

  <Tab title="Python">
    ```python
    # Stream a large file download — returns an async byte iterator
    stream = await sandbox.files.read_stream("/data/model.bin")
    async for chunk in stream:
        # process each bytes chunk...
        pass

    # Stream a large file upload — accepts an async iterator
    async def file_chunks():
        with open("local_model.bin", "rb") as f:
            while chunk := f.read(256 * 1024):
                yield chunk

    await sandbox.files.write_stream("/data/model.bin", file_chunks())
    ```
  </Tab>
</Tabs>

## Examples

### Upload and Run a Script

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    await sandbox.files.write("/tmp/setup.sh", `#!/bin/bash
    apt-get update && apt-get install -y redis-server
    redis-server --daemonize yes
    `);
    await sandbox.exec.run("chmod +x /tmp/setup.sh && /tmp/setup.sh");
    ```
  </Tab>

  <Tab title="Python">
    ```python
    await sandbox.files.write("/tmp/setup.sh", """#!/bin/bash
    apt-get update && apt-get install -y redis-server
    redis-server --daemonize yes
    """)
    await sandbox.exec.run("chmod +x /tmp/setup.sh && /tmp/setup.sh")
    ```
  </Tab>
</Tabs>

<Tip>
  Full reference: [TypeScript SDK](/reference/typescript-sdk#filesystem) · [Python SDK](/reference/python-sdk#filesystem) · [HTTP API](/reference/api#filesystem).
</Tip>


---
title: Mounts
url: "https://docs-v1.opencomputer.dev/sandboxes/mounts.md"
description: "Mount S3, GCS, Azure Blob, SFTP, WebDAV, and Dropbox into a sandbox via FUSE"
---

<Note>
  **Preview:** the mounts API is new. Endpoints, request shape, and SDK
  method names may change before GA.
</Note>

Mount remote filesystems directly into your sandbox so your code reads and
writes them like local files — no SDK calls, no chunked downloads, no
boilerplate. Perfect for pulling in datasets, model weights, or a customer's
bucket without staging anything to disk first.

```typescript
import { Sandbox } from "@opencomputer/sdk";

const sandbox = await Sandbox.create();

await sandbox.mounts.add({
  path: "/mnt/data",
  remote: "s3:my-bucket/datasets",
  backend: "s3",
  creds: {
    access_key_id: process.env.AWS_ACCESS_KEY_ID!,
    secret_access_key: process.env.AWS_SECRET_ACCESS_KEY!,
    region: "us-east-1",
  },
});

// Now anything in the sandbox can read s3://my-bucket/datasets as /mnt/data:
await sandbox.exec.run("ls /mnt/data");
await sandbox.exec.run("head /mnt/data/train.csv");
```

## How it works

Mounts use [`rclone mount`](https://rclone.org/commands/rclone_mount/) under the
hood — a single binary that speaks \~40 backends (S3, GCS, Azure Blob, SFTP,
WebDAV, Dropbox, and more). When you call `mounts.add()`:

1. The control plane templates an rclone config from your `backend` + `creds`.
2. The config is written to a tmpfs file inside the VM (mode `0600` — the
   sandbox user can't read it).
3. `rclone mount` is spawned as a background daemon, exposing the remote at
   the path you specified.
4. Every process in the sandbox can now read/write that path.

Credentials live only inside the VM's tmpfs for the lifetime of the mount.
The worker keeps no copy.

## Supported Backends

`backend` selects how `creds` are templated. The keys you pass map directly to
[rclone config fields](https://rclone.org/docs/) for that backend type.

| Backend     | Common keys                                                                            |
| ----------- | -------------------------------------------------------------------------------------- |
| `s3`        | `access_key_id`, `secret_access_key`, `region`, `provider` (default `AWS`), `endpoint` |
| `gcs`       | `service_account_credentials` (JSON string) or `service_account_file`                  |
| `azureblob` | `account`, `key` *or* `sas_url`                                                        |
| `sftp`      | `host`, `user`, `pass` or `key_file`, `port`                                           |
| `webdav`    | `url`, `vendor`, `user`, `pass`                                                        |
| `dropbox`   | `token`                                                                                |

For anything not in this list, or for advanced tuning, pass `rcloneConfig`
directly with a raw rclone config string — see [Custom Config](#custom-config)
below.

## Examples

### S3 (AWS)

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    await sandbox.mounts.add({
      path: "/mnt/data",
      remote: "s3:my-bucket",
      backend: "s3",
      creds: {
        access_key_id: process.env.AWS_ACCESS_KEY_ID!,
        secret_access_key: process.env.AWS_SECRET_ACCESS_KEY!,
        region: "us-east-1",
      },
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    await sandbox.mounts.add(
        path="/mnt/data",
        remote="s3:my-bucket",
        backend="s3",
        creds={
            "access_key_id": os.environ["AWS_ACCESS_KEY_ID"],
            "secret_access_key": os.environ["AWS_SECRET_ACCESS_KEY"],
            "region": "us-east-1",
        },
    )
    ```
  </Tab>
</Tabs>

### S3-compatible (MinIO, R2, Tigris, etc.)

Override the `provider` and point at the endpoint:

```typescript
await sandbox.mounts.add({
  path: "/mnt/r2",
  remote: "r2:my-bucket",
  backend: "s3",
  creds: {
    provider: "Cloudflare",
    access_key_id: "...",
    secret_access_key: "...",
    endpoint: "https://<accountid>.r2.cloudflarestorage.com",
    region: "auto",
  },
});
```

### Google Cloud Storage

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    import fs from "node:fs";

    await sandbox.mounts.add({
      path: "/mnt/gcs",
      remote: "gcs:my-bucket",
      backend: "gcs",
      creds: {
        service_account_credentials: fs.readFileSync("./sa-key.json", "utf8"),
      },
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    with open("./sa-key.json") as f:
        sa_json = f.read()

    await sandbox.mounts.add(
        path="/mnt/gcs",
        remote="gcs:my-bucket",
        backend="gcs",
        creds={"service_account_credentials": sa_json},
    )
    ```
  </Tab>
</Tabs>

### SFTP

```typescript
await sandbox.mounts.add({
  path: "/mnt/remote",
  remote: "ssh:/home/data",
  backend: "sftp",
  creds: {
    host: "data.example.com",
    user: "agent",
    pass: process.env.SSH_PASSWORD!, // or key_file
  },
});
```

### Custom Config

For backends not in the typed list — or to tune things like `--vfs-cache-mode`
or `--dir-cache-time` — pass an rclone config string directly. The section
name in the config must match the part of `remote` before the colon.

```typescript
await sandbox.mounts.add({
  path: "/mnt/box",
  remote: "box:Reports",
  rcloneConfig: `
[box]
type = box
token = {"access_token":"...","refresh_token":"...","expiry":"..."}
`.trim(),
  mountOptions: ["--dir-cache-time", "1m"],
});
```

## Read-only by default

Mounts are read-only unless you explicitly opt into read-write:

```typescript
await sandbox.mounts.add({
  path: "/mnt/writable",
  remote: "s3:scratch",
  backend: "s3",
  creds: {...},
  readOnly: false,    // opts into RW; uses --vfs-cache-mode writes
});
```

<Warning>
  Object-store FUSE mounts have well-known write footguns — concurrent writers
  to the same key can produce surprising results, and small-write workloads
  amplify request counts (and your bill). Prefer object-store SDKs for
  write-heavy workloads; use mounts for read-heavy access and append-style
  artifacts.
</Warning>

## Listing and removing

```typescript
const mounts = await sandbox.mounts.list();
// [{ path: "/mnt/data", remote: "s3:my-bucket", backend: "s3", readOnly: true }]

await sandbox.mounts.remove("/mnt/data");
```

## Hibernate behavior

Mounts survive hibernate/wake transparently. The VM snapshot captures the live
FUSE mount and the rclone daemon process; when the sandbox wakes, both are
restored as-is. No re-mount call needed — the mount is exactly where you left
it, with the same credentials, serving the same path.

```typescript
await sandbox.mounts.add({ path: "/mnt/data", remote: "s3:my-bucket", backend: "s3", creds });

await sandbox.hibernate();
await sandbox.wake();

await sandbox.exec.run("ls /mnt/data");   // still works, no re-mount needed
```

When you want a mount gone, call `remove()` explicitly:

```typescript
await sandbox.mounts.remove("/mnt/data");
```

That triggers an actual `fusermount3 -u` in the VM and drops the registry
entry. No magic teardown on hibernate.

### Stale credentials

If the credentials behind a mount get rotated or revoked while the sandbox is
hibernated, the in-VM rclone daemon — restored intact from the snapshot — is
still holding the *old* keys and will start hitting auth errors on the next
request. Fix: `mounts.remove(path)` then `mounts.add(...)` again with fresh
credentials.

## CLI

```bash
oc mounts add sb-abc123 \
  --path /mnt/data \
  --remote s3:my-bucket \
  --backend s3 \
  --cred access_key_id=AKIA... \
  --cred secret_access_key=... \
  --cred region=us-east-1

oc mounts list sb-abc123
oc mounts rm sb-abc123 /mnt/data
```

For raw rclone configs, point `--config-file` at a local file:

```bash
oc mounts add sb-abc123 --path /mnt/box --remote box:Reports --config-file ./rclone.conf
```

## Troubleshooting

**`sandbox image is missing rclone and/or fusermount3`** — the sandbox is
running on an older base image. Recreate the sandbox from the latest default
template, or build an image off the current `Dockerfile.default`.

**Mount succeeds but `ls` returns empty** — usually a creds /
permission issue on the remote side. SSH in (`oc shell`) and run
`rclone ls <remote>: --config /run/oc-agent/mounts/<id>.conf` to see the real
error. Or `ls -la` the mount point — FUSE errors surface as filesystem errors.

**`fuse: bad mount point ... permission denied`** — make sure the target path
isn't already a non-empty directory you don't own. The mount call creates the
path with `sandbox:sandbox` ownership; pre-existing roots can conflict.

<Tip>
  CLI equivalent: [`oc mounts`](/cli/mounts). Full reference:
  [TypeScript SDK](/reference/typescript-sdk#mounts) ·
  [Python SDK](/reference/python-sdk#mounts).
</Tip>

## Bring your own FUSE

<Note>
  Advanced — most users want the rclone path above. Reach for this only if you
  already have a FUSE-ready filesystem.
</Note>

Under the hood, `mounts.add()` picks a **driver** — the daemon that actually
establishes the FUSE mount. Everything above uses the default `rclone` driver.
If you already have a FUSE-ready filesystem (your own VFS, gcsfuse, s3fs, …) and
don't want rclone as a middle layer, use the **`command` driver**: you hand us
the daemon's argv and we run it as the sandbox user, inject your env/secrets,
wait for the mount to come live, and tear it down on `remove` — the same
lifecycle as an rclone mount.

```typescript
await sandbox.mounts.add({
  path: "/mnt/data",
  driver: "command",
  command: ["my-vfs-fuse", "--bucket", "gs://my-bucket", "{mountpoint}"],
  secrets: { MY_VFS_TOKEN: process.env.MY_VFS_TOKEN! },
});
```

In Python this is a separate method, `mounts.add_command(...)`.

### The `{mountpoint}` token

`{mountpoint}` (or `{path}`) in your argv is replaced with `path` before the
command runs, so you don't repeat the path twice. The call above runs:

```
my-vfs-fuse --bucket gs://my-bucket /mnt/data
```

You can also hardcode the path — the token is just a convenience.

### env and secrets

* **`env`** — plain environment variables for the daemon. Recorded and returned
  by `list()`.
* **`secrets`** — credentials. Injected into the daemon's **process
  environment** (never the command line, so they don't leak via `ps`), never
  written to disk on the worker, and never returned by `list()`.

For daemons that need a credentials *file* (e.g. a service-account JSON), write
it first with the [filesystem API](/sandboxes/filesystem) and point the command
at it.

`readOnly` is **advisory** for this driver — your command must honor it (we also
export `OC_MOUNT_READONLY=1`). Unlike rclone, the platform can't enforce
read-only on an arbitrary daemon.

### Requirements

Your daemon must:

* be present in the sandbox — bake it into your image, or install/download it at
  runtime before mounting;
* mount at the mountpoint and keep running (a foreground daemon is fine — we
  background it; a self-daemonizing one works too);
* exit when its mount is unmounted (standard libfuse behavior) so `remove`
  cleans up.

The platform handles the rest: `/dev/fuse` access, creating the mountpoint, and
`-o allow_other` support (`user_allow_other` is set in `/etc/fuse.conf`). If the
mount doesn't come up within the timeout, the call fails with the **daemon's own
log tail** so you can see why (a bad flag, missing binary, auth error, etc.).

### Example: gcsfuse

[gcsfuse](https://cloud.google.com/storage/docs/gcsfuse-install) is Google's own
GCS-to-FUSE adapter. Install it in your image (or at runtime), write your
service-account key with the [filesystem API](/sandboxes/filesystem), and mount:

```typescript
await sandbox.files.write("/run/gcp-sa.json", process.env.GCP_SA_JSON!);

await sandbox.mounts.add({
  path: "/mnt/bucket",
  driver: "command",
  command: [
    "gcsfuse", "--foreground", "--implicit-dirs",
    "-o", "allow_other",
    "--key-file", "/run/gcp-sa.json",
    "my-bucket", "{mountpoint}",
  ],
});

await sandbox.exec.run("ls /mnt/bucket");   // GCS objects, read like local files
```

For a **public** bucket, skip the key and pass `--anonymous-access`:

```typescript
command: ["gcsfuse", "--foreground", "--anonymous-access", "-o", "allow_other",
          "gcp-public-data-landsat", "{mountpoint}"],
```

### Example: bindfs

A minimal local FUSE — mirror one directory onto another path:

```typescript
await sandbox.exec.run("apt-get update && apt-get install -y bindfs");
await sandbox.mounts.add({
  path: "/mnt/mirror",
  driver: "command",
  command: ["bindfs", "-f", "-o", "allow_other", "/home/sandbox/data", "{mountpoint}"],
  readOnly: false,
});
// reads/writes under /mnt/mirror now pass through to /home/sandbox/data
```

### CLI

Pass `--command` once per argv element; `--env`/`--secret` take `key=value`:

```bash
oc mounts add sb-abc123 \
  --path /mnt/mirror \
  --command bindfs --command -f --command -o --command allow_other \
  --command /home/sandbox/data --command '{mountpoint}' \
  --secret MY_VFS_TOKEN=...
```

If the mount doesn't come up, the error includes the **daemon's log tail** —
usually a missing binary, a bad flag, or an auth failure. Reproduce by hand with
`oc shell` and run the same command.


---
title: Signed URLs
url: "https://docs-v1.opencomputer.dev/sandboxes/signed-urls.md"
description: Generate pre-signed URLs for direct file uploads and downloads without API keys
---

Signed URLs let you upload or download files from a sandbox without exposing your API key. This is useful for:

* Letting browsers download files directly from a sandbox
* Sharing temporary download links with users
* Uploading files from untrusted clients (e.g. a frontend app)

## How It Works

1. Your backend generates a signed URL using your API key
2. The URL contains an HMAC signature and expiry timestamp
3. Anyone with the URL can upload/download — no API key needed
4. The URL expires automatically (default: 1 hour, max: 24 hours)

## Generating URLs

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    import { Sandbox } from "@opencomputer/sdk";

    const sandbox = await Sandbox.create();

    // Generate a download URL (default: 1 hour expiry)
    const downloadUrl = await sandbox.downloadUrl("/app/output.zip");

    // Generate an upload URL
    const uploadUrl = await sandbox.uploadUrl("/app/input.csv");

    // Custom expiry (in seconds)
    const shortUrl = await sandbox.downloadUrl("/app/output.zip", { expiresIn: 300 }); // 5 minutes
    ```
  </Tab>

  <Tab title="Python">
    ```python
    from opencomputer import Sandbox

    sandbox = await Sandbox.create()

    # Generate a download URL (default: 1 hour expiry)
    download_url = await sandbox.download_url("/app/output.zip")

    # Generate an upload URL
    upload_url = await sandbox.upload_url("/app/input.csv")

    # Custom expiry (in seconds)
    short_url = await sandbox.download_url("/app/output.zip", expires_in=300)  # 5 minutes
    ```
  </Tab>
</Tabs>

## Downloading Files

Use the signed download URL with a plain GET request — no headers required:

<Tabs items={["TypeScript", "Python", "cURL"]}>
  <Tab title="TypeScript">
    ```typescript
    const url = await sandbox.downloadUrl("/app/results.csv");

    // Fetch from browser or server — no API key needed
    const resp = await fetch(url);
    const data = await resp.arrayBuffer();
    ```
  </Tab>

  <Tab title="Python">
    ```python
    import httpx

    url = await sandbox.download_url("/app/results.csv")

    async with httpx.AsyncClient() as http:
        resp = await http.get(url)
        data = resp.content
    ```
  </Tab>

  <Tab title="cURL">
    ```bash
    # The URL is self-contained — just GET it
    curl -o results.csv "https://app.opencomputer.dev/api/sandboxes/sb-xxx/files/download?path=%2Fapp%2Fresults.csv&expires=1773906434&signature=abc123"
    ```
  </Tab>
</Tabs>

## Uploading Files

Use the signed upload URL with a PUT request:

<Tabs items={["TypeScript", "Python", "cURL"]}>
  <Tab title="TypeScript">
    ```typescript
    const url = await sandbox.uploadUrl("/app/data.csv");

    // Upload from browser or server — no API key needed
    await fetch(url, {
      method: "PUT",
      body: fileContent, // string, Uint8Array, Blob, etc.
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    import httpx

    url = await sandbox.upload_url("/app/data.csv")

    async with httpx.AsyncClient() as http:
        await http.put(url, content=file_bytes)
    ```
  </Tab>

  <Tab title="cURL">
    ```bash
    curl -X PUT --data-binary @data.csv "https://app.opencomputer.dev/api/sandboxes/sb-xxx/files/upload?path=%2Fapp%2Fdata.csv&expires=1773906434&signature=abc123"
    ```
  </Tab>
</Tabs>

## Large Files

Signed URLs support files of any size — the entire pipeline streams data in 256KB chunks without buffering. Files up to 200MB+ have been tested at \~20 MB/s throughput.

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    // Upload a 100MB file via signed URL
    const data = new Uint8Array(100 * 1024 * 1024);
    const uploadUrl = await sandbox.uploadUrl("/data/model.bin");
    await fetch(uploadUrl, { method: "PUT", body: data });

    // Download it back
    const downloadUrl = await sandbox.downloadUrl("/data/model.bin");
    const resp = await fetch(downloadUrl);
    const downloaded = new Uint8Array(await resp.arrayBuffer());
    ```
  </Tab>

  <Tab title="Python">
    ```python
    import httpx

    # Upload a 100MB file via signed URL
    data = b"\x00" * (100 * 1024 * 1024)
    upload_url = await sandbox.upload_url("/data/model.bin")
    async with httpx.AsyncClient(timeout=120.0) as http:
        await http.put(upload_url, content=data)

    # Download it back
    download_url = await sandbox.download_url("/data/model.bin")
    async with httpx.AsyncClient(timeout=120.0) as http:
        resp = await http.get(download_url)
        downloaded = resp.content
    ```
  </Tab>
</Tabs>

## Security

* **Tamper-proof:** URLs are signed with HMAC-SHA256. Changing the path, sandbox ID, or expiry invalidates the signature.
* **Time-limited:** URLs expire after the specified duration (default 1 hour, max 24 hours). Expired URLs return `403`.
* **Operation-scoped:** Download URLs cannot be used for uploads and vice versa.
* **Path-scoped:** Each URL is bound to a specific file path. Changing the path in the URL returns `403`.

<Tip>
  Full reference: [TypeScript SDK](/reference/typescript-sdk#filesystem) · [Python SDK](/reference/python-sdk#filesystem) · [HTTP API](/api-reference/files/generate-download-url).
</Tip>


---
title: Interactive Terminals
url: "https://docs-v1.opencomputer.dev/sandboxes/interactive-terminals.md"
description: Full PTY sessions inside sandboxes
---

A PTY session is a full interactive terminal inside the sandbox — like SSH but over WebSocket. Supports colors, cursor movement, and full-screen applications (vim, htop).

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    import { Sandbox } from "@opencomputer/sdk";

    const sandbox = await Sandbox.create();
    const pty = await sandbox.pty.create({
      onOutput: (data) => process.stdout.write(data),
    });

    pty.send("ls -la /app\n");
    // ... wait for output ...
    pty.close();
    await sandbox.kill();
    ```
  </Tab>

  <Tab title="Python">
    ```python
    from opencomputer import Sandbox

    sandbox = await Sandbox.create()
    pty = await sandbox.pty.create(
        on_output=lambda data: print(data.decode(), end=""),
    )

    pty.send("ls -la /app\n")
    output = await pty.recv()  # pull-based alternative
    pty.close()
    await sandbox.kill()
    ```
  </Tab>
</Tabs>

## Creating a PTY

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    const pty = await sandbox.pty.create({
      cols: 120,
      rows: 40,
      onOutput: (data) => process.stdout.write(data),
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    pty = await sandbox.pty.create(
        cols=120,
        rows=40,
        on_output=lambda data: print(data.decode(), end=""),
    )
    ```
  </Tab>
</Tabs>

### Parameters

| Parameter  | Type     | Default | Description                                                 |
| ---------- | -------- | ------- | ----------------------------------------------------------- |
| `cols`     | number   | `80`    | Terminal columns                                            |
| `rows`     | number   | `24`    | Terminal rows                                               |
| `onOutput` | callback | —       | Called with output data (TS: `Uint8Array`, Python: `bytes`) |

## PtySession

| Member         | TypeScript              | Python              | Description                 |
| -------------- | ----------------------- | ------------------- | --------------------------- |
| Session ID     | `pty.sessionId`         | `pty.session_id`    | Unique session ID           |
| Send input     | `pty.send(data)`        | `pty.send(data)`    | Send keystrokes or commands |
| Receive output | via `onOutput` callback | `await pty.recv()`  | Get terminal output         |
| Close          | `pty.close()`           | `await pty.close()` | Close the PTY session       |

<Note>
  `recv()` is Python-only — a pull-based alternative to the `on_output` callback. Returns raw bytes.
</Note>

### Resize

PTY resize is available via the [HTTP API](/reference/api#pty) only (`POST /api/sandboxes/:id/pty/:sessionID/resize`). The SDKs do not expose a resize method — the CLI handles resize automatically for interactive sessions.

## CLI

The fastest way to get an interactive shell:

```bash
oc shell sb-abc123
oc shell sb-abc123 --shell /bin/zsh
```

Terminal size is auto-detected and resize events are forwarded. See [`oc shell`](/cli/shell) for details.

<Tip>
  CLI equivalent: [`oc shell`](/cli/shell). Full reference: [TypeScript SDK](/reference/typescript-sdk#pty) · [Python SDK](/reference/python-sdk#pty) · [HTTP API](/reference/api#pty).
</Tip>


---
title: "Timeout & Hibernation"
url: "https://docs-v1.opencomputer.dev/sandboxes/timeout.md"
description: Control how long a sandbox stays alive when idle
---

Sandboxes have a rolling **idle timeout** that determines when they auto-hibernate to save cost. The timeout is what you'd expect from a "laptop lid" — the sandbox sleeps when nothing is happening and wakes the moment you talk to it again.

## Default: persistent (no auto-hibernate)

By default the timeout is `0`, which means **never auto-hibernate**. The sandbox stays running until you explicitly kill or hibernate it.

<Tabs items={["TypeScript", "Python", "CLI"]}>
  <Tab title="TypeScript">
    ```typescript
    import { Sandbox } from "@opencomputer/sdk";

    // No `timeout` — sandbox is persistent (stays alive until killed).
    const sandbox = await Sandbox.create();
    ```
  </Tab>

  <Tab title="Python">
    ```python
    from opencomputer import Sandbox

    # No `timeout` — sandbox is persistent (stays alive until killed).
    sandbox = await Sandbox.create()
    ```
  </Tab>

  <Tab title="CLI">
    ```bash
    # --timeout defaults to 0 (never hibernate).
    oc sandbox create
    ```
  </Tab>
</Tabs>

<Note>
  Persistent sandboxes keep accruing compute cost for as long as they're running. If you only need the sandbox to respond to occasional requests, set a timeout so it hibernates between them — see below.
</Note>

## Setting a timeout

Pass `timeout` in seconds to have the sandbox auto-hibernate after N seconds of **idle** time. The timer resets on every operation — exec, file access, agent activity — so a busy sandbox never hits the timeout.

<Tabs items={["TypeScript", "Python", "CLI"]}>
  <Tab title="TypeScript">
    ```typescript
    // Auto-hibernate after 5 minutes of inactivity.
    const sandbox = await Sandbox.create({ timeout: 300 });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    # Auto-hibernate after 5 minutes of inactivity.
    sandbox = await Sandbox.create(timeout=300)
    ```
  </Tab>

  <Tab title="CLI">
    ```bash
    oc sandbox create --timeout 300
    ```
  </Tab>
</Tabs>

### Update on a running sandbox

You can change the timeout at any time:

<Tabs items={["TypeScript", "Python", "CLI"]}>
  <Tab title="TypeScript">
    ```typescript
    await sandbox.setTimeout(600); // 10 minutes
    await sandbox.setTimeout(0);   // make it persistent
    ```
  </Tab>

  <Tab title="Python">
    ```python
    await sandbox.set_timeout(600)
    await sandbox.set_timeout(0)
    ```
  </Tab>

  <Tab title="CLI">
    ```bash
    oc sandbox set-timeout sb-abc123 600
    oc sandbox set-timeout sb-abc123 0
    ```
  </Tab>
</Tabs>

## What happens when the timeout expires

When an idle sandbox hits its timeout, the platform:

1. Snapshots the VM state (memory + disk) and stops the VM — no compute cost while hibernated.
2. Marks the sandbox `hibernated`. Its ID, files, and configuration are preserved.

You don't pay for CPU/memory while a sandbox is hibernated — only for the stored snapshot.

## Waking up

**Any operation from the SDK automatically wakes a hibernated sandbox.** You don't need to call `wake()` yourself — calling `sandbox.exec.run(...)`, reading a file, or any other operation transparently restores the VM and then performs the requested action.

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    // Earlier: sandbox auto-hibernated after 5 minutes of idleness.
    // Now, hours later — this call just works. The SDK wakes it first.
    const result = await sandbox.exec.run("echo hello");
    ```
  </Tab>

  <Tab title="Python">
    ```python
    # Same behavior in Python.
    result = await sandbox.exec.run("echo hello")
    ```
  </Tab>
</Tabs>

Wake is typically sub-second thanks to snapshot restore, with a cold-boot fallback if the snapshot isn't available. Once woken, the idle timer restarts with the sandbox's configured timeout.

<Note>
  You can still call `sandbox.wake()` explicitly if you want to pre-warm a sandbox before routing user traffic to it.
</Note>

## Choosing a timeout

| Use case                                          | Suggested `timeout`          |
| ------------------------------------------------- | ---------------------------- |
| Always-on agent / bot that must respond instantly | `0` (persistent)             |
| Interactive dev sandbox, keep for the session     | `600`–`3600` (10 min – 1 hr) |
| Short-lived batch or test run                     | `60`–`300` (1–5 min)         |
| One-off exec — kill it yourself when done         | `0` + explicit `kill()`      |

The tradeoff is latency vs cost: a lower timeout saves money but adds wake latency on the next request after idleness.

## See also

* [Lifecycle overview](/sandboxes/overview#lifecycle) — all sandbox states
* [Set timeout API](/api-reference/sandboxes/set-timeout)
* [Hibernate API](/api-reference/sandboxes/hibernate) / [Wake API](/api-reference/sandboxes/wake)


---
title: Checkpoints
url: "https://docs-v1.opencomputer.dev/sandboxes/checkpoints.md"
description: Snapshot and fork sandbox state
---

A checkpoint is a named snapshot of a running sandbox. Fork new sandboxes from it to start from a known-good environment — like git branches for VMs.

## Checkpoint Types

OpenComputer supports two checkpoint modes:

| Type                 | Preserves                       | Use when                                                                                                                                                                                        |
| -------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Full checkpoint      | Disk, memory, and CPU state     | You want the fastest fork or restore path from an exact running VM state. This is best for templates, important milestones, and branches you expect to fork often.                              |
| Disk-only checkpoint | Rootfs and workspace disk state | You want lightweight, frequent snapshots of files, installed packages, and workspace changes. Forks boot from the saved disks, so they are cheaper to keep but may take longer to become ready. |

Full checkpoints are the default. Use `kind: "disk_only"` when creating autosaves or high-frequency checkpoints where disk state is enough.

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    import { Sandbox } from "@opencomputer/sdk";

    const sandbox = await Sandbox.create();
    await sandbox.exec.run("npm install && npm run build", { cwd: "/app" });

    // Checkpoint after setup
    const cp = await sandbox.createCheckpoint("after-build");

    // Fork two independent sandboxes
    const a = await Sandbox.createFromCheckpoint(cp.id);
    const b = await Sandbox.createFromCheckpoint(cp.id);

    await a.exec.run("npm run test:unit", { cwd: "/app" });
    await b.exec.run("npm run test:e2e", { cwd: "/app" });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    from opencomputer import Sandbox

    sandbox = await Sandbox.create()
    await sandbox.exec.run("npm install && npm run build", cwd="/app")

    cp = await sandbox.create_checkpoint("after-build")

    a = await Sandbox.create_from_checkpoint(cp["id"])
    b = await Sandbox.create_from_checkpoint(cp["id"])

    await a.exec.run("npm run test:unit", cwd="/app")
    await b.exec.run("npm run test:e2e", cwd="/app")
    ```
  </Tab>
</Tabs>

## Checkpoints vs Hibernation

|                  | Checkpoint                                                    | Hibernation                           |
| ---------------- | ------------------------------------------------------------- | ------------------------------------- |
| Purpose          | Fork new sandboxes from saved state                           | Pause and resume the **same** sandbox |
| Original sandbox | Keeps running                                                 | Stopped                               |
| Can fork         | Yes — unlimited new sandboxes                                 | No                                    |
| Count            | Up to 10 per sandbox, with optional oldest-checkpoint cleanup | One hibernation state                 |
| Use case         | Parallel testing, branching experiments                       | Cost savings, idle timeout            |

## What Gets Preserved

Checkpoints capture the filesystem and installed state. Forked sandboxes start with a fresh boot from that disk state — the platform attempts warm restore when possible, but may fall back to a cold boot. Don't assume running processes carry over.

## API Reference

### Create Checkpoint

<Tabs items={["TypeScript", "Python", "CLI"]}>
  <Tab title="TypeScript">
    ```typescript
    const checkpoint = await sandbox.createCheckpoint("before-migration");
    // checkpoint.id, checkpoint.status ("processing" → "ready")
    ```
  </Tab>

  <Tab title="Python">
    ```python
    checkpoint = await sandbox.create_checkpoint("before-migration")
    # checkpoint["id"], checkpoint["status"]
    ```
  </Tab>

  <Tab title="CLI">
    ```bash
    oc cp create sb-abc123 --name before-migration
    ```
  </Tab>
</Tabs>

Checkpoint name must be unique within the sandbox. Status transitions from `processing` to `ready` (or `failed`).

Each sandbox can have up to 10 full checkpoints and up to 100 disk-only checkpoints. To create a new checkpoint without failing at the limit, pass a retention policy that deletes the oldest eligible checkpoint of the same type first:

<Tabs items={["TypeScript", "Python", "CLI"]}>
  <Tab title="TypeScript">
    ```typescript
    const checkpoint = await sandbox.createCheckpoint("autosave", {
      kind: "disk_only",
      retentionPolicy: { mode: "delete_oldest", maxCount: 100 },
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    checkpoint = await sandbox.create_checkpoint(
        "autosave",
        kind="disk_only",
        retention_policy={"mode": "delete_oldest", "maxCount": 100},
    )
    ```
  </Tab>

  <Tab title="CLI">
    ```bash
    oc cp create sb-abc123 \
      --name autosave \
      --kind disk_only \
      --retention-policy delete_oldest \
      --retention-max-count 100
    ```
  </Tab>
</Tabs>

Retention skips checkpoints that are public, have patches, or are still referenced by forked sandboxes. If no eligible checkpoint of the requested type can be deleted, creation fails instead of deleting protected state.

### List Checkpoints

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    const checkpoints = await sandbox.listCheckpoints();
    ```
  </Tab>

  <Tab title="Python">
    ```python
    checkpoints = await sandbox.list_checkpoints()
    ```
  </Tab>
</Tabs>

### Fork from Checkpoint

Creates a new sandbox from a checkpoint:

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    const forked = await Sandbox.createFromCheckpoint(checkpointId, {
      timeout: 600,
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    forked = await Sandbox.create_from_checkpoint(checkpoint_id, timeout=600)
    ```
  </Tab>
</Tabs>

### Restore Checkpoint

Revert a sandbox in-place. All changes since the checkpoint are lost:

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    await sandbox.restoreCheckpoint(checkpointId);
    ```
  </Tab>

  <Tab title="Python">
    ```python
    await sandbox.restore_checkpoint(checkpoint_id)
    ```
  </Tab>
</Tabs>

### Delete Checkpoint

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    await sandbox.deleteCheckpoint(checkpointId);
    ```
  </Tab>

  <Tab title="Python">
    ```python
    await sandbox.delete_checkpoint(checkpoint_id)
    ```
  </Tab>
</Tabs>

## CheckpointInfo

| Field      | TypeScript  | Python      | Description                        |
| ---------- | ----------- | ----------- | ---------------------------------- |
| ID         | `id`        | `id`        | Checkpoint UUID                    |
| Sandbox ID | `sandboxId` | `sandboxID` | Source sandbox                     |
| Name       | `name`      | `name`      | Human-readable name                |
| Status     | `status`    | `status`    | `processing`, `ready`, or `failed` |
| Size       | `sizeBytes` | `sizeBytes` | Snapshot size in bytes             |
| Created    | `createdAt` | `createdAt` | Timestamp                          |

<Note>
  The TypeScript SDK returns typed `CheckpointInfo` objects. Python returns raw dictionaries with the HTTP API field names.
</Note>

## Example: Parallel Exploration

Try multiple approaches from the same starting point:

```typescript
const sandbox = await Sandbox.create();
await sandbox.exec.run("git clone https://github.com/user/repo /app");
const cp = await sandbox.createCheckpoint("fresh-clone");

// Try three different migration strategies in parallel
const strategies = ["--strategy=ours", "--strategy=theirs", "--strategy=recursive"];
const results = await Promise.all(
  strategies.map(async (strategy) => {
    const fork = await Sandbox.createFromCheckpoint(cp.id);
    const result = await fork.exec.run(`cd /app && git merge origin/main ${strategy}`);
    await fork.kill();
    return { strategy, exitCode: result.exitCode };
  }),
);
```

<Tip>
  CLI equivalent: [`oc checkpoint`](/cli/checkpoint). Full reference: [TypeScript SDK](/reference/typescript-sdk#sandbox) · [Python SDK](/reference/python-sdk#sandbox) · [HTTP API](/reference/api#checkpoints).
</Tip>


---
title: Templates
url: "https://docs-v1.opencomputer.dev/sandboxes/templates.md"
description: Define sandbox environments programmatically with the Image builder and Snapshots
---

<Warning>Templates are an alpha, experimental feature. APIs, behavior, and manifest formats may change without notice.</Warning>

Templates provide a code-first approach to defining sandbox environments. Instead of configuring images manually, you define them programmatically using the SDK.

The system supports two workflows:

1. **Declarative images** — build images with varying dependencies *on demand* when creating sandboxes
2. **Pre-built snapshots** — create and register *ready-to-use* snapshots that can be shared across multiple sandboxes

## Declarative image building

Build images on-the-fly when creating sandboxes. Ideal for iterating quickly without creating separate snapshots.

Declarative images are **cached by content hash** — identical manifests produce the same image. Subsequent runs reuse the cached image instantly.

<Tabs items={["TypeScript", "Python", "curl"]}>
  <Tab title="TypeScript">
    ```typescript
    import { Sandbox } from '@opencomputer/sdk';
    import { Image } from '@opencomputer/sdk/node';

    // Define an image with packages, env vars, and files
    const image = Image.base()
      .aptInstall(['curl', 'jq'])
      .pipInstall(['requests', 'pandas'])
      .env({ PROJECT_ROOT: '/workspace' })
      .workdir('/workspace');

    // Create a sandbox — the image is built on first run, cached after
    const sandbox = await Sandbox.create({
      image,
      timeout: 300,
      onBuildLog: (log) => console.log(`build: ${log}`),
    });

    const result = await sandbox.exec.run('which curl');
    console.log(result.exitCode); // 0
    ```
  </Tab>

  <Tab title="Python">
    ```python
    from opencomputer import Sandbox, Image

    # Define an image with packages, env vars, and files
    image = (
        Image.base()
        .apt_install(["curl", "jq"])
        .pip_install(["requests", "pandas"])
        .env({"PROJECT_ROOT": "/workspace"})
        .workdir("/workspace")
    )

    # Create a sandbox — the image is built on first run, cached after
    sandbox = await Sandbox.create(
        image=image,
        timeout=300,
        on_build_log=lambda log: print(f"build: {log}"),
    )

    result = await sandbox.exec.run("which curl")
    print(result.exit_code)  # 0
    ```
  </Tab>

  <Tab title="curl">
    ```bash
    curl -X POST https://app.opencomputer.dev/api/sandboxes \
      -H "X-API-Key: $OPENCOMPUTER_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "image": {
          "steps": [
            {"type": "apt_install", "packages": ["curl", "jq"]},
            {"type": "pip_install", "packages": ["requests", "pandas"]},
            {"type": "env", "vars": {"PROJECT_ROOT": "/workspace"}},
            {"type": "workdir", "path": "/workspace"}
          ]
        },
        "timeout": 300
      }'
    ```
  </Tab>
</Tabs>

When you pass an `image` to `Sandbox.create()`, the server:

1. Hashes the image manifest to compute a **cache key**
2. If cached, creates the sandbox from the existing checkpoint instantly
3. If not cached, boots a build sandbox, executes each step, checkpoints the result, then creates your sandbox from it

### Build memory

Images build in a **4 GB** sandbox by default. If a build runs out of memory (heavy `apt`/`pip`/`npm`, compiling a large toolchain), raise the build-phase RAM with **`.builderMemory(mb)` / `.builder_memory(mb)`**.

This only affects the build. The resulting image is unchanged — you size the actual sandbox when you create it, via `memoryMB`:

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    // 8 GB to build…
    const image = Image.base()
      .aptInstall(['build-essential', 'cmake'])
      .runCommands('make -j')
      .builderMemory(8192);

    // …but the sandbox runs at whatever you ask for
    const sandbox = await Sandbox.create({ image, memoryMB: 4096 });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    image = (
        Image.base()
        .apt_install(["build-essential", "cmake"])
        .run_commands("make -j")
        .builder_memory(8192)
    )
    sandbox = await Sandbox.create(image=image)  # size via the HTTP API's memoryMB
    ```
  </Tab>
</Tabs>

`builderMemory` doesn't change the cache key — it's a build resource, not image content.

## Creating pre-built snapshots

Create named snapshots that persist permanently and can be shared across sandboxes. Snapshots are visible in the dashboard and don't need to be rebuilt.

<Tabs items={["TypeScript", "Python", "curl"]}>
  <Tab title="TypeScript">
    ```typescript
    import { Image, Snapshots } from '@opencomputer/sdk/node';

    const snapshots = new Snapshots();

    // Define the image
    const image = Image.base()
      .aptInstall(['python3-pip'])
      .pipInstall(['pandas', 'numpy', 'scikit-learn'])
      .workdir('/workspace');

    // Create a named snapshot with build log streaming
    await snapshots.create({
      name: 'data-science',
      image,
      onBuildLogs: (log) => console.log(`build: ${log}`),
    });

    // Now create sandboxes from the snapshot — instant, no build step
    const sandbox = await Sandbox.create({ snapshot: 'data-science' });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    from opencomputer import Image, Snapshots

    snapshots = Snapshots()

    # Define the image
    image = (
        Image.base()
        .apt_install(["python3-pip"])
        .pip_install(["pandas", "numpy", "scikit-learn"])
        .workdir("/workspace")
    )

    # Create a named snapshot with build log streaming
    await snapshots.create(
        name="data-science",
        image=image,
        on_build_logs=lambda log: print(f"build: {log}"),
    )

    # Now create sandboxes from the snapshot — instant, no build step
    sandbox = await Sandbox.create(snapshot="data-science")
    ```
  </Tab>

  <Tab title="curl">
    ```bash
    # Create a named snapshot
    curl -X POST https://app.opencomputer.dev/api/snapshots \
      -H "X-API-Key: $OPENCOMPUTER_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "data-science",
        "image": {
          "steps": [
            {"type": "apt_install", "packages": ["python3-pip"]},
            {"type": "pip_install", "packages": ["pandas", "numpy", "scikit-learn"]},
            {"type": "workdir", "path": "/workspace"}
          ]
        }
      }'

    # Create sandbox from the snapshot — instant
    curl -X POST https://app.opencomputer.dev/api/sandboxes \
      -H "X-API-Key: $OPENCOMPUTER_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"snapshot": "data-science"}'
    ```
  </Tab>
</Tabs>

### Sizing a fork's memory

A sandbox created from a snapshot inherits the snapshot's memory by default. Pass `memoryMB` to give it more RAM — useful when one branch needs more headroom than the base.

<Tabs items={["TypeScript", "Python", "curl"]}>
  <Tab title="TypeScript">
    ```typescript
    // Fork the snapshot with 8 GB instead of its captured size
    const sandbox = await Sandbox.create({ snapshot: 'data-science', memoryMB: 8192 });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    # Fork the snapshot with 8 GB instead of its captured size
    sandbox = await Sandbox.create(snapshot="data-science", memory_mb=8192)
    ```
  </Tab>

  <Tab title="curl">
    ```bash
    curl -X POST https://app.opencomputer.dev/api/sandboxes \
      -H "X-API-Key: $OPENCOMPUTER_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"snapshot": "data-science", "memoryMB": 8192}'
    ```
  </Tab>
</Tabs>

`memoryMB` is clamped to a valid range, and the response's `memoryMB` reports the effective value:

* **Floor — the snapshot's own memory.** A smaller request is ignored; a fork can't start smaller than the snapshot it restores. A 4 GB snapshot forked with `memoryMB: 1024` still boots at \~4 GB.
* **Ceiling.** Larger requests are capped to the maximum platform tier.

The same `memoryMB` field works when [forking from a checkpoint](/api-reference/checkpoints/fork).

### Managing snapshots

<Tabs items={["TypeScript", "Python", "curl"]}>
  <Tab title="TypeScript">
    ```typescript
    const snapshots = new Snapshots();

    // List all snapshots
    const list = await snapshots.list();
    for (const s of list) {
      console.log(`${s.name} — ${s.status}`);
    }

    // Get a specific snapshot
    const snapshot = await snapshots.get('data-science');
    console.log(snapshot.status); // "ready"

    // Delete a snapshot
    await snapshots.delete('data-science');
    ```
  </Tab>

  <Tab title="Python">
    ```python
    snapshots = Snapshots()

    # List all snapshots
    snapshot_list = await snapshots.list()
    for s in snapshot_list:
        print(f"{s['name']} — {s['status']}")

    # Get a specific snapshot
    snapshot = await snapshots.get("data-science")
    print(snapshot["status"])  # "ready"

    # Delete a snapshot
    await snapshots.delete("data-science")
    ```
  </Tab>

  <Tab title="curl">
    ```bash
    # List all snapshots
    curl https://app.opencomputer.dev/api/snapshots \
      -H "X-API-Key: $OPENCOMPUTER_API_KEY"

    # Get a specific snapshot
    curl https://app.opencomputer.dev/api/snapshots/data-science \
      -H "X-API-Key: $OPENCOMPUTER_API_KEY"

    # Delete a snapshot
    curl -X DELETE https://app.opencomputer.dev/api/snapshots/data-science \
      -H "X-API-Key: $OPENCOMPUTER_API_KEY"
    ```
  </Tab>
</Tabs>

***

## Default template

Sandboxes use the default template when no image or snapshot is specified. It includes:

* **Ubuntu 22.04**
* **Python 3** with pip, venv, setuptools
* **Node.js 20 LTS** with npm
* **Build tools:** build-essential, cmake, pkg-config
* **CLI tools:** git, git-lfs, curl, wget, jq, rsync, htop, tree
* **Editors:** nano, vim-tiny
* **Database:** sqlite3
* **Networking:** openssh-client, iproute2, net-tools, dnsutils
* **Claude Agent SDK** and claude-code (pre-installed for agent sessions)

***

## Image configuration

The `Image` class provides a fluent, immutable API for defining sandbox environments. Each method returns a **new** `Image` instance — the original is never modified.

| Method                                                            | Description                                                                  |
| ----------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `Image.base()`                                                    | Start from the default OpenSandbox environment                               |
| `.aptInstall(packages)` / `.apt_install(packages)`                | Install system packages via apt-get                                          |
| `.pipInstall(packages)` / `.pip_install(packages)`                | Install Python packages via pip                                              |
| `.runCommands(...cmds)` / `.run_commands(*cmds)`                  | Run shell commands during build                                              |
| `.env(vars)`                                                      | Set environment variables (written to `/etc/environment`)                    |
| `.workdir(path)`                                                  | Set default working directory                                                |
| `.addFile(path, content)` / `.add_file(path, content)`            | Embed a file with inline content                                             |
| `.addLocalFile(local, remote)` / `.add_local_file(local, remote)` | Read a local file into the image                                             |
| `.addLocalDir(local, remote)` / `.add_local_dir(local, remote)`   | Read a local directory into the image                                        |
| `.builderMemory(mb)` / `.builder_memory(mb)`                      | RAM for the build phase (default 4 GB; doesn't affect the resulting sandbox) |
| `.toJSON()` / `.to_dict()`                                        | Return the image manifest                                                    |
| `.cacheKey()` / `.cache_key()`                                    | Compute SHA-256 content hash                                                 |

## SnapshotInfo

| Field          | Type   | Description                            |
| -------------- | ------ | -------------------------------------- |
| `id`           | string | Unique snapshot identifier             |
| `name`         | string | Snapshot name                          |
| `status`       | string | `"building"`, `"ready"`, or `"failed"` |
| `contentHash`  | string | SHA-256 hash of the image manifest     |
| `checkpointId` | string | Linked checkpoint ID                   |
| `manifest`     | object | The declarative image manifest         |
| `createdAt`    | string | ISO 8601 creation timestamp            |
| `lastUsedAt`   | string | ISO 8601 last usage timestamp          |

<Tip>
  Full SDK reference: [TypeScript SDK](/sdks/typescript/templates) · [Python SDK](/sdks/python/templates) · [HTTP API](/reference/api#snapshots).
</Tip>


---
title: Secrets
url: "https://docs-v1.opencomputer.dev/sandboxes/secrets.md"
description: Inject encrypted secrets into sandboxes without exposing them to the VM
---

Secrets let you pass API keys, tokens, and credentials into sandboxes without the real values ever entering the VM. They are encrypted at rest, sealed into opaque tokens at boot, and only revealed by a host-side proxy on outbound HTTPS requests.

## How it works

<Mermaid
  chart="sequenceDiagram
    participant You as Your Code
    participant S as Server
    participant W as Worker
    participant VM as Sandbox VM
    participant P as MITM Proxy
    participant API as External API

    You->>S: Store secret (sk-ant-...)
    Note over S: Encrypted with AES-256-GCM<br/>in Postgres

    You->>S: Create sandbox with secret store
    S->>S: Decrypt secrets
    S->>W: Send decrypted secrets

    W->>W: Seal into opaque tokens
    Note over W: sk-ant-... → osb_sealed_7f3a9c...

    W->>VM: Inject sealed env vars
    Note over VM: $API_KEY = osb_sealed_7f3a9c...<br/>Real secret NEVER in VM memory

    VM->>P: HTTPS request with sealed token
    P->>P: Replace sealed token with real secret
    P->>API: Request with real API key
    API-->>VM: Response"
/>

<Info>
  The real secret value only exists in the host-side proxy's memory — it is never written to disk, never sent to the VM, and never visible via `env` or `/proc` inside the sandbox.
</Info>

## Quick start

Create a secret store, add a secret, and launch a sandbox that uses it:

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    import { Sandbox, SecretStore } from '@opencomputer/sdk';

    // 1. Create a secret store with egress restrictions
    const store = await SecretStore.create({
      name: 'my-agent-secrets',
      egressAllowlist: ['api.anthropic.com'],
    });

    // 2. Add an encrypted secret
    await SecretStore.setSecret(store.id, 'ANTHROPIC_API_KEY', 'sk-ant-...');

    // 3. Create a sandbox — secrets are injected as sealed tokens
    const sandbox = await Sandbox.create({
      secretStore: 'my-agent-secrets',
      timeout: 600,
    });

    // Inside the VM, the env var is sealed — not the real key
    const result = await sandbox.exec.run('echo $ANTHROPIC_API_KEY');
    console.log(result.stdout); // "osb_sealed_7f3a9c..."

    // But HTTPS requests to allowed hosts get the real value via the proxy
    const apiResult = await sandbox.exec.run(`
      curl -s https://api.anthropic.com/v1/messages \\
        -H "x-api-key: $ANTHROPIC_API_KEY" \\
        -H "anthropic-version: 2023-06-01" \\
        -H "content-type: application/json" \\
        -d '{"model":"claude-haiku-4-5-20251001","max_tokens":10,"messages":[{"role":"user","content":"hi"}]}'
    `);
    console.log(apiResult.stdout); // 200 OK — real key was substituted by the proxy
    ```
  </Tab>

  <Tab title="Python">
    ```python
    from opencomputer import Sandbox, SecretStore

    # 1. Create a secret store with egress restrictions
    store = await SecretStore.create(
        name='my-agent-secrets',
        egress_allowlist=['api.anthropic.com'],
    )

    # 2. Add an encrypted secret
    await SecretStore.set_secret(store['id'], 'ANTHROPIC_API_KEY', 'sk-ant-...')

    # 3. Create a sandbox — secrets are injected as sealed tokens
    sandbox = await Sandbox.create(
        secret_store='my-agent-secrets',
        timeout=600,
    )

    # Inside the VM, the env var is sealed — not the real key
    result = await sandbox.exec.run('echo $ANTHROPIC_API_KEY')
    print(result.stdout)  # "osb_sealed_7f3a9c..."

    # But HTTPS requests to allowed hosts get the real value via the proxy
    api_result = await sandbox.exec.run(
        'curl -s https://api.anthropic.com/v1/messages '
        '-H "x-api-key: $ANTHROPIC_API_KEY" '
        '-H "anthropic-version: 2023-06-01" '
        '-H "content-type: application/json" '
        '-d \'{"model":"claude-haiku-4-5-20251001","max_tokens":10,"messages":[{"role":"user","content":"hi"}]}\''
    )
    print(api_result.stdout)  # 200 OK — real key was substituted by the proxy
    ```
  </Tab>
</Tabs>

## Per-secret host restrictions

You can restrict individual secrets so they are only substituted in requests to specific hosts. This prevents a compromised dependency from exfiltrating secrets to an attacker-controlled server.

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    // This secret will only be substituted in requests to api.anthropic.com
    await SecretStore.setSecret(store.id, 'ANTHROPIC_API_KEY', 'sk-ant-...', {
      allowedHosts: ['api.anthropic.com'],
    });

    // This secret works on any allowed egress host
    await SecretStore.setSecret(store.id, 'GENERIC_TOKEN', 'tok-...');
    ```
  </Tab>

  <Tab title="Python">
    ```python
    # This secret will only be substituted in requests to api.anthropic.com
    await SecretStore.set_secret(
        store['id'], 'ANTHROPIC_API_KEY', 'sk-ant-...',
        allowed_hosts=['api.anthropic.com'],
    )

    # This secret works on any allowed egress host
    await SecretStore.set_secret(store['id'], 'GENERIC_TOKEN', 'tok-...')
    ```
  </Tab>
</Tabs>

## Egress allowlists

Secret stores can restrict which hosts the sandbox can make HTTPS requests to. Requests to hosts not on the list are blocked by the proxy.

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    const store = await SecretStore.create({
      name: 'restricted-store',
      egressAllowlist: ['api.anthropic.com', '*.openai.com'],
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    store = await SecretStore.create(
        name='restricted-store',
        egress_allowlist=['api.anthropic.com', '*.openai.com'],
    )
    ```
  </Tab>
</Tabs>

Supports exact matches (`api.anthropic.com`) and wildcards (`*.openai.com`). An empty allowlist means all hosts are allowed.

## Managing secrets

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    // List all secret stores
    const stores = await SecretStore.list();

    // List secrets in a store (metadata only — values are never returned)
    const entries = await SecretStore.listSecrets(store.id);

    // Delete a secret
    await SecretStore.deleteSecret(store.id, 'OLD_KEY');

    // Delete a store and all its secrets
    await SecretStore.delete(store.id);
    ```
  </Tab>

  <Tab title="Python">
    ```python
    # List all secret stores
    stores = await SecretStore.list()

    # List secrets in a store (metadata only — values are never returned)
    entries = await SecretStore.list_secrets(store['id'])

    # Delete a secret
    await SecretStore.delete_secret(store['id'], 'OLD_KEY')

    # Delete a store and all its secrets
    await SecretStore.delete(store['id'])
    ```
  </Tab>
</Tabs>

## Secrets with snapshots and checkpoints

You can attach a secret store when creating a sandbox from a snapshot or checkpoint, even if the original didn't have one. This is useful for baking a base environment (e.g., installed dependencies) and then giving each fork its own scoped credentials.

### Snapshot template with secrets

Pre-build a snapshot once, then create sandboxes from it with different credentials:

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    import { Sandbox, Snapshots, Image } from '@opencomputer/sdk/node';

    // Pre-build a reusable snapshot
    const snapshots = new Snapshots();
    await snapshots.create({
      name: 'data-pipeline',
      image: Image.base().aptInstall(['python3-pip']).pipInstall(['pandas']),
    });

    // Create sandboxes from the snapshot, each with their own credentials
    const worker1 = await Sandbox.create({
      snapshot: 'data-pipeline',
      secretStore: 'worker-1-keys',
    });
    const worker2 = await Sandbox.create({
      snapshot: 'data-pipeline',
      secretStore: 'worker-2-keys',
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    from opencomputer import Sandbox, Snapshots, Image

    # Pre-build a reusable snapshot
    snapshots = Snapshots()
    await snapshots.create(
        name='data-pipeline',
        image=Image.base().apt_install(['python3-pip']).pip_install(['pandas']),
    )

    # Create sandboxes from the snapshot, each with their own credentials
    worker1 = await Sandbox.create(
        snapshot='data-pipeline',
        secret_store='worker-1-keys',
    )
    worker2 = await Sandbox.create(
        snapshot='data-pipeline',
        secret_store='worker-2-keys',
    )
    ```
  </Tab>
</Tabs>

### Checkpoint fork with secrets

Layer a new secret store on top of an existing checkpoint's store:

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    // Bake a base snapshot with git credentials
    const base = await Sandbox.create({ secretStore: 'git-creds' });
    await base.exec.run('git clone https://github.com/org/repo /app');
    const cp = await base.createCheckpoint('repo-cloned');

    // Fork with sandbox-specific API credentials (layered on top of git-creds)
    const worker1 = await Sandbox.createFromCheckpoint(cp.id, {
      secretStore: 'worker-1-keys',
    });
    const worker2 = await Sandbox.createFromCheckpoint(cp.id, {
      secretStore: 'worker-2-keys',
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    # Bake a base snapshot with git credentials
    base = await Sandbox.create(secret_store='git-creds')
    await base.exec.run('git clone https://github.com/org/repo /app')
    cp = await base.create_checkpoint('repo-cloned')

    # Fork with sandbox-specific API credentials (layered on top of git-creds)
    worker1 = await Sandbox.create_from_checkpoint(cp['id'],
        secret_store='worker-1-keys',
    )
    worker2 = await Sandbox.create_from_checkpoint(cp['id'],
        secret_store='worker-2-keys',
    )
    ```
  </Tab>
</Tabs>

### Layering rules

When a checkpoint already has a secret store and you attach another at fork time, the stores are **merged**:

* **Secrets**: Both stores' secrets are available. On name collision, the fork's store wins.
* **Egress allowlists**: Aggregated (union of both stores' lists).
* **Per-secret host restrictions**: Follow the winning secret's store.

This means a base snapshot can provide broad credentials (e.g., git access) while each fork adds its own scoped credentials (e.g., limited API keys).

On a fork-of-fork, the checkpoint's merged result becomes the new base — there's no unbounded chain of stores to resolve.

## Security properties

| Property                  | Detail                                                               |
| ------------------------- | -------------------------------------------------------------------- |
| **Encryption at rest**    | AES-256-GCM in Postgres, key via `OPENSANDBOX_SECRET_ENCRYPTION_KEY` |
| **Never in VM memory**    | Env vars contain opaque `osb_sealed_*` tokens                        |
| **Host-side only**        | Real values exist only in the MITM proxy process on the worker host  |
| **Egress control**        | Allowlists restrict which domains receive secrets                    |
| **Per-secret scoping**    | Individual secrets can be locked to specific hosts                   |
| **Values never returned** | The API only returns secret names and metadata, never values         |

## Next steps

* [CLI reference](/cli/secrets) — manage secret stores and secrets from the command line
* [TypeScript SDK reference](/reference/typescript-sdk#secret-stores) — full API reference
* [Python SDK reference](/reference/python-sdk#secret-stores) — full API reference


---
title: Patches
url: "https://docs-v1.opencomputer.dev/sandboxes/patches.md"
description: Scripts that run when forking from a checkpoint
---

A patch is a shell script attached to a checkpoint. Every time a sandbox is forked from that checkpoint, patches run automatically — inject configuration, update dependencies, or customize state without modifying the checkpoint itself.

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    import { Sandbox } from "@opencomputer/sdk";

    // Attach a patch to an existing checkpoint
    const result = await Sandbox.createCheckpointPatch(checkpointId, {
      script: 'echo "export API_KEY=$1" >> /root/.bashrc',
      description: "Inject API key at fork time",
    });
    console.log(result.patch.id, result.patch.sequence);

    // Fork — patch runs automatically
    const sandbox = await Sandbox.createFromCheckpoint(checkpointId);
    ```
  </Tab>

  <Tab title="Python">
    ```python
    from opencomputer import Sandbox

    result = await Sandbox.create_checkpoint_patch(
        checkpoint_id,
        script='echo "export API_KEY=$1" >> /root/.bashrc',
        description="Inject API key at fork time",
    )

    sandbox = await Sandbox.create_from_checkpoint(checkpoint_id)
    ```
  </Tab>
</Tabs>

## API Reference

### Create Patch

Patch methods are **static** on the Sandbox class — they operate on checkpoints, not sandbox instances.

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    const result = await Sandbox.createCheckpointPatch(checkpointId, {
      script: "apt-get update && apt-get install -y redis-server",
      description: "Install Redis",
    });
    // result.patch.id, result.patch.sequence
    ```
  </Tab>

  <Tab title="Python">
    ```python
    result = await Sandbox.create_checkpoint_patch(
        checkpoint_id,
        script="apt-get update && apt-get install -y redis-server",
        description="Install Redis",
    )
    ```
  </Tab>
</Tabs>

| Parameter      | Type   | Required | Description                    |
| -------------- | ------ | -------- | ------------------------------ |
| `checkpointId` | string | Yes      | Target checkpoint              |
| `script`       | string | Yes      | Bash script to execute on fork |
| `description`  | string | No       | Human-readable description     |

### List Patches

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    const patches = await Sandbox.listCheckpointPatches(checkpointId);
    for (const p of patches) {
      console.log(p.sequence, p.description);
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python
    patches = await Sandbox.list_checkpoint_patches(checkpoint_id)
    ```
  </Tab>
</Tabs>

### Delete Patch

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    await Sandbox.deleteCheckpointPatch(checkpointId, patchId);
    ```
  </Tab>

  <Tab title="Python">
    ```python
    await Sandbox.delete_checkpoint_patch(checkpoint_id, patch_id)
    ```
  </Tab>
</Tabs>

## When Patches Run

Patches execute when a sandbox is **forked** from the checkpoint (via `Sandbox.createFromCheckpoint()` or `oc checkpoint spawn`). The strategy is always `on_wake`.

## Execution Order

Patches run in creation order (by `sequence` number). If you create three patches, they execute as patch 1 → patch 2 → patch 3 every time a sandbox is forked.

## PatchInfo

| Field          | Type   | Description                |
| -------------- | ------ | -------------------------- |
| `id`           | string | Patch UUID                 |
| `checkpointId` | string | Parent checkpoint          |
| `script`       | string | Bash script content        |
| `description`  | string | Human-readable description |
| `strategy`     | string | Always `"on_wake"`         |
| `sequence`     | number | Execution order            |
| `createdAt`    | string | Timestamp                  |

## Example: Environment-Specific Forks

Use patches to create different environments from one checkpoint:

```typescript
// Base checkpoint has the app installed
const cpId = "cp-abc123";

// Patch for staging
await Sandbox.createCheckpointPatch(cpId, {
  script: `
    echo 'DATABASE_URL=postgres://staging-db/app' >> /app/.env
    echo 'LOG_LEVEL=debug' >> /app/.env
  `,
  description: "Staging environment config",
});

// Every fork now gets staging config
const staging = await Sandbox.createFromCheckpoint(cpId);
```

<Tip>
  CLI equivalent: [`oc patch`](/cli/patch). Full reference: [TypeScript SDK](/reference/typescript-sdk#sandbox) · [Python SDK](/reference/python-sdk#sandbox) · [HTTP API](/reference/api#checkpoint-patches).
</Tip>


---
title: Preview URLs
url: "https://docs-v1.opencomputer.dev/sandboxes/preview-urls.md"
description: Expose sandbox ports to the internet
---

A preview URL is a public HTTPS endpoint that routes traffic to a port inside your sandbox. Start a web server and access it immediately via the sandbox's domain.

## Quick Access

Every sandbox has a built-in `domain` property that gives you instant access to port 80 without creating a preview URL explicitly:

```typescript
const sandbox = await Sandbox.create();
await sandbox.exec.run("python3 -m http.server 80 &");

console.log(sandbox.domain);
// → sb-abc123-p80.workers.opencomputer.dev

// For other ports:
console.log(sandbox.getPreviewDomain(3000));
// → sb-abc123-p3000.workers.opencomputer.dev
```

The `domain` and `getPreviewDomain(port)` properties construct the preview hostname directly — no API call needed. Traffic is routed through the control plane proxy to the correct sandbox.

## Authentication

Preview URLs are public by default — anyone who can guess or learn the hostname can hit your sandbox's port. To require a bearer token in front of every request, opt in at create time with `previewAuth`.

When enabled, the control plane validates an `Authorization: Bearer <token>` (or `X-OC-Preview-Token: <token>`) header on every preview-URL request. Missing or wrong → 401. The check happens before traffic ever reaches your sandbox, so a brute-force run also can't wake a hibernated sandbox.

### Enable at create time

<Tabs items={["TypeScript", "Python", "CLI"]}>
  <Tab title="TypeScript">
    ```typescript
    const sandbox = await Sandbox.create({
      previewAuth: { scheme: "bearer", token: "auto" },
    });

    // previewAuthToken is returned exactly once — store it somewhere durable.
    console.log(sandbox.previewAuthToken);
    // → "qx2sSi5IYXWBvnnRqwK9Ky_cIAI-x0Vx1bPCt0XMxsI"
    ```
  </Tab>

  <Tab title="Python">
    ```python
    sandbox = await Sandbox.create(
        preview_auth={"scheme": "bearer", "token": "auto"},
    )

    # preview_auth_token is returned exactly once — store it somewhere durable.
    print(sandbox.preview_auth_token)
    # → "qx2sSi5IYXWBvnnRqwK9Ky_cIAI-x0Vx1bPCt0XMxsI"
    ```
  </Tab>

  <Tab title="CLI">
    ```bash
    oc sandbox create --preview-auth
    # Created sandbox sb-abc123 (status: running)
    # Preview auth token (shown once): qx2sSi5IYXWBvnnRqwK9Ky_cIAI-x0Vx1bPCt0XMxsI
    ```
  </Tab>
</Tabs>

`token: "auto"` (or omitting the field) tells the server to generate a 256-bit random token. To bring your own — useful when your gateway already has a secret it can share with both ends — pass an explicit string of at least 16 characters:

<Tabs items={["TypeScript", "Python", "CLI"]}>
  <Tab title="TypeScript">
    ```typescript
    await Sandbox.create({
      previewAuth: { scheme: "bearer", token: process.env.GATEWAY_TOKEN! },
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    await Sandbox.create(
        preview_auth={"scheme": "bearer", "token": os.environ["GATEWAY_TOKEN"]},
    )
    ```
  </Tab>

  <Tab title="CLI">
    ```bash
    oc sandbox create --preview-auth-token "$GATEWAY_TOKEN"
    ```
  </Tab>
</Tabs>

### Making authenticated requests

Send the token on every request to the preview URL. Two header forms are accepted; pick whichever fits your client:

```bash
curl -H "Authorization: Bearer $TOKEN" https://sb-abc123-p3000.workers.opencomputer.dev/
curl -H "X-OC-Preview-Token: $TOKEN"   https://sb-abc123-p3000.workers.opencomputer.dev/
```

The token gates **every port** on the sandbox — there's one token per sandbox, shared across all preview URLs you create on it.

### Rotation

When you need to roll the token — exposure, scheduled rotation, employee offboarding — call `rotate`. The old token stops working immediately and the new plaintext is returned exactly once:

<Tabs items={["TypeScript", "Python", "CLI"]}>
  <Tab title="TypeScript">
    ```typescript
    const newToken = await sandbox.rotatePreviewAuthToken();
    // sandbox.previewAuthToken is also updated in place.
    ```
  </Tab>

  <Tab title="Python">
    ```python
    new_token = await sandbox.rotate_preview_auth_token()
    # sandbox.preview_auth_token is also updated in place.
    ```
  </Tab>

  <Tab title="CLI">
    ```bash
    oc preview rotate-auth sb-abc123
    # New preview auth token (shown once): <new token>
    ```
  </Tab>
</Tabs>

There is no zero-downtime dual-token mode in v1 — roll out the new token to your caller before discarding the old one, or expect a brief window of 401s during the swap.

### What's stored and what's not

* Only the **SHA-256 hash** of the token is persisted. The server never sees the plaintext after the create or rotate response returns.
* There is no GET endpoint that returns the token. If you lose it, your only options are to rotate (mint a new one) or recreate the sandbox.
* A sandbox created **without** `previewAuth` has open preview URLs — exactly the same as before this feature existed. The feature is fully opt-in and backwards compatible.

### Where the check happens

Authentication is enforced by the cell's control plane immediately before the request is proxied to the worker. That means:

* **Hibernated sandboxes** stay hibernated on missing/wrong tokens — bad tokens can't burn wake capacity.
* **Self-hosted deployments** without the Cloudflare edge get the same gate for free — the check follows the sandbox, not the front door.

## Explicit Preview URLs

For tracking, custom domains, or auth configuration, use the preview URL API:

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    import { Sandbox } from "@opencomputer/sdk";

    const sandbox = await Sandbox.create();
    await sandbox.exec.run("npx create-next-app@latest /app --yes", { timeout: 120 });
    sandbox.exec.start("npm run dev -- --port 3000", { cwd: "/app" });

    const preview = await sandbox.createPreviewURL({ port: 3000 });
    console.log(preview.hostname); // sb-abc123-p3000.workers.opencomputer.dev
    ```
  </Tab>

  <Tab title="Python">
    ```python
    from opencomputer import Sandbox

    sandbox = await Sandbox.create()
    await sandbox.exec.run("npx create-next-app@latest /app --yes", timeout=120)
    await sandbox.exec.start("npm run dev -- --port 3000", cwd="/app")

    preview = await sandbox.create_preview_url(port=3000)
    print(preview["hostname"])
    ```
  </Tab>
</Tabs>

## API Reference

### Create Preview URL

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    const preview = await sandbox.createPreviewURL({
      port: 3000,
      domain: "preview.myapp.com", // optional custom domain
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    preview = await sandbox.create_preview_url(
        port=3000,
        domain="preview.myapp.com",
    )
    ```
  </Tab>
</Tabs>

| Parameter    | Type   | Required | Description                        |
| ------------ | ------ | -------- | ---------------------------------- |
| `port`       | number | Yes      | Container port to expose (1–65535) |
| `domain`     | string | No       | Custom domain                      |
| `authConfig` | object | No       | Authentication configuration       |

### List Preview URLs

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    const previews = await sandbox.listPreviewURLs();
    for (const p of previews) {
      console.log(`Port ${p.port}: https://${p.hostname}`);
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python
    previews = await sandbox.list_preview_urls()
    for p in previews:
        print(f"Port {p['port']}: https://{p['hostname']}")
    ```
  </Tab>
</Tabs>

### Delete Preview URL

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    await sandbox.deletePreviewURL(3000);
    ```
  </Tab>

  <Tab title="Python">
    ```python
    await sandbox.delete_preview_url(3000)
    ```
  </Tab>
</Tabs>

## Custom Domains

Pass a `domain` when creating a preview URL. SSL is provisioned automatically via Cloudflare. Custom domain verification is configured at the organization level through the dashboard — not through the SDK.

## Persistence

Preview URLs persist across hibernation and wake cycles. If you hibernate a sandbox and wake it later, the preview URLs are still active.

## Multiple Ports

Expose multiple services from the same sandbox:

```typescript
// Frontend
await sandbox.createPreviewURL({ port: 3000 });
// API server
await sandbox.createPreviewURL({ port: 8080 });
// Database admin
await sandbox.createPreviewURL({ port: 5432 });
```

<Tip>
  CLI equivalent: [`oc preview`](/cli/preview). Full reference: [TypeScript SDK](/reference/typescript-sdk#sandbox) · [Python SDK](/reference/python-sdk#sandbox) · [HTTP API](/reference/api#preview-urls).
</Tip>


---
title: Webhooks
url: "https://docs-v1.opencomputer.dev/sandboxes/webhooks.md"
description: "Subscribe to sandbox lifecycle events — signed, retried, and redeliverable"
---

<Note>**Preview** — sandbox webhooks are newly available and still evolving; the shape may change. We'd love your feedback.</Note>

Register a **destination** and OpenComputer delivers a sandbox's lifecycle events to it as a small, signed **delivery envelope** — signed, retried, and redeliverable. Use it to react when a sandbox boots, hibernates, stops, or scales, without polling.

The envelope is camelCase, with top-level routing fields and the full event nested under `event`:

```json
{
  "type": "sandbox.stopped",
  "sandboxId": "sb-3f9a…",
  "eventId": "sb-3f9a…:3:142",
  "event": {
    "id": "sb-3f9a…:3:142", "ts": "2026-06-24T12:00:00Z",
    "orgId": "…", "sandboxId": "sb-3f9a…",
    "type": "sandbox.stopped", "data": { "reason": "user_requested" }
  }
}
```

Dedupe on **`deliveryId`** — `verifyWebhook` sets it from the `svix-id` header (stable across retries and redelivery). Need to route on your own metadata? Attach `metadata` when you register the destination and it rides as **custom HTTP headers** on every request — there's no metadata in the body.

## Events

Subscribe to all events, or filter with `eventTypes` (exact like `sandbox.stopped`, or a prefix like `sandbox.*`).

| Event                         | When                                            | `event.data`                                                 |
| ----------------------------- | ----------------------------------------------- | ------------------------------------------------------------ |
| `sandbox.created`             | the sandbox resource is accepted                | `template`                                                   |
| `sandbox.ready`               | the VM has booted and can run commands          | —                                                            |
| `sandbox.hibernated`          | the sandbox was paused (disk persisted)         | —                                                            |
| `sandbox.resumed`             | a hibernated sandbox woke                       | —                                                            |
| `sandbox.stopped`             | the sandbox stopped                             | `reason`: `user_requested` \| `expired` \| `crash`           |
| `sandbox.migrated`            | the sandbox moved to new infrastructure         | —                                                            |
| `sandbox.checkpoint.created`  | a checkpoint was taken                          | `checkpointId`                                               |
| `sandbox.forked`              | a sandbox was forked from a checkpoint          | `parentId`                                                   |
| `sandbox.scaled`              | the sandbox's resources changed                 | `cpuCount`, `memoryMB`                                       |
| `sandbox.preview_url.changed` | a preview-URL port mapping was added or removed | `port`, `url` (`url` is `null` when the mapping was removed) |

## Destinations

A destination is **org-scoped** by default (all your sandboxes), or pinned to one sandbox with `sandboxId`. Set `enabled: false` to **pause** a destination — it stops receiving deliveries until you re-enable it; events that occur while it's paused aren't backfilled.

<Tabs items={["TypeScript SDK", "REST API"]}>
  <Tab title="TypeScript SDK">
    ```ts
    import { Webhooks } from "@opencomputer/sdk";

    const webhooks = new Webhooks({ apiKey: process.env.OPENCOMPUTER_API_KEY! });

    const { id, secret } = await webhooks.create({
      url: "https://app.example.com/oc-webhook",
      eventTypes: ["sandbox.stopped", "sandbox.ready"],
      name: "prod",            // optional display name
    });
    // `secret` (whsec_…) verifies deliveries — returned here and re-fetchable via webhooks.getSecret(id).
    ```
  </Tab>

  <Tab title="REST API">
    ```http
    POST https://app.opencomputer.dev/api/webhooks
    X-API-Key: $OPENCOMPUTER_API_KEY
    Content-Type: application/json

    {
      "url": "https://app.example.com/oc-webhook",
      "eventTypes": ["sandbox.stopped", "sandbox.ready"],
      "name": "prod"
    }
    ```
  </Tab>
</Tabs>

Every destination is **signed**. If you don't pass a `secret`, OpenComputer generates one (`whsec_…`) and returns it on create. The secret is re-fetchable any time via `webhooks.getSecret(id)` (`GET /api/webhooks/:id/secret`), and rotatable via `webhooks.update(id, { rotateSecret: true })`.

<Tabs>
  <Tab title="TypeScript SDK">
    | Call                          | Purpose                                                                         |
    | ----------------------------- | ------------------------------------------------------------------------------- |
    | `webhooks.create(params)`     | Register a new destination (returns the `secret`).                              |
    | `webhooks.list()`             | List destinations.                                                              |
    | `webhooks.get(id)`            | Fetch one (`hasSecret`, not the secret value).                                  |
    | `webhooks.getSecret(id)`      | Fetch the current signing secret (`whsec_…`).                                   |
    | `webhooks.update(id, params)` | Pause/resume (`enabled`), retune `eventTypes`, change `url`, or `rotateSecret`. |
    | `webhooks.delete(id)`         | Delete — removes the endpoint; deleted history isn't queryable.                 |
    | `webhooks.test(id)`           | Enqueue a sample event (delivered asynchronously).                              |
  </Tab>

  <Tab title="REST API">
    | Method · Path                  | Purpose                                                             |
    | ------------------------------ | ------------------------------------------------------------------- |
    | `POST /api/webhooks`           | Register a new destination.                                         |
    | `GET /api/webhooks`            | List destinations.                                                  |
    | `GET /api/webhooks/:id`        | Fetch one.                                                          |
    | `GET /api/webhooks/:id/secret` | Fetch the current signing secret.                                   |
    | `PATCH /api/webhooks/:id`      | Pause/resume, retune `eventTypes`, change `url`, or `rotateSecret`. |
    | `DELETE /api/webhooks/:id`     | Delete (history not queryable afterward).                           |
    | `POST /api/webhooks/:id/test`  | Enqueue a sample event (delivered asynchronously).                  |
  </Tab>
</Tabs>

### Subscribe at create

`sandbox.created` and `sandbox.ready` fire before a separate `webhooks.create` call could know the sandbox id. To capture a sandbox's **full** lifecycle, register the webhook **with** the sandbox — it's pinned to that sandbox and gets every event from `created` on:

<Tabs items={["TypeScript SDK", "REST API"]}>
  <Tab title="TypeScript SDK">
    ```ts
    import { Sandbox } from "@opencomputer/sdk";

    const sandbox = await Sandbox.create({
      webhooks: [{ url: "https://app.example.com/oc-webhook" }],
    });
    // the generated signing secret is on the create response: sandbox.webhooks[0].secret
    ```
  </Tab>

  <Tab title="REST API">
    ```http
    POST https://app.opencomputer.dev/api/sandboxes
    X-API-Key: $OPENCOMPUTER_API_KEY
    Content-Type: application/json

    { "template": "base", "webhooks": [{ "url": "https://app.example.com/oc-webhook" }] }
    ```
  </Tab>
</Tabs>

## Deliveries

Every send and its outcome — the data behind a deliveries dashboard.

<Tabs>
  <Tab title="TypeScript SDK">
    | Call                                           | Purpose                                   |
    | ---------------------------------------------- | ----------------------------------------- |
    | `webhooks.deliveries.list(id)`                 | List recent delivery attempts (up to 50). |
    | `webhooks.deliveries.get(id, messageId)`       | One delivered message, in detail.         |
    | `webhooks.deliveries.redeliver(id, messageId)` | Re-send a message (same `svix-id`).       |
  </Tab>

  <Tab title="REST API">
    | Method · Path                                            | Purpose                           |
    | -------------------------------------------------------- | --------------------------------- |
    | `GET /api/webhooks/:id/deliveries`                       | List recent attempts (up to 50).  |
    | `GET /api/webhooks/:id/deliveries/:messageId`            | One delivered message, in detail. |
    | `POST /api/webhooks/:id/deliveries/:messageId/redeliver` | Re-send a message.                |
  </Tab>
</Tabs>

Each record is a Svix delivery attempt with its `status` and the consumer's `responseStatusCode`; Svix retries failed attempts on its managed schedule and retains recent attempt history (the attempt index lags a send by a few seconds). A deleted destination's history is no longer queryable.

```
Delivery = { id, status: "success" | "pending" | "failed", responseStatusCode?, timestamp }
```

Redelivery re-sends the **same** message to the endpoint (same `svix-id`) — for when the original never landed. A receiver that dedupes on `svix-id` treats it as the same message.

## Signing, retries, and dedupe

* **HTTPS only**, checked at registration. The delivery layer (Svix) additionally blocks private, loopback, link-local, and cloud-metadata addresses at send time.
* **Always signed** with [Standard Webhooks](https://www.standardwebhooks.com) (delivery is by [Svix](https://www.svix.com)). Every request carries `svix-id`, `svix-timestamp`, and `svix-signature` = `v1,<base64(HMAC-SHA256(secret, "{svix-id}.{svix-timestamp}.{rawBody}"))>`. Verify against the **raw request body, before parsing it**; the timestamp guards replay. Any custom headers you attached as destination `metadata` ride along too. Your signing `secret` is re-fetchable any time via `GET /api/webhooks/{id}/secret` and rotatable via update (`rotateSecret: true`).
* **At-least-once.** Once an event is handed off it's retried until it succeeds or the delivery layer gives up. A duplicate can still arrive, so **dedupe on `svix-id`** (the verifier exposes it as both `deliveryId` and `dedupeId`).
* **Not strictly ordered.** Events for one sandbox can arrive out of order (e.g. `sandbox.ready` before `sandbox.created`). Treat each event independently and key off `event.type`; don't assume an ordering.
* **Success / retry / give-up.** A delivery succeeds on HTTP `2xx`. Failures are retried on Svix's managed backoff (immediately, then 5s, 5m, 30m, 2h, 5h, 10h, 10h); an endpoint that keeps failing for \~5 days is auto-disabled. Inspect attempts and recover failed messages via the [deliveries API](#deliveries). Unsafe targets (private / loopback / cloud-metadata addresses) are blocked by the delivery layer.

### Verify a webhook

The SDK ships a verifier — no extra dependency, runs in Node / Cloudflare Workers / browsers — that checks the signature and returns the parsed envelope. It's the **same `verifyWebhook`** used for [session webhooks](/agent-sessions/webhooks); pass `SandboxLifecycleEvent` to type the nested event:

```ts
import { verifyWebhook, type SandboxLifecycleEvent } from "@opencomputer/sdk";

// rawBody = the RAW request body string, read BEFORE JSON.parse.
// Throws on a missing / expired / invalid signature.
const delivery = await verifyWebhook<SandboxLifecycleEvent>(rawBody, request.headers, process.env.OC_WEBHOOK_SECRET!);

if (delivery.type === "sandbox.stopped") {
  console.log(delivery.sandboxId, delivery.event.data.reason); // event.data is typed per event
}
```

Or verify with any [Standard Webhooks](https://www.standardwebhooks.com) / Svix library directly, against the raw body and the `svix-id` / `svix-timestamp` / `svix-signature` headers.

<Note>Webhooks here cover **sandbox lifecycle**. To deliver an agent's *session* events (turns, messages, results), see [Agent Sessions webhooks](/agent-sessions/webhooks) — verified with the same `verifyWebhook` helper. Session webhooks differ in a couple of specifics today: their `webhook-id` is the event id, and signing is opt-in (set a `secret`).</Note>


---
title: Elasticity
url: "https://docs-v1.opencomputer.dev/sandboxes/elasticity.md"
description: "Dynamically scale sandbox memory and CPU — automatically, manually, or via the in-VM API"
---

A sandbox's memory and CPU can be resized at runtime. The platform's recommended path is **Autoscaling** — opt the sandbox in once and we resize it for you based on observed memory pressure. Lower-level controls are also available if you want to drive sizing yourself or freeze it.

CPU scales proportionally to memory in all modes (1 vCPU per \~4 GB up to 16 GB / 4 vCPU). The permissible memory tiers are:

| Memory | vCPU            |
| ------ | --------------- |
| 1 GB   | 1 (best-effort) |
| 4 GB   | 1               |
| 8 GB   | 2               |
| 16 GB  | 4               |

If you want a sandbox to **stay at a fixed size**, see [Locking Resources](#locking-resources).

## Autoscaling

Opt a sandbox into the per-sandbox autoscaler and the platform resizes it for you based on observed memory pressure. Autoscale is **opt-in per sandbox** and **disabled by default**.

This is the recommended way to size a sandbox: turn it on once with bounds you're comfortable with, then leave it. Manual `scale` and the in-VM API are escape hatches for cases where the autoscaler isn't a good fit (e.g. you want a hard guarantee for a benchmark, or you're scripting your own logic).

### Enable

<Tabs items={["TypeScript", "Python", "CLI", "HTTP"]}>
  <Tab title="TypeScript">
    ```typescript
    await sandbox.setAutoscale({
      enabled: true,
      minMemoryMB: 1024,
      maxMemoryMB: 16384,
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    await sandbox.set_autoscale(
        enabled=True,
        min_memory_mb=1024,
        max_memory_mb=16384,
    )
    ```
  </Tab>

  <Tab title="CLI">
    ```bash
    oc sandbox autoscale sb-abc123 --on --min 1024 --max 16384
    ```
  </Tab>

  <Tab title="HTTP">
    ```bash
    curl -sf -X PUT "$API/api/sandboxes/$SANDBOX_ID/autoscale" \
      -H "X-API-Key: $API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"enabled": true, "minMemoryMB": 1024, "maxMemoryMB": 16384}'
    ```
  </Tab>
</Tabs>

`minMemoryMB` and `maxMemoryMB` must be values from the tier table above. The autoscaler keeps the sandbox between these bounds, in tier steps.

### Behavior

* **Scale up**: when the sandbox's 1-minute average memory utilization exceeds 75 %, jump to the next tier. 60-second cooldown between up-events.
* **Scale down**: when **all** of the 1-min, 5-min, and 15-min utilization averages stay below 25 %, drop one tier. 5-minute cooldown between down-events. Down requires at least 15 minutes of low-utilization data, so a brief idle pause won't shrink you.
* **Manual override**: any explicit `scale` call disables autoscale on that sandbox. Re-enable explicitly via `setAutoscale({ enabled: true, ... })` (or the equivalent in any SDK) if you want it back.

### Disable

<Tabs items={["TypeScript", "Python", "CLI", "HTTP"]}>
  <Tab title="TypeScript">
    ```typescript
    await sandbox.setAutoscale({ enabled: false });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    await sandbox.set_autoscale(enabled=False)
    ```
  </Tab>

  <Tab title="CLI">
    ```bash
    oc sandbox autoscale sb-abc123 --off
    ```
  </Tab>

  <Tab title="HTTP">
    ```bash
    curl -sf -X PUT "$API/api/sandboxes/$SANDBOX_ID/autoscale" \
      -H "X-API-Key: $API_KEY" \
      -d '{"enabled": false}'
    ```
  </Tab>
</Tabs>

### Inspect

<Tabs items={["TypeScript", "Python", "CLI", "HTTP"]}>
  <Tab title="TypeScript">
    ```typescript
    const cfg = await sandbox.getAutoscale();
    // { sandboxID: "sb-...", enabled: true, minMemoryMB: 1024, maxMemoryMB: 16384 }
    ```
  </Tab>

  <Tab title="Python">
    ```python
    cfg = await sandbox.get_autoscale()
    # {"sandboxID": "sb-...", "enabled": True, "minMemoryMB": 1024, "maxMemoryMB": 16384}
    ```
  </Tab>

  <Tab title="CLI">
    ```bash
    oc sandbox autoscale sb-abc123
    # Autoscale enabled for sb-abc123 (1024–16384 MB)
    ```
  </Tab>

  <Tab title="HTTP">
    ```bash
    curl -sf "$API/api/sandboxes/$SANDBOX_ID/autoscale" -H "X-API-Key: $API_KEY"
    # {"sandboxID":"sb-...","enabled":true,"minMemoryMB":1024,"maxMemoryMB":16384}
    ```
  </Tab>
</Tabs>

## Manual Scaling (Control Plane)

When you want to resize once — predictable size for a benchmark, a one-off scale-up before a known-heavy task, an operator response to an alert — call `scale` from your application or operator tooling:

<Tabs items={["TypeScript", "Python", "CLI", "HTTP"]}>
  <Tab title="TypeScript">
    ```typescript
    await sandbox.scale({ memoryMB: 4096 });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    await sandbox.scale(memory_mb=4096)
    ```
  </Tab>

  <Tab title="CLI">
    ```bash
    oc sandbox scale sb-abc123 4096
    ```
  </Tab>

  <Tab title="HTTP">
    ```bash
    curl -sf -X POST "$API/api/sandboxes/$SANDBOX_ID/scale" \
      -H "X-API-Key: $API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"memoryMB": 4096}'
    ```
  </Tab>
</Tabs>

A manual scale **disables autoscale** on the sandbox — explicit user intent overrides the loop. Re-enable it later if you want size to track load again.

The endpoint can return:

* **403 `scaling_locked`** — the sandbox is locked. See [Locking Resources](#locking-resources).
* **409 `oom_floor`** — the requested size would force a guest OOM-kill because the current working set exceeds it. Free memory inside the guest, then retry.
* **402 Payment Required** — the requested size exceeds your plan cap.

## In-VM Scaling

Sandboxes expose an internal metadata API at `http://169.254.169.254` that lets code **inside** the VM scale itself. Useful for workload-aware scripts where the workload knows its own demand better than the platform autoscaler can infer (e.g. "scale up before this build, back down after").

<Note>
  This endpoint is only reachable from inside the sandbox. It is not exposed through the control plane API or SDKs.
</Note>

### Scale Memory

Send a `POST` to `/v1/scale` with the desired memory in MB. CPU is adjusted proportionally.

```bash
curl -s -X POST http://169.254.169.254/v1/scale \
  -H "Content-Type: application/json" \
  -d '{"memoryMB": 4096}'
```

### Check Current Limits

```bash
curl -s http://169.254.169.254/v1/limits
```

### Example: Rust Compilation

Rust builds are memory-hungry. You can alias `cargo` to scale up before compilation and scale back down after:

```bash
alias cargo='_cargo_scaled'
_cargo_scaled() {
  # Scale up to 16GB / 4 vCPU before build
  curl -sf -X POST http://169.254.169.254/v1/scale \
    -H "Content-Type: application/json" -d '{"memoryMB": 16384}'

  # Run the actual cargo command
  command cargo "$@"
  local exit_code=$?

  # Scale back down to 4GB / 1 vCPU
  curl -sf -X POST http://169.254.169.254/v1/scale \
    -H "Content-Type: application/json" -d '{"memoryMB": 4096}'

  return $exit_code
}
```

Add this to your sandbox's `~/.bashrc` or inject it via `sandbox.exec.run` so every `cargo build`, `cargo test`, etc. automatically gets the extra resources.

### Example: Custom Scaling Loop

A simple shell script that monitors memory pressure and scales automatically. Useful when you want fine-grained control inside the sandbox itself; for the platform-managed equivalent see [Autoscaling](#autoscaling) above.

```bash
#!/bin/sh
SCALE_API="http://169.254.169.254/v1/scale"
MIN_MB=1024
MAX_MB=8192
SCALE_UP_THRESHOLD=80
SCALE_DOWN_THRESHOLD=30
COOLDOWN=30
last_scale=0

while true; do
  mem_total=$(awk '/MemTotal/{print $2}' /proc/meminfo)
  mem_avail=$(awk '/MemAvailable/{print $2}' /proc/meminfo)
  usage_pct=$(( (mem_total - mem_avail) * 100 / mem_total ))
  total_mb=$((mem_total / 1024))
  now=$(date +%s)
  elapsed=$((now - last_scale))

  if [ $usage_pct -gt $SCALE_UP_THRESHOLD ] && [ $elapsed -gt $COOLDOWN ]; then
    new_mb=$((total_mb * 2))
    [ $new_mb -gt $MAX_MB ] && new_mb=$MAX_MB
    if [ $new_mb -gt $total_mb ]; then
      echo "usage=$usage_pct% -> scaling UP to ${new_mb}MB"
      curl -sf -X POST "$SCALE_API" \
        -H "Content-Type: application/json" \
        -d "{\"memoryMB\":$new_mb}"
      last_scale=$now
    fi
  elif [ $usage_pct -lt $SCALE_DOWN_THRESHOLD ] && [ $total_mb -gt $MIN_MB ] && [ $elapsed -gt $COOLDOWN ]; then
    new_mb=$((total_mb / 2))
    [ $new_mb -lt $MIN_MB ] && new_mb=$MIN_MB
    if [ $new_mb -lt $total_mb ]; then
      echo "usage=$usage_pct% -> scaling DOWN to ${new_mb}MB"
      curl -sf -X POST "$SCALE_API" \
        -H "Content-Type: application/json" \
        -d "{\"memoryMB\":$new_mb}"
      last_scale=$now
    fi
  fi

  sleep 5
done
```

## Locking Resources

If you want a sandbox to **stay at a fixed size** — predictable billing, a benchmark, a long-running pinned workload — lock it. Locking blocks both manual scaling and autoscaling on the sandbox.

<Tabs items={["TypeScript", "Python", "CLI", "HTTP"]}>
  <Tab title="TypeScript">
    ```typescript
    await sandbox.setScalingLock(true);
    ```
  </Tab>

  <Tab title="Python">
    ```python
    await sandbox.set_scaling_lock(True)
    ```
  </Tab>

  <Tab title="CLI">
    ```bash
    oc sandbox lock sb-abc123
    ```
  </Tab>

  <Tab title="HTTP">
    ```bash
    curl -sf -X PUT "$API/api/sandboxes/$SANDBOX_ID/scaling-lock" \
      -H "X-API-Key: $API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"locked": true}'
    ```
  </Tab>
</Tabs>

While locked:

* `scale` returns **403** `scaling_locked`
* `setAutoscale({ enabled: true })` returns **403** `scaling_locked`
* The autoscaler skips the sandbox

Locking automatically disables autoscale on the sandbox, so you have a single user-facing toggle. Unlocking does **not** auto-re-enable autoscale; turn it back on explicitly if you want it.

<Tabs items={["TypeScript", "Python", "CLI", "HTTP"]}>
  <Tab title="TypeScript">
    ```typescript
    // Unlock
    await sandbox.setScalingLock(false);

    // Inspect
    const lock = await sandbox.getScalingLock();
    // { sandboxID: "sb-...", locked: false }
    ```
  </Tab>

  <Tab title="Python">
    ```python
    # Unlock
    await sandbox.set_scaling_lock(False)

    # Inspect
    lock = await sandbox.get_scaling_lock()
    # {"sandboxID": "sb-...", "locked": False}
    ```
  </Tab>

  <Tab title="CLI">
    ```bash
    oc sandbox unlock sb-abc123
    oc sandbox lock-status sb-abc123
    ```
  </Tab>

  <Tab title="HTTP">
    ```bash
    # Unlock
    curl -sf -X PUT "$API/api/sandboxes/$SANDBOX_ID/scaling-lock" \
      -H "X-API-Key: $API_KEY" \
      -d '{"locked": false}'

    # Inspect
    curl -sf "$API/api/sandboxes/$SANDBOX_ID/scaling-lock" -H "X-API-Key: $API_KEY"
    # {"sandboxID":"sb-...","locked":false}
    ```
  </Tab>
</Tabs>


---
title: Burst Sandboxes
url: "https://docs-v1.opencomputer.dev/sandboxes/burst-sandboxes.md"
description: Alpha lower-cost sandboxes that preserve disk across infrastructure restarts
---

<Warning>
  Burst Sandboxes are in alpha. They preserve filesystem state across
  infrastructure restarts, but running processes, in-memory state, terminal
  sessions, and open network connections may restart.
</Warning>

Burst Sandboxes are lower-cost OpenComputer sandboxes designed for workloads that can recover from a process restart. They expose the same OpenComputer API for commands, files, PTY, preview URLs, images, snapshots, scaling, and lifecycle operations.

If OpenComputer receives advance infrastructure interruption notice, the sandbox gets up to **25 seconds** to flush state before it restarts on healthy capacity. The sandbox's filesystem is preserved and made available after resume.

## Pricing

Burst Sandboxes are priced roughly **2x cheaper than on-demand sandboxes**.

## Create a Burst Sandbox

Set `burst: true` when creating the sandbox.

<Tabs items={["TypeScript", "Python", "HTTP"]}>
  <Tab title="TypeScript">
    ```typescript
    import { Sandbox } from "@opencomputer/sdk";

    const sandbox = await Sandbox.create({
      burst: true,
      memoryMB: 4096,
      timeout: 300,
    });

    const result = await sandbox.exec.run("echo hello from burst");
    console.log(result.stdout);

    await sandbox.kill();
    ```
  </Tab>

  <Tab title="Python">
    ```python
    from opencomputer import Sandbox

    async with await Sandbox.create(
        burst=True,
        memory_mb=4096,
        timeout=300,
    ) as sandbox:
        result = await sandbox.exec.run("echo hello from burst")
        print(result.stdout)
    ```
  </Tab>

  <Tab title="HTTP">
    ```bash
    curl -X POST https://app.opencomputer.dev/api/sandboxes \
      -H "X-API-Key: $OPENCOMPUTER_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "burst": true,
        "memoryMB": 4096,
        "timeout": 300
      }'
    ```
  </Tab>
</Tabs>

## What Happens on Interruption

Infrastructure capacity can be reclaimed or restarted by the cloud provider. When OpenComputer receives advance notice, it notifies the sandbox and starts the burst restart flow.

The sandbox receives these environment variables:

* `OPENSANDBOX_RESUMABLE=true`
* `OPENSANDBOX_RESUME_NOTICE_SECONDS=25`

During the notice window, write important state to disk. After resume, restart your process from durable state.

## Good Fits

Burst Sandboxes work well for cost-sensitive workloads that can restart from disk:

* Batch code execution where failed jobs can be requeued.
* CI-style checks, test runners, linters, and formatters.
* AI agent tool calls with durable task state outside the sandbox.
* Parallel exploration jobs where one attempt can restart.
* Development, previews, and experiments where cost matters more than availability.

## Poor Fits

Use on-demand sandboxes when process continuity is required:

* User-facing interactive sessions where a rare disconnect is still disruptive.
* Long-running stateful services that keep important state only in RAM.
* Databases, queues, or coordinators running inside the sandbox.
* Workloads with strict deadlines and no retry budget.
* Any task that cannot safely restart after a partial command failure.

## Alpha Limitations

During alpha, Burst Sandboxes have these restrictions:

| Capability          | Burst alpha behavior                                             |
| ------------------- | ---------------------------------------------------------------- |
| Process state       | May restart                                                      |
| Filesystem          | Preserved across burst restarts                                  |
| Notice window       | Up to 25 seconds when advance notice is available                |
| Sudden host failure | May resume without advance notice                                |
| Capacity            | Best effort; create may fail or wait when burst capacity is full |

## Reliability Pattern

For agent or batch systems, run your work from a small process wrapper and install a restart-notice hook.

1. Store task state on disk or outside the sandbox.
2. Write outputs to durable storage as the task progresses.
3. Install `/home/sandbox/.opencomputer/on-restart-notice` to flush or checkpoint before restart.
4. Make your startup command resume from the last durable state.
5. Use on-demand sandboxes for workloads with no process restart budget.

Example setup:

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    import { Sandbox } from "@opencomputer/sdk";

    const sandbox = await Sandbox.create({
      burst: true,
      timeout: 0,
    });

    await sandbox.exec.run("mkdir -p /home/sandbox/.opencomputer /home/sandbox/app");

    await sandbox.files.write(
      "/home/sandbox/.opencomputer/on-restart-notice",
      `#!/bin/sh
    set -eu
    echo "restart notice: ${1:-25}s" >> /home/sandbox/app/events.log
    touch /home/sandbox/app/restarting
    sync
    `,
    );

    await sandbox.exec.run("chmod +x /home/sandbox/.opencomputer/on-restart-notice");

    await sandbox.files.write(
      "/home/sandbox/app/worker.sh",
      `#!/bin/sh
    set -eu
    cd /home/sandbox/app
    if [ -f restarting ]; then
      echo "resumed after restart" >> events.log
      rm -f restarting
    else
      echo "started" >> events.log
    fi

    i=$(cat counter 2>/dev/null || echo 0)
    while true; do
      i=$((i + 1))
      echo "$i" > counter
      sync
      sleep 5
    done
    `,
    );

    await sandbox.exec.run("chmod +x /home/sandbox/app/worker.sh");
    await sandbox.exec.background("/home/sandbox/app/worker.sh", {
      maxRunAfterDisconnect: 0,
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    from opencomputer import Sandbox

    sandbox = await Sandbox.create(burst=True, timeout=0)

    await sandbox.exec.run("mkdir -p /home/sandbox/.opencomputer /home/sandbox/app")

    await sandbox.files.write(
        "/home/sandbox/.opencomputer/on-restart-notice",
        """#!/bin/sh
    set -eu
    echo "restart notice: ${1:-25}s" >> /home/sandbox/app/events.log
    touch /home/sandbox/app/restarting
    sync
    """,
    )

    await sandbox.exec.run("chmod +x /home/sandbox/.opencomputer/on-restart-notice")

    await sandbox.files.write(
        "/home/sandbox/app/worker.sh",
        """#!/bin/sh
    set -eu
    cd /home/sandbox/app
    if [ -f restarting ]; then
      echo "resumed after restart" >> events.log
      rm -f restarting
    else
      echo "started" >> events.log
    fi

    i=$(cat counter 2>/dev/null || echo 0)
    while true; do
      i=$((i + 1))
      echo "$i" > counter
      sync
      sleep 5
    done
    """,
    )

    await sandbox.exec.run("chmod +x /home/sandbox/app/worker.sh")
    await sandbox.exec.background(
        "/home/sandbox/app/worker.sh",
        max_run_after_disconnect=0,
    )
    ```
  </Tab>
</Tabs>

When OpenComputer receives notice, it runs the hook with the notice window in seconds. If the sandbox resumes on another worker, start your process again and read the state you wrote under `/home/sandbox/app`.

## Related

<CardGroup cols={2}>
  <Card title="Elasticity" href="/sandboxes/elasticity">
    Resource tiers and autoscaling
  </Card>

  <Card title="Checkpoints" href="/sandboxes/checkpoints">
    Save and resume sandbox state
  </Card>
</CardGroup>


---
title: Usage Monitoring
url: "https://docs-v1.opencomputer.dev/sandboxes/usage.md"
description: "Inspect a sandbox's memory allocation and actual utilization over time"
---

[`GET /api/sandboxes/:id/usage`](/api-reference/usage/get-sandbox-usage)
returns a per-minute time series of a sandbox's memory — both the
tier you provisioned and what the process actually consumed —
together with envelope totals. Use it to answer "am I over-paying for
this sandbox" or "when did memory spike?" For cross-sandbox
leaderboards (rank by usage, group by tag) see the aggregator at
[`GET /api/usage`](/api-reference/usage/get-usage) instead.

<Note>
  **Preview:** Usage and tag APIs are new. Endpoints, response fields,
  and SDK method names may change before GA; temporary inaccuracies or
  rough edges are possible while the surface settles.
</Note>

## Example

Allocated tier as a step line (it changes only on resize), measured
usage as a smooth curve. The gap between them is paid-for headroom.

<Image src="/images/usage-chart.svg" alt="Example chart: a sandbox's allocated and used memory over 24 hours, showing a mid-day resize event, a load-test peak, and a brief stop" intrinsicWidth="800" intrinsicHeight="380" placeholder="" />

Everything on the chart — the resize step, the peak, the brief gap —
falls out of `points[]` directly: `allocatedMemoryMb` is the step,
`usedMemoryMbPeak` finds the peak, `uptimeSeconds: 0` marks downtime.

## Request

```http
GET /api/sandboxes/sb-abc/usage?from=2026-05-27T00:00:00Z&to=2026-05-27T01:00:00Z
```

Path carries the sandbox ID. `from` and `to` are optional and accept
ISO dates (`YYYY-MM-DD`, interpreted as UTC midnight) or RFC3339
timestamps; omit them for the last hour. Max window is **30 days**.
Full param reference: the
[API page](/api-reference/usage/get-sandbox-usage).

## Response

```json
{
  "sandboxId": "sb-abc",
  "alias": "my-agent",
  "from": "2026-05-27T00:00:00Z",
  "to":   "2026-05-27T01:00:00Z",
  "totals": {
    "memoryAllocatedGbSeconds": 3600,
    "memoryUsedGbSeconds":      2180,
    "uptimeSeconds":            3600,
    "memoryAllocatedPeakMb":    1024,
    "memoryUsedPeakMb":          742
  },
  "points": [
    { "ts": "2026-05-27T00:00:00Z", "memoryAllocatedGbSeconds": 60, "memoryUsedGbSeconds": 35.86, "uptimeSeconds": 60, "allocatedMemoryMb": 1024, "usedMemoryMbAvg": 612, "usedMemoryMbPeak": 720 },
    { "ts": "2026-05-27T00:01:00Z", "memoryAllocatedGbSeconds": 60, "memoryUsedGbSeconds": 40.02, "uptimeSeconds": 60, "allocatedMemoryMb": 1024, "usedMemoryMbAvg": 683, "usedMemoryMbPeak": 742 }
  ]
}
```

`totals` is the server-side sum of `points[]` — summing any
additive field across the array reproduces the matching `totals.*`
exactly. Full field semantics live in the
[API reference](/api-reference/usage/get-sandbox-usage).

## Allocated vs used

* **`memoryAllocatedGbSeconds`** — the tier you provisioned, integrated
  over time. Same physical quantity the bill is computed from.
  Resize mid-window shows up as a time-weighted blend in the affected
  bucket.
* **`memoryUsedGbSeconds`** — measured resident memory, sampled every
  60s and integrated. The headroom signal: low ratio of used over
  allocated means you're over-provisioned.

Both are in **GiB-seconds** (binary, 2<sup>30</sup> bytes/GiB), so
`used / allocated` gives a clean utilization fraction.

## Calling it

<Tabs items={["TypeScript", "cURL"]}>
  <Tab title="TypeScript">
    ```typescript
    import { Usage } from "@opencomputer/sdk";

    const usage = new Usage(
      "https://app.opencomputer.dev",
      process.env.OPENCOMPUTER_API_KEY!,
    );

    // Default window = last 1 hour. Pass `from`/`to` for up to 30 days.
    const r = await usage.forSandbox("sb-abc");
    const pct = 100 * r.totals.memoryUsedGbSeconds / r.totals.memoryAllocatedGbSeconds;
    console.log(`${pct.toFixed(0)}% utilization`);

    for (const p of r.points) {
      // p.ts, p.usedMemoryMbAvg, p.allocatedMemoryMb — ready to chart
    }
    ```
  </Tab>

  <Tab title="cURL">
    ```bash
    curl -s "$OPENCOMPUTER_API_URL/api/sandboxes/sb-abc/usage?from=2026-05-27T00:00:00Z&to=2026-05-27T01:00:00Z" \
      -H "X-API-Key: $OPENCOMPUTER_API_KEY" | jq '.totals'
    ```
  </Tab>
</Tabs>

Full TypeScript SDK reference: [Usage & Tags](/reference/typescript-sdk/usage).

## Bucket semantics

* Buckets are **1 minute, UTC-aligned**; boundary buckets clamp to
  the original window so a mid-minute `from` doesn't over-report.
* Minutes where the sandbox isn't running emit zero-points — gaps
  read as continuous flat zero on a chart, not nulls.
* If a measurement is missed, `uptimeSeconds` still reads 60 (the
  sandbox was running) but `usedMemoryMbAvg` reads 0 — visible as a
  brief dip under an otherwise continuous allocated line.
* The first non-zero `usedMemoryMbAvg` for a freshly-created sandbox
  appears a few minutes in. Workloads that finish faster than that
  may not register any measured usage even though they ran.
* Window max is **30 days**.


---
title: Browser Sessions
url: "https://docs-v1.opencomputer.dev/browser-sessions/overview.md"
description: "Preview: create cloud browser sessions for Playwright, Magnitude, and profile-backed web automation"
---

<Warning>
  Browser Sessions are currently in invite-only preview. Access is enabled for
  approved organizations, and API routes, SDK method names, response fields,
  limits, and pricing may change before general availability.
</Warning>

Browser Sessions create managed Chromium sessions for web agents and browser
automation. OpenComputer handles API-key auth and org ownership, then returns
browser connection URLs for tools such as Playwright and Magnitude.

If your organization has not been invited to the preview, Browser Session API
calls may fail even when your OpenComputer API key is valid.

Use Browser Sessions when you want a cloud browser without installing Chromium
inside an OpenComputer sandbox. Use [sandboxes](/sandboxes/overview) when you
need a full Linux VM with your own browser runtime and filesystem.

## Create a Browser

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    import { Browser } from "@opencomputer/sdk";

    const browser = await Browser.create({
      headless: false,
      stealth: true,
      startUrl: "https://example.com",
      timeoutSeconds: 120,
    });

    console.log(browser.id);
    console.log(browser.cdpWsUrl);
    console.log(browser.liveViewUrl);

    await browser.delete();
    ```
  </Tab>

  <Tab title="Python">
    ```python
    from opencomputer import Browser

    browser = await Browser.create(
        headless=False,
        stealth=True,
        start_url="https://example.com",
        timeout_seconds=120,
    )

    print(browser.id)
    print(browser.cdp_ws_url)
    print(browser.live_view_url)

    await browser.delete()
    ```
  </Tab>
</Tabs>

Headful browsers return a live-view URL. Headless browsers are lighter, but do
not provide a live view. Telemetry is enabled by default for all browser
sessions. Set `telemetry: false` / `telemetry=False` to disable it, or pass a
Kernel telemetry configuration object directly.

Replay recording is disabled by default. Set `recording: true` /
`recording=True` to enable replay recording for a headful browser session.

## Profile Auth Checks

Saved profiles can start an asynchronous auth check for a site. The SDK returns
a run immediately; `wait()` polls with short requests until the run completes.

```typescript TypeScript
const profile = await BrowserProfile.connect("linkedin-profile");

const run = await profile.checkAuth({
  homepage: "https://www.linkedin.com/feed/",
  user: "motatoes",
  mode: "vision",
  compareFresh: true,
});

const result = await run.wait();
console.log(result.status, result.result);
```

Auth checks are read-only. They create temporary browser sessions with profile
saving disabled and compare the saved profile against a fresh browser when
`compareFresh` is enabled.

## Playwright

The browser response includes a CDP WebSocket URL. Pass it directly to
Playwright's `connectOverCDP`.

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    import { Browser } from "@opencomputer/sdk";
    import { chromium } from "playwright";

    const ocBrowser = await Browser.create({
      headless: false,
      startUrl: "https://example.com",
    });

    try {
      const browser = await chromium.connectOverCDP(ocBrowser.cdpWsUrl);
      const context = browser.contexts()[0] || await browser.newContext();
      const page = context.pages()[0] || await context.newPage();

      await page.goto("https://example.com");
      console.log(await page.title());

      await browser.close();
    } finally {
      await ocBrowser.delete();
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python
    from opencomputer import Browser
    from playwright.async_api import async_playwright

    oc_browser = await Browser.create(
        headless=False,
        start_url="https://example.com",
    )

    try:
        async with async_playwright() as p:
            browser = await p.chromium.connect_over_cdp(oc_browser.cdp_ws_url)
            context = browser.contexts[0] if browser.contexts else await browser.new_context()
            page = context.pages[0] if context.pages else await context.new_page()

            await page.goto("https://example.com")
            print(await page.title())

            await browser.close()
    finally:
        await oc_browser.delete()
    ```
  </Tab>
</Tabs>

## Magnitude

Magnitude can use a Playwright-connected browser. Create the OpenComputer
browser first, connect over CDP, then hand the page to your Magnitude agent.

```typescript TypeScript
import { Browser } from "@opencomputer/sdk";
import { chromium } from "playwright";
import { startBrowserAgent } from "magnitude-core";

const ocBrowser = await Browser.create({
  headless: false,
  stealth: true,
  startUrl: "https://example.com",
});

try {
  const browser = await chromium.connectOverCDP(ocBrowser.cdpWsUrl);
  const context = browser.contexts()[0] || await browser.newContext();
  const page = context.pages()[0] || await context.newPage();

  const agent = await startBrowserAgent({ page });
  await agent.act("Find the page title and summarize what this site is for.");

  await browser.close();
} finally {
  await ocBrowser.delete();
}
```

<Note>
  Magnitude package names and constructors can vary by version. The
  OpenComputer-specific step is stable: create a `Browser`, connect to
  `cdpWsUrl` with Playwright, then pass the resulting page or browser object to
  Magnitude.
</Note>

## Save and Load Profiles

Profiles persist browser state such as cookies and local storage across browser
sessions. Profiles are scoped to your OpenComputer org; another org cannot
list, load, or delete your profiles.

Create a profile once:

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    import { BrowserProfile } from "@opencomputer/sdk";

    const profile = await BrowserProfile.create({
      name: "github-login",
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    from opencomputer import BrowserProfile

    profile = await BrowserProfile.create(name="github-login")
    ```
  </Tab>
</Tabs>

Use the profile when creating a browser:

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    import { Browser } from "@opencomputer/sdk";

    const browser = await Browser.create({
      profile: {
        id: profile.id,
        saveChanges: true,
      },
      headless: false,
    });

    // Log in or update session state, then delete the browser.
    // With saveChanges enabled, changes are saved back to the profile.
    await browser.delete();
    ```
  </Tab>

  <Tab title="Python">
    ```python
    from opencomputer import Browser

    browser = await Browser.create(
        profile={
            "id": profile.id,
            "save_changes": True,
        },
        headless=False,
    )

    # Log in or update session state, then delete the browser.
    # With save_changes enabled, changes are saved back to the profile.
    await browser.delete()
    ```
  </Tab>
</Tabs>

Load the same profile later by ID or name:

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    import { Browser, BrowserProfile } from "@opencomputer/sdk";

    const profile = await BrowserProfile.connect("github-login");

    const browser = await Browser.create({
      profile: {
        id: profile.id,
        saveChanges: true,
      },
      headless: false,
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    from opencomputer import Browser, BrowserProfile

    profile = await BrowserProfile.connect("github-login")

    browser = await Browser.create(
        profile={
            "id": profile.id,
            "save_changes": True,
        },
        headless=False,
    )
    ```
  </Tab>
</Tabs>

## Authentication

The SDK uses your OpenComputer API key:

```bash
export OPENCOMPUTER_API_KEY="osb_..."
```

<Note>
  For local development against a non-production Browser Sessions endpoint, set
  `OPENCOMPUTER_BROWSER_API_URL`.
</Note>


---
title: Reserved Capacity
url: "https://docs-v1.opencomputer.dev/reserved-capacity/overview.md"
description: Commit memory capacity to future 15-minute intervals at a discounted rate
---

Reserved capacity lets you pre-commit to memory you'll use later. OpenComputer
plans for it instead of holding margin against uncertain demand; you get a
lower effective rate on the committed usage. Pure on-demand still works for
everything else.

## Reservations are non-refundable

Once you commit to capacity for an interval, that's a permanent commitment.
There is no cancellation, no transfer, no partial refund. The commitment is
the whole reason the rate is lower — any carve-out re-introduces the demand
risk reserved capacity exists to remove.

The hedge is to **book conservatively**: reserve only what you're confident
you'll run, and let on-demand absorb everything else.

## The unit is 1 GB-hour

Reservations are made against 15-minute UTC intervals on the quarter-hour
grid: `:00`, `:15`, `:30`, `:45`. Within each interval, the minimum
commitment is **4 GB for 15 minutes — that is, 1 GB-hour.** All
reservations must be multiples of 4 GB per interval; anything smaller
isn't a meaningful commitment unit and the API rejects it.

The 15-minute time grain is deliberate. Reserve the slots you'll actually
run in — you aren't pushed into a long contiguous block and asked to pay
for the troughs in between. A nightly batch reserves the eight intervals
it occupies; a weekday workload reserves working hours and skips evenings
and weekends. The reserved rate applies whatever shape your demand takes.

You don't book individual blocks or get block IDs back. You just tell the
API "for this interval, add this many GB to my reserved capacity," and the
calendar shows you the new total.

## A small example

Reserve 16 GB for the 02:00–02:15 UTC slot tomorrow:

```bash curl
curl -X POST https://app.opencomputer.dev/api/capacity/reservations \
  -H "X-API-Key: $OPENCOMPUTER_API_KEY" \
  -H "Idempotency-Key: demo-1" \
  -H "Content-Type: application/json" \
  -d '{
    "intervals": [
      {
        "startsAt": "2026-04-29T02:00:00Z",
        "endsAt":   "2026-04-29T02:15:00Z",
        "capacityGb": 16
      }
    ]
  }'
```

Response:

```json
{
  "reservationId": "9f67b8f7-7b91-4d2d-b1cb-19d0d0a14562",
  "createdAt": "2026-04-28T18:00:05Z",
  "intervals": [
    {
      "startsAt": "2026-04-29T02:00:00Z",
      "endsAt":   "2026-04-29T02:15:00Z",
      "capacityGb": 16
    }
  ]
}
```

That's it. Run sandboxes normally during 02:00–02:15; they'll be allocated
against your reserved capacity automatically. Anything over 16 GB during
that interval falls back to on-demand rates.

## Why reserve instead of running on-demand?

|              | On-demand (instant)               | Reserved (pre-booked)                                      |
| ------------ | --------------------------------- | ---------------------------------------------------------- |
| Rate         | $0.060/GB-hour                    | $0.012/GB-hour                                             |
| Commitment   | None                              | Full — you pay regardless of usage                         |
| Availability | Subject to platform headroom      | Guaranteed for the interval you booked                     |
| Best for     | Unpredictable or bursty workloads | Nightly batch jobs, scheduled runs, known traffic patterns |

If you already know you'll run 48 GB of agents at 3am every night, reserve
the day before, get the lower rate, get the guarantee. If your workload is
"whatever users ask for," stay on-demand. And because the granularity is
15 minutes, you can be precise — reserve the peaks, leave the troughs to
on-demand, and don't commit to capacity outside your actual workload
window.

## Where to go next

<CardGroup cols={2}>
  <Card title="Concepts" icon="shapes" href="/reserved-capacity/concepts">
    The vocabulary: intervals, reservations, the reservation log.
  </Card>

  <Card title="Reading the calendar" icon="calendar" href="/reserved-capacity/calendar">
    Plan reservations before you commit.
  </Card>

  <Card title="Reserving capacity" icon="lock" href="/reserved-capacity/reserving">
    The write flow, idempotency, atomicity.
  </Card>

  <Card title="Usage and overage" icon="gauge" href="/reserved-capacity/usage-and-overage">
    How reserved capacity is consumed and how overage is billed.
  </Card>
</CardGroup>

<Tip>
  Full endpoint reference: [`/api/capacity/*`](/api-reference/capacity/get-calendar).
</Tip>

<Note>
  **Pricing.** Pre-booked capacity bills at **$0.012/GB-hour**; instant
  (overage) bills at **$0.060/GB-hour** — a 5× ratio that's the price of
  optionality for capacity you didn't pre-commit. Both rates are flat
  across sandbox sizes. Disk above the 20 GB allowance is billed
  separately at the disk-overage rate (unchanged).
</Note>


---
title: Concepts
url: "https://docs-v1.opencomputer.dev/reserved-capacity/concepts.md"
description: "Intervals, reservations, the reservation log, and the per-org cap"
---

A small vocabulary. Learn these and the rest of the API reads itself.

## Interval

A 15-minute window on the UTC quarter-hour grid.

| Field      | Meaning                                                                          |
| ---------- | -------------------------------------------------------------------------------- |
| `startsAt` | Inclusive start. Always ends in `:00`, `:15`, `:30`, or `:45` with zero seconds. |
| `endsAt`   | Exclusive end. Always `startsAt + 15 minutes`.                                   |

The 15-minute grain is the smallest commitment unit and that's the point.
You can shape a reservation to the exact contour of your workload — a
nightly window, working-hour blocks, an hourly cron — without paying for
idle slots outside it.

Half-open intervals (`[startsAt, endsAt)`) mean two adjacent reservations
never overlap and never leave gaps.

```text
02:00 ────────────── 02:15 ────────────── 02:30
   interval A            interval B
```

A reservation that spans multiple intervals is a list of adjacent interval
entries, not a single range:

```json
{
  "intervals": [
    { "startsAt": "2026-04-29T02:00:00Z", "endsAt": "2026-04-29T02:15:00Z", "capacityGb": 16 },
    { "startsAt": "2026-04-29T02:15:00Z", "endsAt": "2026-04-29T02:30:00Z", "capacityGb": 16 }
  ]
}
```

## Reservation

A commitment event. Each `POST /api/capacity/reservations` creates one
reservation: a server-generated UUID, a `createdAt` timestamp, and one or
more `(interval, capacityGb)` line items. `capacityGb` must be a positive
multiple of 4 (= 1 GB-hour over the 15-minute interval); single-GB
reservations aren't accepted.

```json
{
  "reservationId": "9f67b8f7-7b91-4d2d-b1cb-19d0d0a14562",
  "createdAt": "2026-04-28T18:00:05Z",
  "intervals": [
    { "startsAt": "2026-04-29T02:00:00Z", "endsAt": "2026-04-29T02:15:00Z", "capacityGb": 16 }
  ]
}
```

Reservations are **append-only**. Once created, a reservation cannot be
cancelled, modified, or transferred. To add more capacity to an interval,
post another reservation; the totals add up at the interval level.

## The reservation log

The list of all your reservations is the durable, auditable record of your
commitments over time. `GET /api/capacity/reservations` returns it in
reverse-chronological order.

This is also how the calendar is computed internally: at any moment,
`reservedGb` for an interval is just the sum of `capacityGb` across all
your reservations covering that interval.

## Calendar

The planning read model. Given a time range, returns one row per interval
with the numbers you need to plan:

| Field                | Meaning                                                                                |
| -------------------- | -------------------------------------------------------------------------------------- |
| `reservableGb`       | Additional GB you could add to this interval right now.                                |
| `reservationLimitGb` | The policy cap on how much you can hold in this interval (your org's `max_memory_gb`). |
| `reservedGb`         | What you already hold in this interval (sum of all your reservations covering it).     |

The calendar **previews availability** but never holds capacity. Capacity
can change between when you read the calendar and when you submit a
reservation; if it does, the write returns `capacity_not_available` and you
re-read.

See [Reading the calendar](/reserved-capacity/calendar) for the full shape.

## Per-org capacity cap

Your org has a configured ceiling: `max_memory_gb`. It limits how
much capacity you can hold in any single 15-minute interval — your
`reservedGb` for an interval cannot exceed it. The cap appears as
`reservationLimitGb` in calendar interval rows.

If you need to reserve more than your current limit allows, ask to
have the limit raised.

## Usage and overage

Starting and stopping sandboxes is independent of reserving capacity.
Reservations don't pre-warm or pre-allocate compute; they're an
accounting commitment.

For each 15-minute interval, at every instant:

* If your concurrent memory ≤ `reservedGb` → you're inside your reserved
  capacity. That portion of the interval is billed at the reserved rate.
* If your concurrent memory > `reservedGb` → the excess is overage,
  billed at the on-demand rate.

Reserved capacity is a flat ceiling at any instant, not a GB-second budget
that averages out over the interval. A brief spike above the ceiling
produces overage during those seconds, even if your average usage was
below it.

You also pay for the **full reservation** regardless of actual usage.
That's the commitment; it's why reserved rates are lower.

## Where to go next

<CardGroup cols={2}>
  <Card title="Reading the calendar" icon="calendar" href="/reserved-capacity/calendar">
    Plan reservations before you commit.
  </Card>

  <Card title="Reserving capacity" icon="lock" href="/reserved-capacity/reserving">
    The write flow, idempotency, atomicity.
  </Card>

  <Card title="Usage and overage" icon="gauge" href="/reserved-capacity/usage-and-overage">
    How reserved capacity is consumed.
  </Card>

  <Card title="API reference" icon="code" href="/api-reference/capacity/get-calendar">
    Every endpoint with full request/response shapes.
  </Card>
</CardGroup>


---
title: Reading the Calendar
url: "https://docs-v1.opencomputer.dev/reserved-capacity/calendar.md"
description: Preview reservable capacity and existing commitments per 15-minute interval
---

The calendar is how you plan. It answers, per 15-minute interval:

1. How much GB can I still reserve here?
2. How much do I already hold here?
3. What's the policy cap I'm working under?

The calendar never holds capacity — it's a snapshot. Write validation is
always authoritative.

## Request

```bash curl
curl "https://app.opencomputer.dev/api/capacity/calendar?from=2026-04-29T02:00:00Z&to=2026-04-29T04:00:00Z" \
  -H "X-API-Key: $OPENCOMPUTER_API_KEY"
```

| Query param | Required | Notes                                     |
| ----------- | -------- | ----------------------------------------- |
| `from`      | yes      | Window start, aligned to 15 minutes, UTC. |
| `to`        | yes      | Window end, aligned to 15 minutes, UTC.   |

## Response

```json
{
  "from":      "2026-04-29T02:00:00Z",
  "to":        "2026-04-29T04:00:00Z",
  "resource":  "memory_gb",
  "intervals": [
    {
      "startsAt": "2026-04-29T02:00:00Z",
      "endsAt":   "2026-04-29T02:15:00Z",
      "reservationLimitGb": 300,
      "reservedGb": 80,
      "reservableGb": 220
    }
  ]
}
```

### Top-level fields

| Field        | Meaning                                       |
| ------------ | --------------------------------------------- |
| `from`, `to` | The query window, echoed back.                |
| `resource`   | Always `memory_gb` in v1.                     |
| `intervals`  | One row per 15-minute interval in the window. |

### Per-interval fields

| Field                | Meaning                                                                                  |
| -------------------- | ---------------------------------------------------------------------------------------- |
| `startsAt`, `endsAt` | The 15-minute UTC interval.                                                              |
| `reservationLimitGb` | Maximum GB this org may commit in this interval (your `max_memory_gb`).                  |
| `reservedGb`         | Total GB this org already has reserved in this interval.                                 |
| `reservableGb`       | Additional GB the server would currently accept. **This is the number to plan against.** |

The server enforces a **30-minute minimum lead time** between `now`
and a reservation's `startsAt`. Intervals starting sooner are returned
in the calendar (so you can see your existing commitments) but the
write endpoint rejects new reservations against them.

## `reservableGb` is the planning field

Everything you need to decide "can I reserve 50 GB at 02:00?" is in
`reservableGb`. The other fields are context:

* `reservationLimitGb` minus `reservedGb` is your remaining headroom under
  your org's per-interval cap. The server may still reduce `reservableGb`
  below that headroom because of platform-level capacity.
* `reservedGb` lets you confirm what you already hold without a separate
  call to `GET /api/capacity/reservations`.

## The calendar is a snapshot, not a hold

The calendar reflects state at the moment the request was served.
Capacity can change between read and write — another tenant (or you,
in another tab) may reserve in the meantime.

This is fine. Writes are atomic and validation is authoritative: if
capacity moved, the write returns `capacity_not_available` and you read
again.

## Race patterns

### Plan then write

```text
GET  /api/capacity/calendar                       → reservableGb: 220
POST /api/capacity/reservations  capacityGb: 80   → applied
```

### Plan then write, lost race

```text
GET  /api/capacity/calendar                       → reservableGb: 48
POST /api/capacity/reservations  capacityGb: 80   → rejected with reservableGb: 28
```

On rejection, read the calendar again and either:

* Re-submit with the actual current `reservableGb`, or
* Accept a smaller amount via multiple writes at different intervals, or
* Bail out — not all demand fits.

V1 does not accept a "reserve up to N, at least M" shape. Keep retries
explicit in client code.

### Multi-interval request, partial conflict

```text
POST /api/capacity/reservations
  intervals: [
    02:00 → 02:15, 80 GB
    02:15 → 02:30, 80 GB    # only 30 available
  ]
→ rejected; nothing reserved
```

Multi-interval writes are atomic. If any interval fails, none are
reserved. Split your request if you want partial success.

## Choosing the window

The calendar accepts arbitrary `from`–`to` ranges aligned to the 15-minute
grid. Practical guidance:

* For interactive planning UIs, pull a day or a week at a time; render a
  heatmap of `reservableGb`.
* For scheduled reservation scripts, query just the intervals you're about
  to write — don't pull a week if you only need two hours.
* For large time ranges, expect the response to scale linearly (`4 ×
  hours` rows). Chunk your queries if you need months.

## What the calendar does *not* tell you

* **Past reservation events.** Use `GET /api/capacity/reservations` for
  the audit log.
* **Other customers' commitments.** Numbers are org-scoped.
* **Future limit changes.** Your `reservationLimitGb` reflects current
  policy.

## Where to go next

<CardGroup cols={2}>
  <Card title="Reserving capacity" icon="lock" href="/reserved-capacity/reserving">
    The write flow, idempotency, atomicity.
  </Card>

  <Card title="Concepts" icon="shapes" href="/reserved-capacity/concepts">
    Vocabulary: intervals, reservations, the log.
  </Card>

  <Card title="Get calendar endpoint" icon="code" href="/api-reference/capacity/get-calendar">
    Full request/response reference.
  </Card>
</CardGroup>


---
title: Reserving Capacity
url: "https://docs-v1.opencomputer.dev/reserved-capacity/reserving.md"
description: "Write flow, idempotency, atomicity, and the audit list"
---

There are two write-side endpoints: `POST` to commit, `GET` to audit. Both
work in terms of intervals and integer GB; there are no individual blocks
or unit IDs.

Reservations compose at 15-minute granularity, so a multi-hour or
multi-day commitment is just a list of contiguous (or non-contiguous)
intervals. Reserve precisely the windows your workload occupies — there's
no minimum block size beyond the interval itself, and no requirement to
reserve continuously through periods you won't use.

## Make a reservation

One call commits capacity to one or more 15-minute intervals. Atomic across
the whole request — either every interval is reserved, or nothing is.

```bash curl
curl -X POST https://app.opencomputer.dev/api/capacity/reservations \
  -H "X-API-Key: $OPENCOMPUTER_API_KEY" \
  -H "Idempotency-Key: nightly-batch-2026-04-29" \
  -H "Content-Type: application/json" \
  -d '{
    "intervals": [
      { "startsAt": "2026-04-29T02:00:00Z", "endsAt": "2026-04-29T02:15:00Z", "capacityGb": 16 },
      { "startsAt": "2026-04-29T02:15:00Z", "endsAt": "2026-04-29T02:30:00Z", "capacityGb": 16 }
    ]
  }'
```

Response (success):

```json
{
  "reservationId": "9f67b8f7-7b91-4d2d-b1cb-19d0d0a14562",
  "createdAt": "2026-04-28T18:00:05Z",
  "intervals": [
    { "startsAt": "2026-04-29T02:00:00Z", "endsAt": "2026-04-29T02:15:00Z", "capacityGb": 16 },
    { "startsAt": "2026-04-29T02:15:00Z", "endsAt": "2026-04-29T02:30:00Z", "capacityGb": 16 }
  ]
}
```

### Validation

The server rejects malformed requests with `400 Bad Request` and a
plain text error message:

| Rule                                            | Failure                                                          |
| ----------------------------------------------- | ---------------------------------------------------------------- |
| `startsAt` / `endsAt` UTC and 15-minute aligned | Misaligned values rejected.                                      |
| `endsAt == startsAt + 15 minutes`               | Multi-interval spans rejected — enumerate intervals instead.     |
| `capacityGb` is a positive multiple of 4        | Other values rejected. The grain is 1 GB-hour (= 4 GB × 15 min). |
| `startsAt >= now + 30 minutes`                  | Lead-time violation rejected.                                    |

### Capacity shortfall (409)

When the request is well-formed but doesn't fit available capacity,
the server returns `409 capacity_not_available` with a list of every
problematic interval, so the client can retry with adjusted numbers in
a single round-trip:

```json
{
  "error": "capacity_not_available",
  "intervals": [
    {
      "startsAt": "2026-04-29T02:00:00Z",
      "requestedGb": 80,
      "reservableGb": 28,
      "reason": "insufficient_capacity"
    }
  ]
}
```

`reason` is one of:

| `reason`                | Meaning                                                                                                                 |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `insufficient_capacity` | Request exceeds current `reservableGb` for the interval (either the platform-side limit or your org's `max_memory_gb`). |
| `concurrent_write`      | Another reservation for the same interval committed first; retry with the updated `reservableGb`.                       |

## Idempotency

Both reservation writes accept an `Idempotency-Key` header. The server
stores the key with a hash of the request body and the response. Retries
return the cached response, even if the original write succeeded.

| Behavior                 | Response                                                      |
| ------------------------ | ------------------------------------------------------------- |
| Same key, same body      | Cached response from the first call.                          |
| Same key, different body | `409 idempotency_key_conflict`.                               |
| No key                   | Request processed normally; retries are the client's problem. |

Build keys from a deterministic name for the operation, e.g.
`reserve-nightly-2026-04-29`, so a client retry sends the same key.

## Cancellation does not exist

Reservations are non-refundable. Once committed, capacity is permanent for
that interval. There is no `DELETE`, no cancel endpoint, no transfer to a
different interval.

The hedge: book conservatively, only what you're confident you'll run, and
let on-demand absorb the variability above.

## List your reservations

The audit list. Returns every reservation event you've made in the time
window, in reverse-chronological order.

```bash curl
curl "https://app.opencomputer.dev/api/capacity/reservations?from=2026-04-01T00:00:00Z&to=2026-05-01T00:00:00Z" \
  -H "X-API-Key: $OPENCOMPUTER_API_KEY"
```

Response:

```json
{
  "from": "2026-04-01T00:00:00Z",
  "to":   "2026-05-01T00:00:00Z",
  "reservations": [
    {
      "reservationId": "9f67b8f7-7b91-4d2d-b1cb-19d0d0a14562",
      "createdAt": "2026-04-28T18:00:05Z",
      "intervals": [
        { "startsAt": "2026-04-29T02:00:00Z", "endsAt": "2026-04-29T02:15:00Z", "capacityGb": 16 },
        { "startsAt": "2026-04-29T02:15:00Z", "endsAt": "2026-04-29T02:30:00Z", "capacityGb": 16 }
      ]
    }
  ],
  "nextCursor": null
}
```

| Query param | Required | Meaning                                                    |
| ----------- | -------- | ---------------------------------------------------------- |
| `from`      | yes      | Return reservations whose `createdAt >= from`.             |
| `to`        | yes      | Return reservations whose `createdAt < to`.                |
| `cursor`    | no       | Pagination cursor from a previous response's `nextCursor`. |
| `limit`     | no       | Max reservations per page. Server applies a ceiling.       |

For end-of-month reconciliation, list reservations and group totals by
interval. The calendar gives you the current per-interval state; the list
gives you the history of how it got there.

## Concurrency and atomicity

| Operation            | Atomic across                | On failure        |
| -------------------- | ---------------------------- | ----------------- |
| `POST /reservations` | All intervals in the request | Nothing reserved. |

Parallel writes to the same interval from the same org serialize on the
server. Whichever request commits first claims the remaining capacity;
later requests see the updated `reservableGb` in their validation error.

## Where to go next

<CardGroup cols={2}>
  <Card title="Usage and overage" icon="gauge" href="/reserved-capacity/usage-and-overage">
    How reserved capacity consumes actual usage.
  </Card>

  <Card title="Reading the calendar" icon="calendar" href="/reserved-capacity/calendar">
    Plan before you commit.
  </Card>

  <Card title="API reference" icon="code" href="/api-reference/capacity/create-reservations">
    Every endpoint with full request/response shapes.
  </Card>
</CardGroup>


---
title: Usage and Overage
url: "https://docs-v1.opencomputer.dev/reserved-capacity/usage-and-overage.md"
description: How reserved capacity is consumed and how overage is calculated
---

## Every second is its own settlement

Reserved capacity is billed and settled per second. Not per minute, not per
interval, not against a GB-second budget. At each second `t` within an
interval:

* if `usage(t) ≤ reservedGb`: that second's memory is covered by your
  reservation and billed at the reserved rate
* if `usage(t) > reservedGb`: the part above the ceiling is overage, billed
  at the on-demand rate

Per 15-minute interval, per `(org, resource)`:

```text
reservedBilled  =  reservedGb × 900 sec                 (the commitment, regardless of usage)
overageBilled   =  ∑_t  max(usage(t) − reservedGb, 0)    (per-second integration)
```

Two charges, computed independently:

* **Reserved**: a flat `reservedGb × 900` GB-seconds, billed every interval
  at the reserved rate, whether you used it or not. That's the commitment.
* **Overage**: whatever exceeded the reserved ceiling at any instant,
  summed second by second, billed at the on-demand rate.

There is no draw-down, no carry-forward, no averaging. The ceiling is
re-checked every second.

## A worked example

You reserve 16 GB for `02:00–02:15`. Your sandboxes use:

| Phase | Duration | Memory |
| ----: | -------: | -----: |
|     1 |    600 s |   8 GB |
|     2 |     60 s |  30 GB |
|     3 |    240 s |   0 GB |

Each second is settled against the 16 GB ceiling:

| Phase | Reserved per s | Overage per s | Reserved GB·s | Overage GB·s |
| ----: | -------------: | ------------: | ------------: | -----------: |
|     1 |              8 |             0 |         4 800 |            0 |
|     2 |             16 |            14 |           960 |      **840** |
|     3 |              0 |             0 |             0 |            0 |

Bill for the interval:

| Charge                | GB-seconds | Rate      |
| --------------------- | ---------: | --------- |
| Reserved (commitment) |     14 400 | reserved  |
| Overage               |        840 | on-demand |

You pay the full 14 400 GB-seconds (`16 × 900`) for the reservation
regardless of consumption. The 60-second spike to 30 GB produced 840
GB-seconds of overage on top, calculated second by second.

## Three contrasting cases

**Usage stays below the ceiling.** Reserve 16 GB, run 4 GB throughout.
Reserved = 14 400 GB·s (commitment, paid in full). Overage = 0. The unused
capacity is the cost of optionality you gave up.

**Usage stays above the ceiling.** Reserve 16 GB, run 24 GB throughout.
Reserved = 14 400 GB·s. Overage = `8 × 900 = 7 200` GB·s.

**Usage averages below the ceiling but spikes above.** Reserve 16 GB, run
4 GB for 800 s then 25 GB for 100 s. Average is 6.8 GB — under
reservation. The 100 s above the ceiling still produces `9 × 100 = 900`
GB·s of overage. The ceiling is checked every second; averages are
irrelevant.

## Reservation and overage are independent

| Question                                              | Answer                                                            |
| ----------------------------------------------------- | ----------------------------------------------------------------- |
| Does the reservation amount affect the reserved bill? | Yes. Reserved bill = `reservedGb × 900` per interval, always.     |
| Does actual usage affect the reserved bill?           | No. The commitment is paid in full regardless.                    |
| Does the reservation amount affect the overage bill?  | Yes. Higher ceiling → less excess at each instant → less overage. |
| Does actual usage affect the overage bill?            | Yes. Sum of seconds where usage exceeded the ceiling.             |
| Do I get a credit for unused reserved capacity?       | No. Unused reserved is the cost of commitment.                    |

## Common confusions

> **"I'll only get overage when my total usage exceeds the GB-second budget."**

There is no GB-second budget. Every second is settled independently
against the ceiling.

> **"I averaged less than my reservation, so there can't be overage."**

Wrong if you ever exceeded the ceiling. Averaging is never applied.

> **"I didn't use my reservation; I shouldn't pay for it."**

You pay for it because reserving means committing unconditionally. The
reserved rate is lower precisely because of that.

> **"My reservation should auto-cover spikes."**

It doesn't. Spikes above the ceiling are overage. To cover spikes,
reserve a higher amount.

## Edge cases

| Case                                                      | Behavior                                                                                 |
| --------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| You hold a reservation but run nothing                    | Reserved billed in full. No overage.                                                     |
| You run workloads but have no reservation                 | Ceiling is at zero. Every second of usage is overage.                                    |
| You hold a reservation in one interval but run in another | No carryover. Allocation is per interval.                                                |
| Multiple sandboxes' memory sums above the ceiling         | Concurrent memory is summed before the ceiling is applied. No per-sandbox attribution.   |
| A sandbox crashes mid-interval                            | Memory drops as soon as the scale event closes; subsequent seconds see the lower number. |
| You try to reserve for an interval you're already in      | Rejected — reservations require a 30-minute minimum lead time.                           |

## Why per-second

A GB-second budget would let arbitrary spikes ride for free as long as
quiet time balanced them out. That's burstable behavior, not
committed-capacity behavior, and it creates incentives for adversarial
spike patterns.

Per-second settlement matches what the reservation actually is: at every
moment, the ceiling is what you committed to. Above it is overage. Below
it is covered.

## Where to go next

<CardGroup cols={2}>
  <Card title="Reserving capacity" icon="lock" href="/reserved-capacity/reserving">
    How to commit to capacity in advance.
  </Card>

  <Card title="Reading the calendar" icon="calendar" href="/reserved-capacity/calendar">
    Plan capacity you'll actually use.
  </Card>
</CardGroup>


---
title: CLI
url: "https://docs-v1.opencomputer.dev/cli/overview.md"
description: Manage sandboxes from your terminal
---

The `oc` CLI lets you manage OpenComputer sandboxes directly from your terminal — create sandboxes, run commands, open interactive shells, manage checkpoints, and more.

## Installation

<Tabs items={["macOS (Apple Silicon)", "macOS (Intel)", "Linux (x86_64)", "Linux (ARM64)"]}>
  <Tab title="macOS (Apple Silicon)">
    ```bash
    curl -fsSL https://github.com/diggerhq/opencomputer/releases/latest/download/oc-darwin-arm64 -o /usr/local/bin/oc
    chmod +x /usr/local/bin/oc
    ```
  </Tab>

  <Tab title="macOS (Intel)">
    ```bash
    curl -fsSL https://github.com/diggerhq/opencomputer/releases/latest/download/oc-darwin-amd64 -o /usr/local/bin/oc
    chmod +x /usr/local/bin/oc
    ```
  </Tab>

  <Tab title="Linux (x86_64)">
    ```bash
    curl -fsSL https://github.com/diggerhq/opencomputer/releases/latest/download/oc-linux-amd64 -o /usr/local/bin/oc
    chmod +x /usr/local/bin/oc
    ```
  </Tab>

  <Tab title="Linux (ARM64)">
    ```bash
    curl -fsSL https://github.com/diggerhq/opencomputer/releases/latest/download/oc-linux-arm64 -o /usr/local/bin/oc
    chmod +x /usr/local/bin/oc
    ```
  </Tab>
</Tabs>

## Sign in

```bash
oc login
oc whoami
```

`oc login` prints a short confirmation code and opens the hosted sign-in page.
Use `oc login --no-browser` over SSH or when a coding agent is running the
command for you. The CLI stores the resulting credential in
`~/.oc/config.json` with private file permissions; it never prints the key.

```bash
oc logout          # revoke the CLI-created key, then clear it locally
oc logout --local  # clear local login state without claiming remote revocation
```

`oc login` authenticates the local CLI. CI, servers, and SDK applications
should continue to receive an explicit org key through their secret manager and
`OPENCOMPUTER_API_KEY`.

### Credential resolution

| Priority    | Source                | Example                                 |
| ----------- | --------------------- | --------------------------------------- |
| 1 (highest) | CLI flags             | `--api-key=xxx`                         |
| 2           | Environment variables | `OPENCOMPUTER_API_KEY`                  |
| 3           | Config file           | `oc login` or `oc config set api-key …` |
| 4 (lowest)  | Defaults              | `https://app.opencomputer.dev`          |

An active `--api-key` or `OPENCOMPUTER_API_KEY` override must be removed before
`oc login` or remote `oc logout`; `oc logout --local` can still clear the saved
CLI login. Manually setting an API key remains supported:

```bash
oc config set api-key "$OPENCOMPUTER_API_KEY"
oc config show
```

## Global Flags

| Flag        | Environment Variable   | Description                      |
| ----------- | ---------------------- | -------------------------------- |
| `--api-key` | `OPENCOMPUTER_API_KEY` | API key for authentication       |
| `--api-url` | `OPENCOMPUTER_API_URL` | Control plane URL                |
| `--json`    | —                      | Output as JSON instead of tables |

## Key Workflows

### Quick Start

```bash
# Create a sandbox
oc create

# Run a command and wait for the result
oc exec sb-abc123 --wait -- echo "Hello from the cloud"

# Open an interactive shell
oc shell sb-abc123

# Clean up
oc sandbox kill sb-abc123
```

### JSON Output & Scripting

All commands support `--json` for machine-readable output:

```bash
# Get a sandbox ID programmatically
ID=$(oc create --json | jq -r '.sandboxID')

# List all running sandbox IDs
oc ls --json | jq -r '.[].sandboxID'

# Run a command and capture the result
RESULT=$(oc exec $ID --json --wait -- npm test)
echo $RESULT | jq '.exitCode'
```

### Top-level Shortcuts

| Shortcut    | Expands to          |
| ----------- | ------------------- |
| `oc create` | `oc sandbox create` |
| `oc ls`     | `oc sandbox list`   |
| `oc cp`     | `oc checkpoint`     |

### Create and Shell In

```bash
oc shell $(oc create --json | jq -r '.sandboxID')
```

### Hibernate for Cost Savings

```bash
oc sandbox hibernate sb-abc123
# ... hours later ...
oc sandbox wake sb-abc123
oc shell sb-abc123
```

### Checkpoint and Fork

```bash
oc cp create sb-abc --name ready-state
ID1=$(oc cp spawn cp-xyz --json | jq -r '.sandboxID')
ID2=$(oc cp spawn cp-xyz --json | jq -r '.sandboxID')
oc exec $ID1 --wait -- ./test-a.sh
oc exec $ID2 --wait -- ./test-b.sh
```

## Command Reference

| Command                                 | Description                                |
| --------------------------------------- | ------------------------------------------ |
| [`oc sandbox`](/cli/sandbox)            | Create, list, kill, hibernate, wake        |
| [`oc exec`](/cli/exec)                  | Run commands, manage exec sessions         |
| [`oc shell`](/cli/shell)                | Interactive PTY terminal                   |
| [`oc checkpoint`](/cli/checkpoint)      | Snapshot, fork, restore                    |
| [`oc patch`](/cli/patch)                | Checkpoint-attached scripts                |
| [`oc preview`](/cli/preview)            | Expose ports to the internet               |
| [`oc agent`](/reference/cli/agent)      | Invoke durable agents and manage Hook URLs |
| [`oc login`](/reference/cli/auth)       | Sign in, inspect identity, and log out     |
| [`oc config`](/reference/cli#oc-config) | Configure API key and API URL              |

<Tip>
  Full flag reference for every command: [CLI Reference](/reference/cli).
</Tip>


---
title: Sandbox Management
url: "https://docs-v1.opencomputer.dev/cli/sandbox.md"
description: "Create, manage, and control sandbox lifecycles from the CLI"
---

## Creating a Sandbox

`oc create` (shortcut for `oc sandbox create`) provisions a new sandbox VM:

```bash
oc create
oc create --timeout 600 --cpu 2 --memory 2048 --env NODE_ENV=production
```

The sandbox ID is printed on success. Use `--json` to capture it programmatically:

```bash
ID=$(oc create --json | jq -r '.sandboxID')
```

## Listing & Inspecting

```bash
# List all sandboxes
oc ls

# Detailed info for a specific sandbox
oc sandbox get sb-abc123
```

`oc ls` shows a table with ID, template, status, CPU, memory, and age. Add `--json` for machine-readable output.

## Hibernation & Wake

Save state and stop paying for compute. Wake resumes the same sandbox — the platform attempts fast snapshot restore with a cold-boot fallback if needed.

```bash
oc sandbox hibernate sb-abc123

# ... hours later ...
oc sandbox wake sb-abc123
oc shell sb-abc123
```

The sandbox keeps the same ID across hibernate/wake cycles. Preview URLs remain active.

## Adjusting Timeout

The idle timeout resets on every operation (exec, file access, agent activity). Default: 300s.

```bash
oc sandbox set-timeout sb-abc123 3600  # 1 hour
```

## Killing a Sandbox

```bash
oc sandbox kill sb-abc123
```

All data is lost unless you've created a [checkpoint](/cli/checkpoint) first.

## Common Patterns

### Create and Shell In

```bash
oc shell $(oc create --json | jq -r '.sandboxID')
```

### Filter Running Sandboxes

```bash
oc ls --json | jq '.[] | select(.status == "running") | .sandboxID'
```

### Batch Cleanup

```bash
oc ls --json | jq -r '.[].sandboxID' | xargs -I{} oc sandbox kill {}
```

<Tip>
  SDK usage: [Sandboxes](/sandboxes/overview). Full flags: [CLI Reference](/reference/cli#oc-sandbox).
</Tip>


---
title: Running Commands
url: "https://docs-v1.opencomputer.dev/cli/exec.md"
description: Execute shell commands from the CLI
---

## Running a Command

Use `--wait` to run a command synchronously — output streams to your terminal and the CLI exits with the process exit code:

```bash
oc exec sb-abc123 --wait -- echo "Hello, World!"
oc exec sb-abc123 --wait -- npm run build
```

Without `--wait`, `oc exec` creates an **exec session** and prints the session ID with attach instructions. This is useful for long-running commands:

```bash
oc exec sb-abc123 -- node server.js
# → Session es-xyz created. Attach with: oc exec attach sb-abc es-xyz
```

## Working Directory & Environment

```bash
oc exec sb-abc123 --wait --cwd /app --env NODE_ENV=production -- npm run build
oc exec sb-abc123 --wait --env API_KEY=xxx --env DEBUG=true -- ./run.sh
```

## Timeouts

Set a timeout with `--timeout` (in seconds). Default is 0 (no timeout). When exceeded, the command is killed:

```bash
oc exec sb-abc123 --wait --timeout 30 -- npm test
```

## Creating an Exec Session

Plain `oc exec` (without `--wait`) creates a session for long-running commands. The command keeps running even after you disconnect:

```bash
oc exec sb-abc123 -- python train.py
# Session es-abc created
```

## Capturing Output as JSON

Combine `--json` with `--wait` for scripting:

```bash
RESULT=$(oc exec sb-abc123 --json --wait -- node -e 'console.log("hello")')
echo $RESULT | jq '.stdout'   # "hello\n"
echo $RESULT | jq '.exitCode' # 0
```

## Managing Exec Sessions

```bash
# List active sessions
oc exec list sb-abc123

# Kill a session
oc exec kill sb-abc123 es-xyz
oc exec kill sb-abc123 es-xyz --signal 15  # SIGTERM
```

<Warning>
  `oc exec attach` exists as a command but is not yet implemented. It prints guidance to use the SDK or a WebSocket client. Use `oc exec list` and `oc exec kill` to manage sessions from the CLI.
</Warning>

## Shell vs Exec

| Use Case                           | Command                  |
| ---------------------------------- | ------------------------ |
| Single command, capture output     | `oc exec --wait`         |
| Scripting and automation           | `oc exec --wait --json`  |
| Long-running background process    | `oc exec` (no `--wait`)  |
| Interactive development, debugging | [`oc shell`](/cli/shell) |

<Tip>
  SDK usage: [Running Commands](/sandboxes/running-commands). Full flags: [CLI Reference](/reference/cli#oc-exec).
</Tip>


---
title: Shell
url: "https://docs-v1.opencomputer.dev/cli/shell.md"
description: Interactive terminal sessions from the CLI
---

## Opening a Shell

```bash
oc shell sb-abc123
```

Opens a full interactive terminal over WebSocket. Press `Ctrl+D` or type `exit` to disconnect.

## Choosing a Shell

```bash
oc shell sb-abc123 --shell /bin/zsh
```

Default: `/bin/bash`.

## What Works

The PTY supports everything you'd expect from a real terminal:

* **Editors:** vim, nano
* **Monitors:** top, htop
* **Tab completion** and command history
* **Colors** and ANSI escape codes
* **Terminal resizing** — automatic, resize events forwarded

Full-screen applications like `tmux` and `less` work correctly.

## Shell vs Exec

| Use Case                             | Command                 |
| ------------------------------------ | ----------------------- |
| Run a single command, capture output | `oc exec --wait`        |
| Scripting and automation             | `oc exec --wait --json` |
| Interactive development              | `oc shell`              |
| Debugging                            | `oc shell`              |

## Tips

### Create and Shell In

```bash
oc shell $(oc create --json | jq -r '.sandboxID')
```

### Shell with Custom Environment

Set up the environment before shelling in:

```bash
ID=$(oc create --json | jq -r '.sandboxID')
oc exec $ID --wait -- bash -c 'echo "export PATH=/app/bin:\$PATH" >> ~/.bashrc'
oc shell $ID
```

<Tip>
  SDK usage: [Interactive Terminals](/sandboxes/interactive-terminals). Full flags: [CLI Reference](/reference/cli#oc-shell).
</Tip>


---
title: Checkpoints
url: "https://docs-v1.opencomputer.dev/cli/checkpoint.md"
description: "Snapshot, fork, and restore from the CLI"
---

## Creating a Checkpoint

Capture the current state of a running sandbox:

```bash
oc checkpoint create sb-abc123 --name before-migration
# or using the shortcut:
oc cp create sb-abc123 --name before-migration
```

The checkpoint saves the filesystem and installed state. Status starts as `processing` and transitions to `ready`.

```bash
oc cp list sb-abc123
# ID          NAME               STATUS  SIZE     CREATED
# cp-7f3a1b2c before-migration   ready   128 MB   2025-01-15T10:30:00Z
```

## Forking from a Checkpoint

Create new sandboxes from a saved checkpoint. Each fork is independent:

```bash
# Spawn two independent sandboxes from the same checkpoint
ID1=$(oc cp spawn cp-7f3a1b2c --json | jq -r '.sandboxID')
ID2=$(oc cp spawn cp-7f3a1b2c --json | jq -r '.sandboxID')

# Run different experiments
oc exec $ID1 --wait -- ./experiment-a.sh
oc exec $ID2 --wait -- ./experiment-b.sh
```

Forked sandboxes start with a fresh boot from the saved disk state — don't assume running processes carry over.

## Restoring

Revert a sandbox in-place to a checkpoint. All changes since the checkpoint are lost:

```bash
oc cp restore sb-abc123 cp-7f3a1b2c
```

## Listing and Deleting

```bash
oc cp list sb-abc123
oc cp delete sb-abc123 cp-7f3a1b2c
```

Each sandbox can have up to 10 full checkpoints and up to 100 disk-only checkpoints.

To create a new checkpoint at the limit, opt into automatic deletion of the oldest eligible checkpoint of the same type:

```bash
oc cp create sb-abc123 \
  --name autosave \
  --kind disk_only \
  --retention-policy delete_oldest \
  --retention-max-count 100
```

Retention skips public checkpoints, patched checkpoints, and checkpoints that are still referenced by forked sandboxes.

## Checkpoint vs Hibernate

|                  | Checkpoint                    | Hibernate                      |
| ---------------- | ----------------------------- | ------------------------------ |
| Original sandbox | Keeps running                 | Stopped                        |
| Can fork         | Yes — unlimited new sandboxes | No                             |
| Use case         | Branching, parallel testing   | Pause and resume, cost savings |

Use checkpoints when you need to explore multiple paths from the same state. Use hibernation when you just want to pause and resume later.

<Tip>
  SDK usage: [Checkpoints](/sandboxes/checkpoints). Full flags: [CLI Reference](/reference/cli#oc-checkpoint).
</Tip>


---
title: Patches
url: "https://docs-v1.opencomputer.dev/cli/patch.md"
description: Attach scripts to checkpoints from the CLI
---

## What Patches Do

A patch is a shell script attached to a checkpoint. Patches run in order every time a sandbox is forked from that checkpoint — layer setup without re-creating the checkpoint.

## Creating a Patch

From a file or stdin:

```bash
# From a file
oc patch create cp-7f3a1b2c --script ./setup.sh --description "Install deps"

# One-liner from stdin
echo "npm install -g typescript" | oc patch create cp-7f3a1b2c --script=-
```

## Layering Setup

Add multiple patches — they execute in creation order on every fork:

```bash
oc patch create cp-7f3a1b2c --script ./install-node.sh --description "Node.js 20"
oc patch create cp-7f3a1b2c --script ./app-config.sh --description "App config"

# Each spawned sandbox gets both patches applied in order
oc cp spawn cp-7f3a1b2c
```

## Inspecting Patches

Review what will run before forking:

```bash
oc patch list cp-7f3a1b2c
# ID          SEQ  DESCRIPTION     STRATEGY  CREATED
# pa-abc123   1    Node.js 20      on_wake   2025-01-15T10:30:00Z
# pa-def456   2    App config      on_wake   2025-01-15T10:31:00Z
```

## Removing a Patch

```bash
oc patch delete cp-7f3a1b2c pa-abc123
```

<Tip>
  SDK usage: [Patches](/sandboxes/patches). Full flags: [CLI Reference](/reference/cli#oc-patch).
</Tip>


---
title: Preview URLs
url: "https://docs-v1.opencomputer.dev/cli/preview.md"
description: Expose sandbox ports from the CLI
---

## Exposing a Port

Create a public HTTPS URL that proxies traffic to a port inside your sandbox:

```bash
oc preview create sb-abc123 --port 3000
# → hostname: sb-abc123-p3000.workers.opencomputer.dev
```

## Sharing a Dev Server

Start a server inside the sandbox, expose it, and share the URL:

```bash
# Start a dev server in the background
oc exec sb-abc123 -- bash -c 'cd /app && npm run dev &'

# Expose port 3000
oc preview create sb-abc123 --port 3000

# Get the URL
oc preview list sb-abc123
```

## Multiple Ports

Expose multiple services from the same sandbox:

```bash
oc preview create sb-abc123 --port 3000  # Frontend
oc preview create sb-abc123 --port 8080  # API server

oc preview list sb-abc123
# PORT  HOSTNAME                                      SSL     CREATED
# 3000  sb-abc123-p3000.workers.opencomputer.dev      active  ...
# 8080  sb-abc123-p8080.workers.opencomputer.dev      active  ...
```

## Custom Domains

Pass `--domain` to use your own domain. DNS must point to OpenComputer's ingress, and the domain must be verified in the dashboard. SSL is provisioned automatically.

```bash
oc preview create sb-abc123 --port 3000 --domain preview.myapp.com
```

## Cleanup

```bash
oc preview delete sb-abc123 3000
```

Preview URLs persist across hibernation/wake cycles — no need to re-create them.

## Bearer-Token Authentication

By default, anyone who knows the preview hostname can hit your sandbox's port. To require an `Authorization: Bearer <token>` header on every request, opt in at create time:

```bash
oc sandbox create --preview-auth
# Created sandbox sb-abc123 (status: running)
# Preview auth token (shown once): qx2sSi5IYXWBvnnRqwK9Ky_cIAI-x0Vx1bPCt0XMxsI
```

Then call your preview URL with the token:

```bash
curl -H "Authorization: Bearer qx2sSi5IYX..." https://sb-abc123-p3000.workers.opencomputer.dev/
```

Bring your own token if your gateway already has a shared secret:

```bash
oc sandbox create --preview-auth-token "$GATEWAY_TOKEN"
```

Rotate the token (old one stops working immediately):

```bash
oc preview rotate-auth sb-abc123
# New preview auth token (shown once): <new token>
```

Token is shown exactly once and only its SHA-256 hash is stored. See [Authentication](/sandboxes/preview-urls#authentication) for the SDK equivalents.

<Tip>
  SDK usage: [Preview URLs](/sandboxes/preview-urls). Full flags: [CLI Reference](/reference/cli#oc-preview).
</Tip>


---
title: Secrets
url: "https://docs-v1.opencomputer.dev/cli/secrets.md"
description: Manage secret stores and secrets from the CLI
---

## Secret stores

```bash
# Create a store (with optional egress restrictions)
oc secret-store create --name my-secrets --egress-allowlist api.anthropic.com

# List stores
oc secret-store list

# Update a store
oc secret-store update <store-id> --egress-allowlist api.anthropic.com,*.openai.com

# Delete a store (and all its secrets)
oc secret-store delete <store-id>
```

## Secrets

```bash
# Set a secret
oc secret set <store-id> ANTHROPIC_API_KEY sk-ant-...

# Set from stdin
echo "sk-ant-..." | oc secret set <store-id> ANTHROPIC_API_KEY --from-stdin

# Restrict a secret to specific hosts
oc secret set <store-id> ANTHROPIC_API_KEY sk-ant-... --allowed-hosts api.anthropic.com

# List secrets (names only — values are never returned)
oc secret list <store-id>

# Delete a secret
oc secret delete <store-id> ANTHROPIC_API_KEY
```

## Using secrets in sandboxes

```bash
# Create a sandbox with a secret store
oc create --secret-store my-secrets

# Env vars are sealed — real values are never in the VM
oc exec <sandbox-id> -- echo '$ANTHROPIC_API_KEY'
# osb_sealed_7f3a9c... (not the real key)
```

For how secrets work under the hood, see [Secrets](/sandboxes/secrets).


---
title: Build a Lovable Clone
url: "https://docs-v1.opencomputer.dev/guides/build-a-lovable-clone.md"
description: Build an open-source AI app builder using OpenComputer sandboxes
---

In this guide you'll build a fully functional **Lovable clone** — an AI-powered web app builder where users describe an app in plain English and get a live, running React application in seconds. The entire project is open-source and uses OpenComputer sandboxes as the execution engine.

<Note>
  For new app-builder projects, start with [Durable Agent Sessions](/agent-sessions/quickstart) and the [app-builder example](https://github.com/diggerhq/oc-sessions-demos). This guide shows the older raw-sandbox orchestration pattern, which is still useful when you want to own the agent loop yourself.
</Note>

<CardGroup cols={2}>
  <Card title="GitHub Repository" icon="github" href="https://github.com/diggerhq/osslovable">
    Source code for the complete project.
  </Card>

  <Card title="Durable sessions example" icon="github" href="https://github.com/diggerhq/oc-sessions-demos">
    The recommended app-builder architecture for new projects.
  </Card>

  <Card title="Live Demo" icon="globe" href="https://openlovable.cc">
    Try the running app at openlovable.cc.
  </Card>
</CardGroup>

***

## Prerequisites

* **Node.js 20+** installed locally
* An **Anthropic API key** (for Claude)
* An **OpenComputer API key**

***

## Step 1: Sign Up for OpenComputer

Go to [app.opencomputer.dev](https://app.opencomputer.dev) and create an account.

## Step 2: Generate an API Token

From the OpenComputer dashboard, generate an API token. You'll use this to authenticate SDK calls that create and manage sandboxes.

## Step 3: (Optional) Add a Custom Domain

If you want deployed apps to be accessible on your own domain (e.g. `*.mycompany.com`) instead of the default OpenComputer hostnames:

1. In the OpenComputer dashboard, add your domain (e.g. `mycompany.com`).
2. Create a **TXT** record on your domain to verify ownership — the dashboard will show you the exact record to add.
3. Create a **CNAME** record for `*.mycompany.com` pointing to the value provided by OpenComputer. This routes all subdomain traffic to your sandboxes.

Once verified, preview URLs for your sandboxes will be served under your custom domain.

## Step 4: Set Up the Project

Clone the repository and install dependencies:

```bash
git clone https://github.com/diggerhq/osslovable.git
cd osslovable
npm run install:all
```

Create a `.env` file at the project root with your API keys:

```bash
ANTHROPIC_API_KEY=sk-ant-...
OPENCOMPUTER_API_KEY=your-opencomputer-token
DEPLOY_DOMAIN=mycompany.com  # optional — defaults to openlovable.cc
```

If you configured a custom domain in Step 3, set `DEPLOY_DOMAIN` to that domain so that deployed apps are served under it (e.g. `https://<id>.mycompany.com`). If you skip this, deploy URLs will use the default `openlovable.cc` domain.

Start the development server:

```bash
npm run dev
```

This starts both the Express backend (port 3001) and the Vite frontend (port 5173). Open `http://localhost:5173` in your browser, type a prompt, and watch your app get built.

***

## How It Works

The architecture is straightforward: a React frontend sends a user prompt to an Express backend, which orchestrates Claude and an OpenComputer sandbox to generate, write, and run the code.

### Generation Flow

1. **User submits a prompt** — The frontend sends the prompt to `POST /api/generate`.
2. **Sandbox is created** — The server calls `Sandbox.create()` from the OpenComputer SDK to spin up an isolated Node.js environment.
3. **Claude generates code** — The prompt is sent to Claude with a system prompt that instructs it to output complete file contents in a structured XML format (`<file path="...">...</file>`).
4. **Files are streamed and written** — As Claude streams its response, the server parses file blocks in real-time. Each file is written into the sandbox's filesystem using `sandbox.files.write()`.
5. **Dev server starts** — Once all files are written, the server runs `npm install` and starts a Vite dev server inside the sandbox.
6. **Preview URL is returned** — The server calls `sandbox.createPreviewURL({ port: 80 })` to get a public URL, which is sent to the frontend and displayed in an iframe.

### Sandbox as Tool Calls

The key idea is that **every interaction with the sandbox is a tool call** from the server to the OpenComputer SDK. The server uses these SDK methods as its "tools":

```typescript
import { Sandbox } from "@opencomputer/sdk";

// Create an isolated environment
const sandbox = await Sandbox.create({ template: "node", timeout: 600 });

// Write files
await sandbox.files.write("/workspace/src/App.tsx", code);

// Run commands
await sandbox.exec.run("cd /workspace && npm install");

// Get a public URL
const preview = await sandbox.createPreviewURL({ port: 80 });
```

This is a simple but effective pattern: Claude generates the code as text, and the server uses the sandbox SDK to make it real. There are no actual LLM tool-use calls involved — the server orchestrates everything itself.

### Limitations of This Approach

* **No iterative editing** — Claude generates the entire project in one shot. There's no back-and-forth where the LLM can see errors, fix them, and retry. A more robust version would use Claude's tool-use capability to let it write files, run commands, see output, and iterate.
* **No persistent state** — The development sandbox is ephemeral. If the server restarts, in-memory sandbox references are lost. There's no git history or checkpoint system to restore previous states.
* **Single-turn only** — Each prompt creates a fresh generation. Follow-up prompts create a new sandbox rather than iterating on the existing code.

***

## Deployment Model

The app includes a **Deploy** button that creates a shareable, publicly accessible URL for the generated app.

### How Deployment Works

1. **A new sandbox is created** — Deployment does *not* reuse the development sandbox. A fresh sandbox is spun up specifically for the deployed version.
2. **Files are copied over** — All source files (excluding `node_modules`) are copied from the dev sandbox to the deploy sandbox using the SDK's file APIs.
3. **Dependencies are installed and the server starts** — The deploy sandbox runs `npm install` and starts the Vite dev server, just like the dev sandbox.
4. **A fixed deploy URL is created** — The server calls `sandbox.createPreviewURL()` with a custom domain configuration, producing a stable URL (e.g. `https://<id>.openlovable.cc`) that can be shared with others.

```typescript
const domain = process.env.DEPLOY_DOMAIN || "openlovable.cc";
const url = await sandbox.createPreviewURL({
  port: 80,
  domain,
  authConfig: {},
});
```

### Why a Separate Sandbox?

The deploy sandbox is intentionally separate from the dev sandbox so that:

* The developer can keep iterating in the dev sandbox without affecting the deployed version.
* The deployed version has a fixed, shareable URL that doesn't change.
* Each deployment is a clean, reproducible build.

### Hibernation and Wake-Up

OpenComputer sandboxes have a built-in **hibernation model**. When a deployed sandbox receives no traffic, it is automatically hibernated to save resources. When someone visits the deploy URL, the sandbox is woken up on demand. This means:

* **Deploy URLs are persistent** — they continue to work even after the sandbox hibernates.
* **Cold starts are fast** — sandboxes resume from hibernation in seconds.
* **No always-on cost** — you're not paying for idle compute.

This makes the deployment model practical for sharing prototypes and demos without running up a large bill.

***

## Limitations and What's Coming

### Current Limitations

* **No version control** — There is no git server backing the sandbox, so there's no way to checkpoint, diff, or roll back changes. If you want to restore a previous version, you'd need to regenerate it.
* **No secret management** — API keys and environment variables are passed in plain text. There's no built-in mechanism for securely sealing secrets that the sandbox needs at runtime.
* **Ephemeral dev state** — The development sandbox lives only as long as the server process. A server restart or crash loses all active sandbox references.
* **Single-turn generation** — The current architecture doesn't support multi-turn conversations where Claude can iterate on its output based on build errors or user feedback.
* **No private link sharing** — Deploy URLs are either fully public or not accessible at all. There's no way to share a preview link with specific team members or collaborators behind authentication.

### What's Changed Since This Guide Was Written

Several features mentioned as limitations now exist in OpenComputer:

* **In-sandbox agent loop** — [Agent sessions](/agent-sessions/overview) now run the Claude Agent SDK inside the sandbox. The agent reads files, runs commands, and iterates on errors locally without leaving the VM. This is the recommended approach for new projects.
* **Preview URL authentication** — `authConfig` on [preview URLs](/sandboxes/preview-urls) supports restricting access. Not full private link sharing yet, but a step toward it.
* **Checkpoints** — [Checkpoint and fork](/sandboxes/checkpoints) workflows are available for snapshotting sandbox state, restoring, and branching.
* **Custom templates** — Define [custom environments](/sandboxes/templates) with the Image builder and pre-built snapshots.

### Still Planned

* **Built-in git server** — Git-based version control for sandbox state
* **Secret sealing** — Encrypted secrets decrypted only inside the sandbox at runtime


---
title: Browser Automation
url: "https://docs-v1.opencomputer.dev/guides/browser-automation.md"
description: "Run headless and headed browsers inside OpenComputer sandboxes — for scraping, logged-in workflows, and AI-driven automation."
---

OpenComputer sandboxes are full Linux VMs, which means you can run a real Chromium browser inside them the same way you would on a laptop. This guide walks through the setup that makes it *actually work*: the right Chromium flavor, the system libraries you need, the OpenComputer-specific networking quirks, and how to persist browser state across runs.

The examples use [**libretto**](https://libretto.sh), an AI-friendly CLI + library on top of Playwright. Everything here applies equally to raw Playwright, Puppeteer, [browser-use](https://browser-use.com/), or [Browserbase](https://www.browserbase.com/) — libretto is just a convenient default.

<CardGroup cols={2}>
  <Card title="GitHub Repository" icon="github" href="https://github.com/diggerhq/oc-browser-automation">
    Runnable reference implementation of everything in this guide.
  </Card>

  <Card title="libretto Docs" icon="book" href="https://libretto.sh/docs">
    CLI + library reference for the browser-automation tool used in the examples.
  </Card>
</CardGroup>

***

## When to reach for a browser (vs. an API)

* The target has no API (flight aggregators, airline sites, most SaaS admin UIs).
* The target needs a real logged-in session (cookies + localStorage + JS-challenge cookies).
* You want **AI-driven interaction** — describe a task in English, let the agent figure out the clicks.
* You need screenshots / visual artefacts for verification.

If the site has a good API, use the API. A browser is slower, heavier, and flakier. But when you need it, OpenComputer gives you real VMs — not containers — so you can run full Chromium with no Docker-flavored limitations.

***

## Step 1: Build a snapshot with Chromium pre-installed

Browser setup is heavy (apt packages + Chromium binary is \~500MB). Bake it into a [named snapshot](/sandboxes/snapshots) once, launch sandboxes from it in seconds.

```typescript build-snapshot.ts
import { Image, Snapshots } from "@opencomputer/sdk/node";

// Runtime deps Chromium links against on Ubuntu 22.04. Matches Playwright's
// published dependency list.
const CHROMIUM_DEPS = [
  "libnss3", "libnspr4", "libatk1.0-0", "libatk-bridge2.0-0", "libcups2",
  "libdrm2", "libxkbcommon0", "libxcomposite1", "libxdamage1", "libxfixes3",
  "libxrandr2", "libxext6", "libgbm1", "libpango-1.0-0", "libcairo2",
  "libasound2", "fonts-liberation",
  // libnss3-tools gives us `certutil` — needed at runtime to trust OC's
  // egress-proxy CA in Chromium (Chromium ignores SSL_CERT_FILE).
  "libnss3-tools",
];

const image = Image.base()
  .aptInstall(CHROMIUM_DEPS)
  .workdir("/home/sandbox")
  .runCommands(
    "cd /home/sandbox && npm init -y >/dev/null",
    // Install libretto + AI adapter locally so `npx libretto` resolves without
    // a cold npm fetch at runtime.
    "cd /home/sandbox && npm install --no-audit --no-fund libretto @ai-sdk/anthropic",
    // Playwright downloads its own Chromium build — we want headless-shell
    // for lightweight scraping, or full chromium for headed/VNC scenarios.
    "cd /home/sandbox && npx --yes playwright install chromium-headless-shell",
  );

const snapshots = new Snapshots();
await snapshots.create({ name: "browser", image });
```

<Warning>
  **Don't apt-install `chromium-browser` on Ubuntu 22.04.** That package is a snap shim that won't run in a minimal VM. Either install Google Chrome via its apt repo (`google-chrome-stable`), or rely on Playwright's bundled Chromium (recommended — it's purpose-built for automation).
</Warning>

***

## Step 2: Per-boot setup that can't be baked in

A few things need to run at sandbox startup rather than during snapshot build, because they depend on per-sandbox state.

```typescript launch.ts
import { Sandbox, SecretStore } from "@opencomputer/sdk/node";

const sandbox = await Sandbox.create({
  snapshot: "browser",
  envs: {
    ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY!,
    // Keep Playwright/Chromium off the small tmpfs /dev/shm and /tmp —
    // write profile dirs, cache, everything to the data disk.
    TMPDIR: "/home/sandbox/tmp",
  },
  secretStore: "browser-egress",  // see "Networking" section below
});

// Populate /etc/hosts — the guest kernel sets up /etc/resolv.conf but not
// /etc/hosts, and libretto's Playwright CDP client hardcodes http://localhost
// which will ENOTFOUND without this.
await sandbox.commands.run(
  "grep -q 'localhost' /etc/hosts || " +
    "(printf '127.0.0.1 localhost\\n::1 localhost\\n' | sudo tee -a /etc/hosts)",
);

// Trust OC's egress-proxy CA in Chromium's NSS store. Chromium ignores
// SSL_CERT_FILE / NODE_EXTRA_CA_CERTS — the env vars OC sets for libraries
// have no effect on the browser's TLS validation.
await sandbox.commands.run(
  [
    "mkdir -p /home/sandbox/.pki/nssdb",
    "certutil -d sql:/home/sandbox/.pki/nssdb -N --empty-password || true",
    "certutil -d sql:/home/sandbox/.pki/nssdb -A -n opensandbox-proxy -t 'TC,C,T' " +
      "-i /usr/local/share/ca-certificates/opensandbox-proxy.crt",
  ].join(" && "),
);

await sandbox.commands.run("mkdir -p /home/sandbox/tmp && chmod 700 /home/sandbox/tmp");
```

***

## Networking: the `secretStore` requirement

<Warning>
  This is the single most common gotcha. A sandbox without a `secretStore` attached **has no outbound egress at all** — every HTTPS request gets a `407` from the internal proxy.
</Warning>

OpenComputer routes all outbound traffic through a secrets-injection proxy. The proxy only accepts traffic from sandboxes that have at least one sealed secret registered, because session registration happens as part of sealing secrets.

The workaround is to create a `SecretStore` with a wildcard egress allowlist and at least one (possibly dummy) entry:

```typescript
import { SecretStore } from "@opencomputer/sdk/node";

const stores = await SecretStore.list();
let store = stores.find((s) => s.name === "browser-egress");
if (!store) {
  store = await SecretStore.create({
    name: "browser-egress",
    egressAllowlist: ["*"],  // wildcard — scope this down for production
  });
}
await SecretStore.setSecret(store.id, "PLACEHOLDER", "not-used-just-triggers-session");

const sandbox = await Sandbox.create({
  snapshot: "browser",
  secretStore: "browser-egress",
});
```

With the store attached, outbound traffic flows normally and the `opensandbox-proxy.crt` we trusted earlier lets Chromium validate the MITM-rewritten certs.

***

## Step 3: Run a headless browser

With the snapshot and per-boot setup in place, driving the browser is a normal libretto session:

```typescript
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const run = promisify(execFile);

// Open a page in a named session — libretto persists cookies/localStorage
// per session name in .libretto/sessions/<name>/.
await run("npx", ["libretto", "open", "https://example.com", "--session", "demo", "--headless"], {
  cwd: "/home/sandbox",
});

// AI snapshot — Claude analyzes the page and returns a summary + selectors.
const { stdout } = await run("npx", [
  "libretto", "snapshot", "--session", "demo",
  "--objective", "Describe the main content and interactive elements.",
  "--context", "Freshly loaded page",
], { cwd: "/home/sandbox", maxBuffer: 8 * 1024 * 1024 });

console.log(stdout);
```

The `snapshot` command requires an `ANTHROPIC_API_KEY` (or an OpenAI / Gemini / Vertex key — configure via `.libretto/config.json`). It's the AI-driven feature that makes libretto different from raw Playwright.

***

## Step 4: Run a headed browser (for interactive login)

For workflows where the user needs to log in themselves, you can render the browser to a virtual display and expose it via VNC. This lets you embed the running browser in a web UI.

Install the VNC stack at runtime (same no-rebuild pattern as per-boot setup):

```typescript
await sandbox.commands.run(
  "sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq xvfb x11vnc novnc websockify",
);

// Start Xvfb → x11vnc → websockify as long-lived exec sessions so they
// persist across individual commands.
const xvfb = await sandbox.exec.start("Xvfb", {
  args: [":99", "-screen", "0", "1280x800x24", "-ac"],
});
await new Promise((r) => setTimeout(r, 1500));

const x11vnc = await sandbox.exec.start("x11vnc", {
  args: ["-display", ":99", "-forever", "-shared", "-nopw", "-rfbport", "5900", "-quiet"],
});

const websockify = await sandbox.exec.start("websockify", {
  args: ["--web=/usr/share/novnc/", "6080", "localhost:5900"],
});
```

Set `DISPLAY=:99` in the sandbox envs when you create it, then open with `--headed`:

```typescript
await run("npx", ["libretto", "open", "https://app.example.com", "--session", "login", "--headed"], {
  cwd: "/home/sandbox",
  env: { ...process.env, DISPLAY: ":99" },
});
```

The VNC WebSocket is available on port 6080 — get its preview URL with `sandbox.getPreviewDomain(6080)` and embed `<iframe src="https://<id>-p6080.<domain>/vnc.html?autoconnect=true">` in your UI. Users click/type directly in the real browser.

***

## Step 5: Persist login across runs

After the user logs in, save the auth state:

```typescript
await run("npx", ["libretto", "save", "app.example.com", "--session", "login"], {
  cwd: "/home/sandbox",
});
// → writes /home/sandbox/.libretto/profiles/app.example.com.json
```

The profile lives on the data disk, which survives sandbox hibernation. For cross-sandbox persistence, [snapshot the sandbox](/sandboxes/snapshots) after login and launch future sandboxes from that warm snapshot — they'll boot already logged in.

***

## Resource and concurrency limits

* **Memory per VM** — default is \~1GB. Each headless Chromium uses \~300-500MB; headed full Chromium uses \~500-800MB. Pass `memoryMB: 16384` to `Sandbox.create` for browser-heavy work.
* **File descriptors** — the default `nofile` limit of 1024 is too low for Chromium (which opens hundreds of FDs). Launch the server process under `sudo bash -c "ulimit -n 65535 && exec ..."` to raise it.
* **Concurrency** — with default memory, run 2-3 browsers concurrently per sandbox. With 16GB and proper limits, 5+ works cleanly.

***

## Choosing a browser automation library

| Library                 | Best for                                          | OC integration                                                                   |
| ----------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------- |
| **Playwright** (direct) | Scripted automation with known selectors          | No special setup beyond this guide                                               |
| **libretto**            | AI-agent-driven snapshots + interactive workflows | Shown above                                                                      |
| **browser-use**         | End-to-end natural-language agents                | Install via `pip install browser-use`; set `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` |
| **Browserbase**         | Production-grade bot-detection bypass             | Use libretto with `--provider browserbase` + API key                             |

All four run inside OC sandboxes; the snapshot recipe above is compatible with all of them.

***

## Troubleshooting

**`407 from proxy after CONNECT`**
Your sandbox isn't registered with the secrets proxy. Attach a `secretStore` with at least one secret entry.

**Chromium exits with status 1 on launch**
Missing shared library. Check `.libretto/sessions/<name>/logs.jsonl` for the specific `.so` missing. Add it to `CHROMIUM_DEPS` in the snapshot or install at runtime.

**`net::ERR_CERT_AUTHORITY_INVALID`**
Chromium doesn't trust OC's egress-proxy CA. Run the `certutil` NSS-import step from "Per-boot setup".

**`ENOTFOUND localhost`**
The guest's `/etc/hosts` is missing the localhost entry. Run the `grep … /etc/hosts` step from "Per-boot setup".

**`EAGAIN` / `uv_thread_create` / `Killed`**
Out of memory or leaked browser processes from prior runs. Reap stale Chromium processes between sessions (`pkill -9 -f chrome-headless-shell`) and reduce `MAX_CONCURRENT_BROWSERS`.

**Preview URL shows "Waiting for server"**
Your server is bound to `127.0.0.1` instead of `0.0.0.0`. OC's edge forwards from the worker interface, not loopback. Set `hostname: "0.0.0.0"` when calling your web server's `listen()`.

**SSE / streaming responses return 502**
OC's preview-URL edge buffers response bodies — streaming chunks don't reach the client. Use short-polling (`POST /api/job` → `GET /api/job/:id`) instead of SSE or WebSockets for progress updates. WebSockets (for VNC, etc.) *do* pass through as upgrade connections; buffering only affects regular HTTP responses.

***

## Next steps

* Read the [libretto docs](https://libretto.sh/docs) for the full CLI + library reference.
* See [Snapshots](/sandboxes/snapshots) for how to checkpoint a warmed-up browser VM.
* See [Secret Stores](/sandboxes/secrets) for scoping egress and sealing real credentials.


---
title: Agent Skill
url: "https://docs-v1.opencomputer.dev/guides/agent-skill.md"
description: Use AI agents to manage cloud sandboxes with natural language
---

The OpenComputer skill lets AI agents like Claude Code create and manage cloud sandboxes using the `oc` CLI. Instead of writing commands yourself, just describe what you want.

## Install

```bash
npx skills add diggerhq/opencomputer
```

This works with Claude Code, Codex, Cursor, and any agent that supports the [Agent Skills](https://agentskills.io) standard.

## Prerequisites

The `oc` CLI must be installed and configured:

```bash
curl -fsSL https://raw.githubusercontent.com/diggerhq/opencomputer/main/scripts/install.sh | bash
oc login
```

If the command prints a browser URL or confirmation code, relay it to the user
and wait for them to approve it. The CLI resumes without asking anyone to paste
an API key into chat.

## Usage

The skill activates automatically when you mention sandboxes or the `oc` CLI. Just ask naturally:

**Create a custom base image:**

> Create a base image for me that includes a placeholder React app I can use as a starter kit. Checkpoint it so I can spawn copies later.

**Fork and experiment:**

> Spawn two sandboxes from my react-starter checkpoint. In the first one, add Tailwind CSS. In the second, add Material UI.

**Run commands:**

> Install PostgreSQL in my sandbox and run the test suite

**Manage state:**

> Checkpoint my sandbox before I try this migration, so I can roll back if it breaks

The agent handles all the `oc` commands under the hood — creating sandboxes, running `exec`, taking checkpoints, applying patches, and cleaning up.


---
title: TypeScript SDK
url: "https://docs-v1.opencomputer.dev/reference/typescript-sdk/overview.md"
description: Complete TypeScript SDK reference
---

## Installation

<Tabs items={["npm", "yarn", "pnpm"]}>
  <Tab title="npm">
    ```bash
    npm install @opencomputer/sdk
    ```
  </Tab>

  <Tab title="yarn">
    ```bash
    yarn add @opencomputer/sdk
    ```
  </Tab>

  <Tab title="pnpm">
    ```bash
    pnpm add @opencomputer/sdk
    ```
  </Tab>
</Tabs>

```typescript
import { Sandbox } from "@opencomputer/sdk";
import { Image, Snapshots } from "@opencomputer/sdk/node";
```

> Note: The SDK is ESM-only. Use Node 18+ with `"type": "module"` (or `.mjs/.mts`) and top-level `await`, or use `const { Sandbox } = await import("@opencomputer/sdk"); const { Image, Snapshots } = await import("@opencomputer/sdk/node");` in CommonJS.

## Modules

<CardGroup cols={2}>
  <Card title="Sandbox" icon="box" href="/reference/typescript-sdk/sandbox">
    Create, connect, and manage sandbox lifecycle
  </Card>

  <Card title="Usage & Tags" icon="gauge" href="/reference/typescript-sdk/usage">
    Query usage, group by tag, and manage sandbox tags
  </Card>

  <Card title="Exec" icon="terminal" href="/reference/typescript-sdk/exec">
    Run commands and manage exec sessions
  </Card>

  <Card title="Sandbox agent" icon="robot" href="/reference/typescript-sdk/agent">
    Start and manage an agent session inside a sandbox (low-level; deprecated)
  </Card>

  <Card title="Filesystem" icon="folder" href="/reference/typescript-sdk/filesystem">
    Read, write, and manage files
  </Card>

  <Card title="PTY" icon="terminal" href="/reference/typescript-sdk/pty">
    Interactive terminal sessions
  </Card>

  <Card title="Image" icon="layer-group" href="/reference/typescript-sdk/image">
    Declarative sandbox image builder
  </Card>

  <Card title="Snapshots" icon="camera" href="/reference/typescript-sdk/snapshots">
    Pre-built sandbox environments
  </Card>

  <Card title="Secret Stores" icon="key" href="/reference/typescript-sdk/secrets">
    Manage secrets and egress control
  </Card>
</CardGroup>


---
title: Sandbox
url: "https://docs-v1.opencomputer.dev/reference/typescript-sdk/sandbox.md"
description: "Create, connect, and manage sandbox lifecycle"
---

## Static Methods

### `Sandbox.create(opts?)`

Create a new sandbox. [HTTP API →](/api-reference/sandboxes/create)

<ParamField body="template" type="string" default="base">
  Template name
</ParamField>

<ParamField body="timeout" type="number" default="300">
  Idle timeout in seconds
</ParamField>

<ParamField body="apiKey" type="string">
  API key (falls back to `OPENCOMPUTER_API_KEY` env var)
</ParamField>

<ParamField body="apiUrl" type="string">
  API URL (falls back to `OPENCOMPUTER_API_URL` env var)
</ParamField>

<ParamField body="envs" type="Record<string, string>">
  Environment variables
</ParamField>

<ParamField body="metadata" type="Record<string, string>">
  Arbitrary metadata
</ParamField>

<ParamField body="burst" type="boolean">
  Create a [Burst Sandbox](/sandboxes/burst-sandboxes). Disk is preserved across infrastructure restarts; processes may restart.
</ParamField>

<ParamField body="cpuCount" type="number">
  CPU cores
</ParamField>

<ParamField body="memoryMB" type="number">
  Memory in MB
</ParamField>

<ParamField body="secretStore" type="string">
  Secret store name — resolves encrypted secrets and egress allowlist
</ParamField>

<ParamField body="image" type="Image">
  Declarative image definition (see [Image](/reference/typescript-sdk/image))
</ParamField>

<ParamField body="snapshot" type="string">
  Name of a pre-built snapshot
</ParamField>

<ParamField body="onBuildLog" type="(log: string) => void">
  Build log callback (when using `image`)
</ParamField>

**Returns:** `Promise<Sandbox>`

```typescript
const sandbox = await Sandbox.create({ template: "my-stack", timeout: 600 });
```

Create a Burst Sandbox:

```typescript
const sandbox = await Sandbox.create({
  burst: true,
  timeout: 300,
});
```

<Note>
  Burst Sandboxes are alpha. They preserve filesystem state across
  infrastructure restarts, may restart running processes, and are priced roughly
  2x cheaper than on-demand sandboxes.
</Note>

***

### `Sandbox.connect(sandboxId, opts?)`

Connect to an existing sandbox. [HTTP API →](/api-reference/sandboxes/get)

<ParamField body="sandboxId" type="string" required>
  Sandbox ID
</ParamField>

<ParamField body="apiKey" type="string">
  API key
</ParamField>

<ParamField body="apiUrl" type="string">
  API URL
</ParamField>

**Returns:** `Promise<Sandbox>`

```typescript
const sandbox = await Sandbox.connect("sb-abc123");
```

***

### `Sandbox.createFromCheckpoint(checkpointId, opts?)`

Create a new sandbox from a checkpoint. [HTTP API →](/api-reference/checkpoints/fork)

<ParamField body="checkpointId" type="string" required>
  Checkpoint ID
</ParamField>

<ParamField body="timeout" type="number" default="300">
  Idle timeout
</ParamField>

**Returns:** `Promise<Sandbox>`

```typescript
const forked = await Sandbox.createFromCheckpoint("cp-abc123");
```

***

### `Sandbox.createCheckpointPatch(checkpointId, opts)`

Create a patch for a checkpoint. [HTTP API →](/api-reference/patches/create)

<ParamField body="checkpointId" type="string" required>
  Target checkpoint
</ParamField>

<ParamField body="script" type="string" required>
  Bash script
</ParamField>

<ParamField body="description" type="string">
  Description
</ParamField>

**Returns:** `Promise<PatchResult>`

```typescript
const result = await Sandbox.createCheckpointPatch("cp-abc", { script: "apt install -y curl" });
```

***

### `Sandbox.listCheckpointPatches(checkpointId, opts?)`

[HTTP API →](/api-reference/patches/list)

**Returns:** `Promise<PatchInfo[]>`

***

### `Sandbox.deleteCheckpointPatch(checkpointId, patchId, opts?)`

[HTTP API →](/api-reference/patches/delete)

**Returns:** `Promise<void>`

***

## Instance Methods

### `sandbox.kill()`

Terminate the sandbox. [HTTP API →](/api-reference/sandboxes/delete)

**Returns:** `Promise<void>`

### `sandbox.isRunning()`

Check if the sandbox is running.

**Returns:** `Promise<boolean>`

### `sandbox.hibernate()`

Snapshot VM state and stop. No compute cost while hibernated. [HTTP API →](/api-reference/sandboxes/hibernate)

**Returns:** `Promise<void>`

### `sandbox.wake(opts?)`

Resume a hibernated sandbox. [HTTP API →](/api-reference/sandboxes/wake)

<ParamField body="timeout" type="number" default="300">
  Idle timeout after wake
</ParamField>

**Returns:** `Promise<void>`

### `sandbox.setTimeout(timeout)`

Update the idle timeout. [HTTP API →](/api-reference/sandboxes/set-timeout)

<ParamField body="timeout" type="number" required>
  New timeout in seconds
</ParamField>

**Returns:** `Promise<void>`

### `sandbox.createCheckpoint(name, opts?)`

Create a named checkpoint. [HTTP API →](/api-reference/checkpoints/create)

<ParamField body="name" type="string" required>
  Checkpoint name (unique per sandbox)
</ParamField>

<ParamField body="opts.retentionPolicy" type="object">
  Optional retention policy. Use `{ mode: "delete_oldest", maxCount: 10 }` for
  full checkpoints or `{ mode: "delete_oldest", maxCount: 100 }` with
  `kind: "disk_only"` to delete the oldest eligible checkpoint of the same type
  before creating a new one.
</ParamField>

**Returns:** `Promise<CheckpointInfo>`

### `sandbox.listCheckpoints()`

[HTTP API →](/api-reference/checkpoints/list) — **Returns:** `Promise<CheckpointInfo[]>`

### `sandbox.restoreCheckpoint(checkpointId)`

Revert in-place to a checkpoint. [HTTP API →](/api-reference/checkpoints/restore)

**Returns:** `Promise<void>`

### `sandbox.deleteCheckpoint(checkpointId)`

[HTTP API →](/api-reference/checkpoints/delete) — **Returns:** `Promise<void>`

### `sandbox.getPreviewDomain(port)`

Get the preview URL domain for a specific port. No API call — constructs the hostname locally.

<ParamField body="port" type="number" required>
  Port number
</ParamField>

**Returns:** `string` (e.g., `sb-abc123-p3000.workers.opencomputer.dev`)

```typescript
const domain = sandbox.getPreviewDomain(3000);
console.log(`https://${domain}`);
```

***

### `sandbox.downloadUrl(path, opts?)`

Generate a signed download URL. [HTTP API →](/api-reference/files/generate-download-url) · [Guide →](/sandboxes/signed-urls)

<ParamField body="path" type="string" required>
  Absolute path to the file
</ParamField>

<ParamField body="opts.expiresIn" type="number" default="3600">
  URL lifetime in seconds (max: 86400)
</ParamField>

**Returns:** `Promise<string>`

### `sandbox.uploadUrl(path, opts?)`

Generate a signed upload URL. [HTTP API →](/api-reference/files/generate-upload-url) · [Guide →](/sandboxes/signed-urls)

<ParamField body="path" type="string" required>
  Absolute path for the destination file
</ParamField>

<ParamField body="opts.expiresIn" type="number" default="3600">
  URL lifetime in seconds (max: 86400)
</ParamField>

**Returns:** `Promise<string>`

### `sandbox.createPreviewURL(opts)`

[HTTP API →](/api-reference/preview/create)

<ParamField body="port" type="number" required>
  Container port (1–65535)
</ParamField>

<ParamField body="domain" type="string">
  Custom domain
</ParamField>

<ParamField body="authConfig" type="Record<string, unknown>">
  Auth configuration
</ParamField>

**Returns:** `Promise<PreviewURLResult>`

### `sandbox.listPreviewURLs()`

[HTTP API →](/api-reference/preview/list) — **Returns:** `Promise<PreviewURLResult[]>`

### `sandbox.deletePreviewURL(port)`

[HTTP API →](/api-reference/preview/delete) — **Returns:** `Promise<void>`

***

### `sandbox.getAllowedHosts()`

Return the egress allowlist + per-secret allowed hosts the sandbox's secrets proxy enforces. Useful for debugging "why is my outbound HTTP call being blocked" without having to cross-reference the [secret store](/sandboxes/secrets) config separately.

Sandboxes created without a `secretStore` option return an empty allowlist with `secretStore` undefined — the sandbox has no per-store egress restriction.

**Returns:** `Promise<AllowedHostsInfo>`

```typescript
const info = await sandbox.getAllowedHosts();
console.log(info.egressAllowlist);          // ['api.openai.com', 'api.anthropic.com']
console.log(info.perSecretAllowedHosts);    // { 'OPENAI_API_KEY': ['api.openai.com'] }
```

The returned `AllowedHostsInfo`:

<ParamField body="sandboxID" type="string">
  Sandbox ID
</ParamField>

<ParamField body="secretStore" type="string">
  Name of the primary secret store the sandbox is bound to (the "winning" store on the row, whose secrets shadow the base store on env-name collisions). Empty when the sandbox was created without a `secretStore` option.
</ParamField>

<ParamField body="baseSecretStore" type="string">
  Name of the inherited parent store, present only when a fork layered an additional `secretStore` on top of an inherited one. Empty for sandboxes created from scratch and for forks without an override.
</ParamField>

<ParamField body="egressAllowlist" type="string[]">
  Hosts the sandbox can reach via the secrets proxy. For layered forks this is the **union** of every layered store's allowlist (base first, primary's additions appended, deduped) — matches what the runtime proxy actually enforces.
</ParamField>

<ParamField body="perSecretAllowedHosts" type="Record<string, string[]>">
  Optional per-secret restrictions. When the sandbox uses a listed secret in a request, only the listed hosts are reachable for that request. For layered forks, the primary store's secrets shadow the base on name collisions (matches runtime env-collision behavior).
</ParamField>

***

## Properties

<ParamField body="sandboxId" type="string">
  Sandbox ID (readonly)
</ParamField>

<ParamField body="status" type="string">
  Current status (readonly)
</ParamField>

<ParamField body="domain" type="string">
  Preview URL domain for port 80 (e.g., `sb-abc123-p80.workers.opencomputer.dev`). Returns empty string if sandbox domain is not configured.
</ParamField>

<ParamField body="agent" type="SandboxAgent">
  [Sandbox agent sessions](/reference/typescript-sdk/agent)
</ParamField>

<ParamField body="exec" type="Exec">
  [Command execution](/reference/typescript-sdk/exec)
</ParamField>

<ParamField body="files" type="Filesystem">
  [File operations](/reference/typescript-sdk/filesystem)
</ParamField>

<ParamField body="pty" type="Pty">
  [Terminal sessions](/reference/typescript-sdk/pty)
</ParamField>

<ParamField body="commands" type="Exec">
  **Deprecated** — alias for `exec`
</ParamField>

***

## Types

<AccordionGroup>
  <Accordion title="SandboxOpts">
    ```typescript
    interface SandboxOpts {
      template?: string;
      timeout?: number;
      apiKey?: string;
      apiUrl?: string;
      envs?: Record<string, string>;
      metadata?: Record<string, string>;
      cpuCount?: number;
      memoryMB?: number;
      secretStore?: string;
      image?: Image;
      snapshot?: string;
      onBuildLog?: (log: string) => void;
    }
    ```
  </Accordion>
</AccordionGroup>

<AccordionGroup>
  <Accordion title="CheckpointInfo">
    ```typescript
    interface CheckpointInfo {
      id: string;
      sandboxId: string;
      orgId: string;
      name: string;
      sandboxConfig: object;
      status: string;       // "processing" | "ready" | "failed"
      sizeBytes: number;
      createdAt: string;
    }
    ```
  </Accordion>
</AccordionGroup>

<AccordionGroup>
  <Accordion title="PatchInfo / PatchResult">
    ```typescript
    interface PatchInfo {
      id: string;
      checkpointId: string;
      sequence: number;
      script: string;
      description: string;
      strategy: string;     // "on_wake"
      createdAt: string;
    }

    interface PatchResult {
      patch: PatchInfo;
    }
    ```
  </Accordion>
</AccordionGroup>

<AccordionGroup>
  <Accordion title="PreviewURLResult">
    ```typescript
    interface PreviewURLResult {
      id: string;
      sandboxId: string;
      orgId: string;
      hostname: string;
      customHostname?: string;
      port: number;
      cfHostnameId?: string;
      sslStatus: string;
      authConfig?: object;
      createdAt: string;
    }
    ```
  </Accordion>
</AccordionGroup>


---
title: Scaling
url: "https://docs-v1.opencomputer.dev/reference/typescript-sdk/scaling.md"
description: "Resize a sandbox manually, autoscale on memory pressure, or freeze its resources."
---

OpenComputer sandboxes can change size at runtime. There are three knobs:

| What you want                       | Use                                                  |
| ----------------------------------- | ---------------------------------------------------- |
| Resize once, predictably            | [`scale()`](#sandbox-scale-opts)                     |
| Track memory pressure automatically | [`setAutoscale()`](#sandbox-setautoscale-opts)       |
| Freeze the current size             | [`setScalingLock()`](#sandbox-setscalinglock-locked) |

CPU follows memory per the platform's tier table. You don't pick CPU separately.

## How the three interact

* **Manual `scale()`** disables autoscale on the sandbox as a side effect — explicit user intent overrides the loop. Re-enable autoscale with `setAutoscale({ enabled: true, ... })` after if you want.
* **Setting a scaling lock** disables autoscale at the same time (single-knob: "I don't want this scaling, period"). While locked, both `scale()` and `setAutoscale({ enabled: true })` reject with `ScalingLockedError`. Unlocking does NOT auto-re-enable autoscale.
* **Plan caps** apply everywhere. Free-tier orgs are capped at 4 GB. Calls above the cap throw `PlanLimitError`.

***

## `sandbox.scale(opts)`

Manually resize the sandbox. [HTTP API →](/api-reference/sandboxes/scale)

<ParamField body="memoryMB" type="number" required>
  Target memory in MB.
</ParamField>

**Returns:** `Promise<{ sandboxID: string; memoryMB: number; cpuPercent: number }>`

**Throws:**

* `ScalingLockedError` — sandbox has a scaling lock active.
* `PlanLimitError` — `memoryMB` exceeds the org's plan cap.

```typescript
import { Sandbox, ScalingLockedError, PlanLimitError } from "@opencomputer/sdk";

const sandbox = await Sandbox.connect("sb-abc123");

try {
  const result = await sandbox.scale({ memoryMB: 8192 });
  console.log(`scaled to ${result.memoryMB}MB / ${result.cpuPercent}% CPU`);
} catch (err) {
  if (err instanceof ScalingLockedError) {
    console.warn("sandbox is locked — unlock to scale");
  } else if (err instanceof PlanLimitError) {
    console.warn("upgrade required for larger instances");
  } else {
    throw err;
  }
}
```

***

## `sandbox.setAutoscale(opts)`

Enable or disable per-sandbox autoscale.

<ParamField body="enabled" type="boolean" required>
  Whether autoscale should be active.
</ParamField>

<ParamField body="minMemoryMB" type="number">
  Lower bound when `enabled=true`.
</ParamField>

<ParamField body="maxMemoryMB" type="number">
  Upper bound when `enabled=true`. Must be ≥ `minMemoryMB`.
</ParamField>

**Returns:** `Promise<{ sandboxID: string; enabled: boolean; minMemoryMB: number; maxMemoryMB: number }>`

**Throws:**

* `ScalingLockedError` — sandbox has a scaling lock active.
* `PlanLimitError` — `maxMemoryMB` exceeds the org's plan cap.

When `enabled=true`, the platform watches the sandbox's memory pressure and resizes it within the bounds:

* **Scale up** on a single 1-min sample above 75% memory utilization. Cooldown 60s between up-scales.
* **Scale down** only when 1-min, 5-min, AND 15-min averages all sit below 25%. Cooldown 5 min between down-scales.

The asymmetry matches user perception: rapid response when the user notices lag, conservative shrink after sustained idle. The 15-min window is the dominant constraint on shrink, so a sandbox that briefly idles and resumes won't sawtooth.

```typescript
await sandbox.setAutoscale({
  enabled: true,
  minMemoryMB: 1024,
  maxMemoryMB: 16384,
});
```

To turn off:

```typescript
await sandbox.setAutoscale({ enabled: false });
```

***

## `sandbox.getAutoscale()`

Get the current autoscale configuration.

**Returns:** `Promise<{ sandboxID: string; enabled: boolean; minMemoryMB: number; maxMemoryMB: number }>`

```typescript
const cfg = await sandbox.getAutoscale();
if (cfg.enabled) {
  console.log(`autoscale ${cfg.minMemoryMB}–${cfg.maxMemoryMB} MB`);
}
```

***

## `sandbox.setScalingLock(locked)`

Lock or unlock the sandbox's resources against any size change.

<ParamField body="locked" type="boolean" required>
  `true` to freeze, `false` to allow scaling again.
</ParamField>

**Returns:** `Promise<{ sandboxID: string; locked: boolean }>`

While locked:

* `scale()` rejects with `ScalingLockedError`.
* `setAutoscale({ enabled: true })` rejects with `ScalingLockedError`.
* The platform autoscaler skips this sandbox entirely.

Locking ALSO disables autoscale (single knob — "I don't want this scaling"). Unlocking does **not** re-enable autoscale; call `setAutoscale({ enabled: true, ... })` explicitly if you want it back.

```typescript
// Pin a sandbox at its current size during a critical workload
await sandbox.setScalingLock(true);

// ...

// Allow scaling again after
await sandbox.setScalingLock(false);
```

***

## `sandbox.getScalingLock()`

Get the current scaling-lock state.

**Returns:** `Promise<{ sandboxID: string; locked: boolean }>`

***

## Errors

### `ScalingLockedError`

Thrown when the sandbox has a scaling lock active. Has a `code` property of `"scaling_locked"` for parity with the HTTP API's error code.

```typescript
import { ScalingLockedError } from "@opencomputer/sdk";

try {
  await sandbox.scale({ memoryMB: 8192 });
} catch (err) {
  if (err instanceof ScalingLockedError) {
    // unlock first, or surface to user
  }
}
```

### `PlanLimitError`

Thrown when the requested size exceeds the org's plan cap. The HTTP API returns 402 Payment Required for this case.


---
title: "Usage & Tags"
url: "https://docs-v1.opencomputer.dev/reference/typescript-sdk/usage.md"
description: "Query sandbox usage, group by tag, and manage sandbox tags"
---

Use `Usage` for billing-aligned usage queries and per-sandbox
time-series drilldowns. Use `Tags` to set and read sandbox tags used
by the usage aggregator.

<Note>
  **Preview:** Usage and tag APIs are new. Endpoints, response fields,
  and SDK method names may change before GA; temporary inaccuracies or
  rough edges are possible while the surface settles.
</Note>

```typescript
import { Usage, Tags } from "@opencomputer/sdk";

const usage = new Usage("https://app.opencomputer.dev", process.env.OPENCOMPUTER_API_KEY!);
const tags = new Tags("https://app.opencomputer.dev", process.env.OPENCOMPUTER_API_KEY!);
```

The `apiUrl` may include or omit `/api`; both
`https://app.opencomputer.dev` and `https://app.opencomputer.dev/api`
work.

## `usage.forSandbox(sandboxId, opts?)`

Return 1-minute memory points plus envelope totals for one sandbox.
[HTTP API ->](/api-reference/usage/get-sandbox-usage)

<ParamField body="sandboxId" type="string" required>
  Sandbox ID.
</ParamField>

<ParamField body="from" type="string">
  ISO date (`YYYY-MM-DD`) or RFC3339 lower bound. Default: now minus
  1 hour.
</ParamField>

<ParamField body="to" type="string">
  ISO date (`YYYY-MM-DD`) or RFC3339 upper bound. Default: now.
  Window must be at most 30 days.
</ParamField>

**Returns:** `Promise<SandboxUsageResponse>`

```typescript
const r = await usage.forSandbox("sb-abc123", {
  from: "2026-05-27T00:00:00Z",
  to: "2026-05-27T01:00:00Z",
});

const utilization =
  r.totals.memoryAllocatedGbSeconds === 0
    ? 0
    : r.totals.memoryUsedGbSeconds / r.totals.memoryAllocatedGbSeconds;

console.log(`${(utilization * 100).toFixed(0)}% memory utilization`);

for (const point of r.points) {
  // point.ts, point.allocatedMemoryMb, point.usedMemoryMbAvg
  // are ready to render as chart series.
}
```

## `usage.bySandbox(opts?)`

Rank sandboxes by usage for an organization. [HTTP API ->](/api-reference/usage/get-usage)

<ParamField body="from" type="string">
  ISO date (`YYYY-MM-DD`) or RFC3339 lower bound. Default: now minus
  30 days.
</ParamField>

<ParamField body="to" type="string">
  ISO date (`YYYY-MM-DD`) or RFC3339 upper bound. Default: now.
  Window must be at most 90 days.
</ParamField>

<ParamField body="filter" type="Record<string, string>">
  Tag filters. Keys are dimensions such as `"tag:team"`. Values are
  exact tag values, comma-separated OR values, or `""` to match
  sandboxes without that tag key.
</ParamField>

<ParamField body="sort" type="string">
  `"-memoryGbSeconds"` (default) or `"-diskOverageGbSeconds"`.
</ParamField>

<ParamField body="limit" type="number">
  Max rows per page. Default 50, max 500.
</ParamField>

<ParamField body="cursor" type="string">
  Cursor returned by the previous page's `nextCursor`.
</ParamField>

**Returns:** `Promise<UsageBySandboxResponse>`

```typescript
const page = await usage.bySandbox({
  from: "2026-05-01T00:00:00Z",
  to: "2026-06-01T00:00:00Z",
  filter: { "tag:team": "payments" },
  limit: 20,
});

for (const item of page.items) {
  console.log(item.sandboxId, item.memoryGbSeconds, item.tags);
}

if (page.nextCursor) {
  const next = await usage.bySandbox({
    from: "2026-05-01T00:00:00Z",
    to: "2026-06-01T00:00:00Z",
    filter: { "tag:team": "payments" },
    cursor: page.nextCursor,
  });
}
```

## `usage.byTag(tagKey, opts?)`

Group usage by the value of one tag key. Tag keys may contain `:`.
[HTTP API ->](/api-reference/usage/get-usage)

<ParamField body="tagKey" type="string" required>
  Tag key to group by, for example `"team"` or `"team:subsystem"`.
</ParamField>

<ParamField body="filter" type="Record<string, string>">
  Optional tag filters applied before grouping.
</ParamField>

<ParamField body="from" type="string">
  ISO date (`YYYY-MM-DD`) or RFC3339 lower bound. Default: now minus
  30 days.
</ParamField>

<ParamField body="to" type="string">
  ISO date (`YYYY-MM-DD`) or RFC3339 upper bound. Default: now.
  Window must be at most 90 days.
</ParamField>

<ParamField body="sort" type="string">
  `"-memoryGbSeconds"` (default) or `"-diskOverageGbSeconds"`.
</ParamField>

<ParamField body="limit" type="number">
  Max rows per page. Default 50, max 500.
</ParamField>

<ParamField body="cursor" type="string">
  Cursor returned by the previous page's `nextCursor`.
</ParamField>

**Returns:** `Promise<UsageByTagResponse>`

```typescript
const byTeam = await usage.byTag("team", {
  filter: { "tag:env": "prod" },
});

for (const row of byTeam.items) {
  console.log(row.tagValue, row.memoryGbSeconds, row.sandboxCount);
}

console.log("untagged", byTeam.untagged.memoryGbSeconds);

const bySubsystem = await usage.byTag("team:subsystem");
```

## `tags.set(sandboxId, tags)`

Replace the full tag set for a sandbox. Partial updates are not
supported: read, modify, then write the full map. [HTTP API ->](/api-reference/sandboxes/set-tags)

<ParamField body="sandboxId" type="string" required>
  Sandbox ID.
</ParamField>

<ParamField body="tags" type="Record<string, string>" required>
  Flat tag map. `{}` clears all tags.
</ParamField>

**Returns:** `Promise<{ tags: Record<string, string>; tagsLastUpdatedAt: string | null }>`

```typescript
await tags.set("sb-abc123", {
  team: "payments",
  env: "prod",
  "team:subsystem": "checkout",
});
```

## `tags.get(sandboxId)`

Read the current tag set for a sandbox. [HTTP API ->](/api-reference/sandboxes/get-tags)

**Returns:** `Promise<{ tags: Record<string, string>; tagsLastUpdatedAt: string | null }>`

```typescript
const current = await tags.get("sb-abc123");
console.log(current.tags);
```

## `tags.listKeys()`

List tag keys seen in the organization, with counts for tagged
sandboxes and distinct values. [HTTP API ->](/api-reference/usage/list-tags)

**Returns:** `Promise<TagKeyInfo[]>`

```typescript
const keys = await tags.listKeys();
for (const key of keys) {
  console.log(key.key, key.sandboxCount, key.valueCount);
}
```

## Units

All `*GbSeconds` fields are **GiB-seconds**. For example, a sandbox
provisioned at `4096` MiB for one minute reports `240`
`memoryAllocatedGbSeconds`.

`memoryAllocatedGbSeconds` is billing-aligned provisioned memory.
`memoryUsedGbSeconds` is measured resident memory from the worker
collector, so it can lag by a few minutes after a sandbox starts.

## Types

<AccordionGroup>
  <Accordion title="SandboxUsageResponse">
    ```typescript
    interface SandboxUsageResponse {
      sandboxId: string;
      alias?: string;
      from: string;
      to: string;
      totals: SandboxUsageTotals;
      points: SandboxUsagePoint[];
    }
    ```
  </Accordion>
</AccordionGroup>

<AccordionGroup>
  <Accordion title="SandboxUsagePoint">
    ```typescript
    interface SandboxUsagePoint {
      ts: string;
      memoryAllocatedGbSeconds: number;
      memoryUsedGbSeconds: number;
      uptimeSeconds: number;
      allocatedMemoryMb: number;
      usedMemoryMbAvg: number;
      usedMemoryMbPeak: number;
    }
    ```
  </Accordion>
</AccordionGroup>

<AccordionGroup>
  <Accordion title="SandboxUsageTotals">
    ```typescript
    interface SandboxUsageTotals {
      memoryAllocatedGbSeconds: number;
      memoryUsedGbSeconds: number;
      uptimeSeconds: number;
      memoryAllocatedPeakMb: number;
      memoryUsedPeakMb: number;
    }
    ```
  </Accordion>
</AccordionGroup>

<AccordionGroup>
  <Accordion title="UsageSandboxItem">
    ```typescript
    interface UsageSandboxItem {
      sandboxId: string;
      alias?: string;
      status?: string;
      tags: Record<string, string>;
      tagsLastUpdatedAt: string | null;
      memoryGbSeconds: number;
      diskOverageGbSeconds: number;
    }
    ```
  </Accordion>
</AccordionGroup>

<AccordionGroup>
  <Accordion title="UsageTagItem">
    ```typescript
    interface UsageTagItem {
      tagKey: string;
      tagValue: string;
      memoryGbSeconds: number;
      diskOverageGbSeconds: number;
      sandboxCount: number;
    }
    ```
  </Accordion>
</AccordionGroup>


---
title: Exec
url: "https://docs-v1.opencomputer.dev/reference/typescript-sdk/exec.md"
description: Run commands and manage exec sessions
---

Accessed via `sandbox.exec`.

## `sandbox.exec.run(command, opts?)`

Run a command synchronously. [HTTP API →](/api-reference/exec/run)

<ParamField body="command" type="string" required>
  Shell command
</ParamField>

<ParamField body="timeout" type="number" default="60">
  Timeout in seconds
</ParamField>

<ParamField body="env" type="Record<string, string>">
  Environment variables
</ParamField>

<ParamField body="cwd" type="string">
  Working directory
</ParamField>

**Returns:** `Promise<ProcessResult>`

```typescript
const result = await sandbox.exec.run("npm test", { cwd: "/app" });
```

***

## `sandbox.exec.start(command, opts?)`

Start a long-running command with streaming. [HTTP API →](/api-reference/exec/create-session)

<ParamField body="command" type="string" required>
  Command
</ParamField>

<ParamField body="args" type="string[]">
  Arguments
</ParamField>

<ParamField body="env" type="Record<string, string>">
  Environment variables
</ParamField>

<ParamField body="cwd" type="string">
  Working directory
</ParamField>

<ParamField body="timeout" type="number">
  Timeout in seconds
</ParamField>

<ParamField body="maxRunAfterDisconnect" type="number">
  Seconds to keep running after disconnect
</ParamField>

<ParamField body="onStdout" type="(data: Uint8Array) => void">
  Stdout callback
</ParamField>

<ParamField body="onStderr" type="(data: Uint8Array) => void">
  Stderr callback
</ParamField>

<ParamField body="onExit" type="(exitCode: number) => void">
  Exit callback
</ParamField>

**Returns:** `Promise<ExecSession>`

```typescript
const session = await sandbox.exec.start("node server.js", {
  onStdout: (data) => process.stdout.write(data),
});
```

***

## `sandbox.exec.background(command, opts?)`

Alias for [`start`](#sandbox-exec-start-command-opts). Same options, same return type. Use when the intent is "run this in the background and observe it" — more discoverable than `start` for that case.

```typescript
const server = await sandbox.exec.background("npm", {
  args: ["run", "dev"],
  onStdout: (data) => process.stdout.write(data),
});
```

***

## `sandbox.exec.shell(opts?)`

Open a stateful shell session. Subsequent `.run()` calls share the same bash process, so `cwd`, exported env vars, and shell functions persist — the ergonomics of a terminal tab.

Backed by a long-running `bash --noprofile --norc` session. Foreground-only: concurrent `.run()` rejects with `ShellBusyError`. Use [`background`](#sandbox-exec-background-command-opts) for fire-and-forget processes.

<ParamField body="cwd" type="string">
  Initial working directory
</ParamField>

<ParamField body="env" type="Record<string, string>">
  Initial environment variables
</ParamField>

**Returns:** `Promise<Shell>`

```typescript
const sh = await sandbox.exec.shell({ cwd: "/app" });

await sh.run("npm install");
await sh.run("export NODE_ENV=test");
const r = await sh.run("npm test", {
  onStdout: (b) => process.stdout.write(b),
});
console.log(r.exitCode);

await sh.close();
```

### Shell

| Member            | Type                     | Description                                 |
| ----------------- | ------------------------ | ------------------------------------------- |
| `sessionId`       | `string`                 | Underlying exec session ID                  |
| `run(cmd, opts?)` | `Promise<ProcessResult>` | Run a command, blocking until it exits      |
| `close()`         | `Promise<void>`          | Write `exit` and wait for bash to terminate |

### ShellRunOpts

| Parameter  | Type                         | Description              |
| ---------- | ---------------------------- | ------------------------ |
| `onStdout` | `(data: Uint8Array) => void` | Per-call stdout callback |
| `onStderr` | `(data: Uint8Array) => void` | Per-call stderr callback |

<Note>
  Per-call `cwd`, `env`, and `timeout` are intentionally not supported in v1. Use inline shell syntax (`cd /x && cmd`, `FOO=bar cmd`) — the shell state carries across calls. Timeouts will land once we have a "signal foreground job" primitive.
</Note>

Errors:

* `ShellBusyError` — another `run()` is in flight (shell is foreground-only).
* `ShellClosedError` — bash has exited (explicit `close()`, a command ran `exit`, or the session dropped).

***

## `sandbox.exec.attach(sessionId, opts?)`

Reconnect to a running exec session.

<ParamField body="sessionId" type="string" required>
  Session ID
</ParamField>

<ParamField body="onStdout" type="(data: Uint8Array) => void">
  Stdout callback
</ParamField>

<ParamField body="onStderr" type="(data: Uint8Array) => void">
  Stderr callback
</ParamField>

<ParamField body="onExit" type="(exitCode: number) => void">
  Exit callback
</ParamField>

<ParamField body="onScrollbackEnd" type="() => void">
  Scrollback replay done
</ParamField>

**Returns:** `Promise<ExecSession>`

***

## `sandbox.exec.list()`

List all exec sessions. [HTTP API →](/api-reference/exec/list-sessions)

**Returns:** `Promise<ExecSessionInfo[]>`

***

## `sandbox.exec.kill(sessionId, signal?)`

Kill an exec session. Default signal: `9` (SIGKILL). [HTTP API →](/api-reference/exec/kill-session)

**Returns:** `Promise<void>`

***

## ExecSession

| Member            | Type              | Description                       |
| ----------------- | ----------------- | --------------------------------- |
| `sessionId`       | `string`          | Session ID                        |
| `done`            | `Promise<number>` | Resolves with exit code           |
| `sendStdin(data)` | method            | Send input (string or Uint8Array) |
| `kill(signal?)`   | `Promise<void>`   | Kill process                      |
| `close()`         | method            | Close WebSocket                   |

***

## Types

<AccordionGroup>
  <Accordion title="ProcessResult">
    ```typescript
    interface ProcessResult {
      exitCode: number;
      stdout: string;
      stderr: string;
    }
    ```
  </Accordion>
</AccordionGroup>

<AccordionGroup>
  <Accordion title="ExecSessionInfo">
    ```typescript
    interface ExecSessionInfo {
      sessionID: string;
      sandboxID: string;
      command: string;
      args: string[];
      running: boolean;
      exitCode?: number;
      startedAt: string;
      attachedClients: number;
    }
    ```
  </Accordion>
</AccordionGroup>


---
title: Filesystem
url: "https://docs-v1.opencomputer.dev/reference/typescript-sdk/filesystem.md"
description: "Read, write, and manage files"
---

Accessed via `sandbox.files`.

## `sandbox.files.read(path)`

Read a file as a UTF-8 string. [HTTP API →](/api-reference/files/read)

<ParamField body="path" type="string" required>
  Absolute path to the file
</ParamField>

**Returns:** `Promise<string>`

***

## `sandbox.files.readBytes(path)`

Read a file as raw bytes.

<ParamField body="path" type="string" required>
  Absolute path to the file
</ParamField>

**Returns:** `Promise<Uint8Array>`

***

## `sandbox.files.write(path, content)`

Write content to a file. [HTTP API →](/api-reference/files/write)

<ParamField body="path" type="string" required>
  Absolute path for the destination file
</ParamField>

<ParamField body="content" type="string | Uint8Array" required>
  File content
</ParamField>

**Returns:** `Promise<void>`

***

## `sandbox.files.list(path?)`

List directory contents. [HTTP API →](/api-reference/files/list-directory)

<ParamField body="path" type="string" default="/">
  Directory path
</ParamField>

**Returns:** `Promise<EntryInfo[]>`

***

## `sandbox.files.makeDir(path)`

Create a directory (recursive). [HTTP API →](/api-reference/files/mkdir)

<ParamField body="path" type="string" required>
  Directory path
</ParamField>

**Returns:** `Promise<void>`

***

## `sandbox.files.remove(path)`

Delete a file or directory. [HTTP API →](/api-reference/files/delete)

<ParamField body="path" type="string" required>
  Path to remove
</ParamField>

**Returns:** `Promise<void>`

***

## `sandbox.files.readStream(path)`

Stream a file as a `ReadableStream`. Avoids buffering the entire file in memory — ideal for large files (50MB+).

<ParamField body="path" type="string" required>
  Absolute path to the file
</ParamField>

**Returns:** `Promise<ReadableStream<Uint8Array>>`

***

## `sandbox.files.writeStream(path, stream)`

Stream content to a file. Accepts a `ReadableStream` or `Uint8Array` without buffering an extra copy in memory.

<ParamField body="path" type="string" required>
  Absolute path for the destination file
</ParamField>

<ParamField body="stream" type="ReadableStream<Uint8Array> | Uint8Array" required>
  File content as a stream or byte array
</ParamField>

**Returns:** `Promise<void>`

***

## `sandbox.files.exists(path)`

Check if a path exists. Client-side wrapper — attempts a read and returns `false` on error.

<ParamField body="path" type="string" required>
  Path to check
</ParamField>

**Returns:** `Promise<boolean>`

***

## Types

<AccordionGroup>
  <Accordion title="EntryInfo">
    ```typescript
    interface EntryInfo {
      name: string;
      isDir: boolean;
      path: string;
      size: number;
    }
    ```
  </Accordion>
</AccordionGroup>


---
title: PTY
url: "https://docs-v1.opencomputer.dev/reference/typescript-sdk/pty.md"
description: Interactive terminal sessions
---

Accessed via `sandbox.pty`.

## `sandbox.pty.create(opts?)`

Create an interactive terminal session. [HTTP API →](/api-reference/pty/create)

<ParamField body="cols" type="number" default="80">
  Terminal columns
</ParamField>

<ParamField body="rows" type="number" default="24">
  Terminal rows
</ParamField>

<ParamField body="onOutput" type="(data: Uint8Array) => void">
  Output callback
</ParamField>

**Returns:** `Promise<PtySession>`

***

## PtySession

| Member       | Type     | Description                       |
| ------------ | -------- | --------------------------------- |
| `sessionId`  | `string` | Session ID                        |
| `send(data)` | method   | Send input (string or Uint8Array) |
| `close()`    | method   | Close the PTY                     |


---
title: Image
url: "https://docs-v1.opencomputer.dev/reference/typescript-sdk/image.md"
description: Declarative sandbox image builder
---

Fluent, immutable builder for declarative sandbox images. Each method returns a new `Image` instance.

## `Image.base()`

Start from the default OpenSandbox environment (Ubuntu 22.04, Python, Node.js, build tools).

```typescript
import { Image } from "@opencomputer/sdk";

const image = Image.base()
  .aptInstall(['curl', 'jq'])
  .pipInstall(['requests', 'pandas'])
  .env({ PROJECT_ROOT: '/workspace' })
  .workdir('/workspace');
```

**Returns:** `Image`

***

## Builder Methods

### `image.aptInstall(packages)`

<ParamField body="packages" type="string[]" required>
  System packages to install via apt-get
</ParamField>

**Returns:** `Image`

***

### `image.pipInstall(packages)`

<ParamField body="packages" type="string[]" required>
  Python packages to install via pip
</ParamField>

**Returns:** `Image`

***

### `image.runCommands(...cmds)`

<ParamField body="cmds" type="string[]" required>
  Shell commands to run during build
</ParamField>

**Returns:** `Image`

***

### `image.env(vars)`

<ParamField body="vars" type="Record<string, string>" required>
  Environment variables to set
</ParamField>

**Returns:** `Image`

***

### `image.workdir(path)`

<ParamField body="path" type="string" required>
  Default working directory
</ParamField>

**Returns:** `Image`

***

### `image.addFile(remotePath, content)`

<ParamField body="remotePath" type="string" required>
  Destination path in the image
</ParamField>

<ParamField body="content" type="string" required>
  File content
</ParamField>

**Returns:** `Image`

***

### `image.addLocalFile(localPath, remotePath)`

<ParamField body="localPath" type="string" required>
  Path to the local file
</ParamField>

<ParamField body="remotePath" type="string" required>
  Destination path in the image
</ParamField>

**Returns:** `Image`

***

### `image.addLocalDir(localPath, remotePath)`

<ParamField body="localPath" type="string" required>
  Path to the local directory
</ParamField>

<ParamField body="remotePath" type="string" required>
  Destination path in the image
</ParamField>

**Returns:** `Image`

### `image.builderMemory(mb)`

Sets the RAM (MB) used during the build phase. Raise this when a build OOMs
(heavy `apt`/`pip`/`npm`). Defaults to **4096**. Does **not** affect the resulting
sandbox's memory — size that at create time via `memoryMB`.

<ParamField body="mb" type="number" required>
  Build-phase memory in MB
</ParamField>

**Returns:** `Image`

***

## Utility Methods

### `image.toJSON()`

Returns the image manifest as a plain object.

**Returns:** `ImageManifest`

### `image.cacheKey()`

Computes a deterministic SHA-256 hash of the manifest for cache lookups.

**Returns:** `string`


---
title: Snapshots
url: "https://docs-v1.opencomputer.dev/reference/typescript-sdk/snapshots.md"
description: Pre-built sandbox environments
---

Standalone class — not a property on Sandbox.

## Constructor

```typescript
import { Snapshots } from "@opencomputer/sdk";

const snapshots = new Snapshots();
// or with explicit config:
const snapshots = new Snapshots({ apiKey: "...", apiUrl: "..." });
```

<ParamField body="apiKey" type="string">
  API key (falls back to `OPENCOMPUTER_API_KEY` env var)
</ParamField>

<ParamField body="apiUrl" type="string">
  API URL (falls back to `OPENCOMPUTER_API_URL` env var)
</ParamField>

***

## `snapshots.create(opts)`

Create a pre-built snapshot from a declarative image. [HTTP API →](/api-reference/snapshots/create)

<ParamField body="name" type="string" required>
  Unique snapshot name
</ParamField>

<ParamField body="image" type="Image" required>
  Declarative image definition
</ParamField>

<ParamField body="onBuildLogs" type="(log: string) => void">
  Build log streaming callback
</ParamField>

**Returns:** `Promise<SnapshotInfo>`

***

## `snapshots.list()`

List all snapshots. [HTTP API →](/api-reference/snapshots/list)

**Returns:** `Promise<SnapshotInfo[]>`

***

## `snapshots.get(name)`

Get a snapshot by name. [HTTP API →](/api-reference/snapshots/get)

<ParamField body="name" type="string" required>
  Snapshot name
</ParamField>

**Returns:** `Promise<SnapshotInfo>`

***

## `snapshots.delete(name)`

Delete a snapshot. Existing sandboxes created from it are not affected. [HTTP API →](/api-reference/snapshots/delete)

<ParamField body="name" type="string" required>
  Snapshot name
</ParamField>

**Returns:** `Promise<void>`

***

## Types

<AccordionGroup>
  <Accordion title="SnapshotInfo">
    ```typescript
    interface SnapshotInfo {
      id: string;
      name: string;
      status: string;       // "building" | "ready" | "failed"
      contentHash: string;
      checkpointId: string;
      manifest: object;
      createdAt: string;
      lastUsedAt: string;
    }
    ```
  </Accordion>
</AccordionGroup>


---
title: Secret Stores
url: "https://docs-v1.opencomputer.dev/reference/typescript-sdk/secrets.md"
description: Manage secrets and egress control
---

## `SecretStore.create(opts)`

Create a new secret store.

```typescript
import { SecretStore } from '@opencomputer/sdk';

const store = await SecretStore.create({
  name: 'my-agent-secrets',
  egressAllowlist: ['api.anthropic.com'],
});
```

<ParamField body="name" type="string" required>
  Store name (unique per organization)
</ParamField>

<ParamField body="egressAllowlist" type="string[]">
  Allowed egress hosts (e.g. `["api.anthropic.com"]`)
</ParamField>

**Returns:** `Promise<SecretStoreInfo>`

***

## `SecretStore.list(opts?)`

List all secret stores.

**Returns:** `Promise<SecretStoreInfo[]>`

***

## `SecretStore.get(storeId, opts?)`

Get a secret store by ID.

<ParamField body="storeId" type="string" required>
  UUID of the secret store
</ParamField>

**Returns:** `Promise<SecretStoreInfo>`

***

## `SecretStore.update(storeId, opts)`

Partial updates — only the fields you pass are changed.

```typescript
const updated = await SecretStore.update('store-uuid', {
  name: 'new-name',
  egressAllowlist: ['api.anthropic.com', '*.openai.com'],
});
```

<ParamField body="storeId" type="string" required>
  UUID of the store to update
</ParamField>

<ParamField body="name" type="string">
  New store name
</ParamField>

<ParamField body="egressAllowlist" type="string[]">
  New allowed egress hosts
</ParamField>

**Returns:** `Promise<SecretStoreInfo>`

***

## `SecretStore.delete(storeId, opts?)`

Deletes the store and all its secrets. Running sandboxes are not affected.

**Returns:** `Promise<void>`

***

## `SecretStore.setSecret(storeId, name, value, opts?)`

Set a secret in a store. Secrets are encrypted at rest. The value is never returned by the API.

```typescript
await SecretStore.setSecret('store-uuid', 'ANTHROPIC_API_KEY', 'sk-ant-...');
```

Optionally restrict which hosts can receive this secret:

```typescript
await SecretStore.setSecret('store-uuid', 'ANTHROPIC_API_KEY', 'sk-ant-...', {
  allowedHosts: ['api.anthropic.com'],
});
```

<ParamField body="storeId" type="string" required>
  UUID of the secret store
</ParamField>

<ParamField body="name" type="string" required>
  Secret name (used as the env var name in sandboxes)
</ParamField>

<ParamField body="value" type="string" required>
  Secret value (encrypted at rest, never returned by API)
</ParamField>

<ParamField body="allowedHosts" type="string[]">
  Restrict this secret to specific hosts only
</ParamField>

**Returns:** `Promise<void>`

***

## `SecretStore.listSecrets(storeId, opts?)`

Returns secret metadata only. Values are never exposed.

**Returns:** `Promise<SecretEntryInfo[]>`

***

## `SecretStore.deleteSecret(storeId, name, opts?)`

**Returns:** `Promise<void>`

***

## Using Secrets with Sandboxes

Pass the `secretStore` option to `Sandbox.create()` to inject the store's secrets:

```typescript
const sandbox = await Sandbox.create({
  secretStore: 'my-agent-secrets',
  timeout: 600,
});

// Secrets are available as sealed env vars
const result = await sandbox.commands.run('echo $ANTHROPIC_API_KEY');
// stdout: "osb_sealed_abc123..." (sealed, not the real key)

// But outbound HTTPS requests get the real value substituted by the proxy
const apiResult = await sandbox.commands.run(
  'curl -s https://api.anthropic.com/v1/messages -H "x-api-key: $ANTHROPIC_API_KEY" ...'
);
```

## Using Secrets with Snapshots and Checkpoints

### Snapshot template with secrets

Attach a secret store when creating a sandbox from a pre-built snapshot — even if the snapshot was built without one:

```typescript
import { Sandbox, Snapshots, Image } from '@opencomputer/sdk/node';

// Pre-build a snapshot with dependencies
const snapshots = new Snapshots();
await snapshots.create({
  name: 'data-pipeline',
  image: Image.base().aptInstall(['python3-pip']).pipInstall(['pandas']),
});

// Create sandboxes from the snapshot, each with their own credentials
const worker1 = await Sandbox.create({
  snapshot: 'data-pipeline',
  secretStore: 'worker-1-keys',
});
const worker2 = await Sandbox.create({
  snapshot: 'data-pipeline',
  secretStore: 'worker-2-keys',
});
```

### Checkpoint fork with secrets

Attach or layer a secret store when forking from a checkpoint:

```typescript
// Bake a base environment with git credentials
const base = await Sandbox.create({ secretStore: 'git-creds' });
await base.exec.run('git clone https://github.com/org/repo /app', { cwd: '/' });
const cp = await base.createCheckpoint('repo-cloned');

// Fork with different credentials per sandbox (layered on top of git-creds)
const worker = await Sandbox.createFromCheckpoint(cp.id, {
  secretStore: 'worker-keys',  // layered on top of git-creds
});
```

When layered, secrets merge (fork's store wins on collision) and egress allowlists aggregate.

***

## Types

<AccordionGroup>
  <Accordion title="SecretStoreInfo">
    | Property          | Type       | Description          |
    | ----------------- | ---------- | -------------------- |
    | `id`              | `string`   | Store UUID           |
    | `orgId`           | `string`   | Organization UUID    |
    | `name`            | `string`   | Store name           |
    | `egressAllowlist` | `string[]` | Allowed egress hosts |
    | `createdAt`       | `string`   | ISO 8601 timestamp   |
    | `updatedAt`       | `string`   | ISO 8601 timestamp   |
  </Accordion>
</AccordionGroup>

<AccordionGroup>
  <Accordion title="SecretEntryInfo">
    | Property       | Type       | Description                |
    | -------------- | ---------- | -------------------------- |
    | `id`           | `string`   | Entry UUID                 |
    | `storeId`      | `string`   | Parent store UUID          |
    | `name`         | `string`   | Secret name (env var name) |
    | `allowedHosts` | `string[]` | Host restrictions          |
    | `createdAt`    | `string`   | ISO 8601 timestamp         |
    | `updatedAt`    | `string`   | ISO 8601 timestamp         |
  </Accordion>
</AccordionGroup>


---
title: Python SDK
url: "https://docs-v1.opencomputer.dev/reference/python-sdk/overview.md"
description: Complete Python SDK reference
---

## Installation

```bash
pip install opencomputer-sdk
```

```python
from opencomputer import Sandbox, Image, Snapshots
```

## Modules

<CardGroup cols={2}>
  <Card title="Sandbox" icon="box" href="/reference/python-sdk/sandbox">
    Create, connect, and manage sandbox lifecycle
  </Card>

  <Card title="Exec" icon="terminal" href="/reference/python-sdk/exec">
    Run commands and manage exec sessions
  </Card>

  <Card title="Agent" icon="robot" href="/reference/python-sdk/agent">
    Start and manage Claude agent sessions
  </Card>

  <Card title="Filesystem" icon="folder" href="/reference/python-sdk/filesystem">
    Read, write, and manage files
  </Card>

  <Card title="PTY" icon="terminal" href="/reference/python-sdk/pty">
    Interactive terminal sessions
  </Card>

  <Card title="Image" icon="layer-group" href="/reference/python-sdk/image">
    Declarative sandbox image builder
  </Card>

  <Card title="Snapshots" icon="camera" href="/reference/python-sdk/snapshots">
    Pre-built sandbox environments
  </Card>

  <Card title="Secret Stores" icon="key" href="/reference/python-sdk/secrets">
    Manage secrets and egress control
  </Card>
</CardGroup>


---
title: Sandbox
url: "https://docs-v1.opencomputer.dev/reference/python-sdk/sandbox.md"
description: "Create, connect, and manage sandbox lifecycle"
---

## Class Methods

### `await Sandbox.create(...)`

Create a new sandbox. [HTTP API →](/api-reference/sandboxes/create)

<ParamField body="template" type="str" default="base">
  Template name
</ParamField>

<ParamField body="timeout" type="int" default="300">
  Idle timeout in seconds
</ParamField>

<ParamField body="api_key" type="str">
  API key (falls back to `OPENCOMPUTER_API_KEY` env var)
</ParamField>

<ParamField body="api_url" type="str">
  API URL (falls back to `OPENCOMPUTER_API_URL` env var)
</ParamField>

<ParamField body="envs" type="dict[str, str]">
  Environment variables
</ParamField>

<ParamField body="metadata" type="dict[str, str]">
  Arbitrary metadata
</ParamField>

<ParamField body="burst" type="bool">
  Create a [Burst Sandbox](/sandboxes/burst-sandboxes). Disk is preserved across infrastructure restarts; processes may restart.
</ParamField>

<ParamField body="image" type="Image">
  Declarative image definition (see [Image](/reference/python-sdk/image))
</ParamField>

<ParamField body="snapshot" type="str">
  Name of a pre-built snapshot
</ParamField>

<ParamField body="on_build_log" type="Callable[[str], None]">
  Build log callback (when using `image`)
</ParamField>

**Returns:** `Sandbox`

```python
sandbox = await Sandbox.create(template="my-stack", timeout=600)
```

Create a Burst Sandbox:

```python
sandbox = await Sandbox.create(
    burst=True,
    timeout=300,
)
```

<Note>
  Burst Sandboxes are alpha. They preserve filesystem state across
  infrastructure restarts, may restart running processes, and are priced roughly
  2x cheaper than on-demand sandboxes.
</Note>

***

### `await Sandbox.connect(sandbox_id, ...)`

Connect to an existing sandbox. [HTTP API →](/api-reference/sandboxes/get)

<ParamField body="sandbox_id" type="str" required>
  Sandbox ID
</ParamField>

**Returns:** `Sandbox`

```python
sandbox = await Sandbox.connect("sb-abc123")
```

***

### `await Sandbox.create_from_checkpoint(checkpoint_id, ...)`

Create a new sandbox from a checkpoint. [HTTP API →](/api-reference/checkpoints/fork)

<ParamField body="checkpoint_id" type="str" required>
  Checkpoint ID
</ParamField>

<ParamField body="timeout" type="int" default="300">
  Idle timeout
</ParamField>

**Returns:** `Sandbox`

```python
forked = await Sandbox.create_from_checkpoint("cp-abc123")
```

***

### `await Sandbox.create_checkpoint_patch(checkpoint_id, script, ...)`

[HTTP API →](/api-reference/patches/create)

<ParamField body="checkpoint_id" type="str" required>
  Target checkpoint
</ParamField>

<ParamField body="script" type="str" required>
  Bash script
</ParamField>

<ParamField body="description" type="str">
  Description
</ParamField>

**Returns:** `dict`

***

### `await Sandbox.list_checkpoint_patches(checkpoint_id, ...)`

[HTTP API →](/api-reference/patches/list) — **Returns:** `list[dict]`

### `await Sandbox.delete_checkpoint_patch(checkpoint_id, patch_id, ...)`

[HTTP API →](/api-reference/patches/delete) — **Returns:** `None`

***

## Context Manager

Auto-kills the sandbox on exit:

```python
async with await Sandbox.create() as sandbox:
    result = await sandbox.exec.run("echo hello")
    print(result.stdout)
# sandbox.kill() called automatically
```

***

## Instance Methods

### `await sandbox.kill()`

Terminate the sandbox. [HTTP API →](/api-reference/sandboxes/delete)

**Returns:** `None`

### `await sandbox.is_running()`

Check if the sandbox is running.

**Returns:** `bool`

### `await sandbox.set_timeout(timeout)`

Update idle timeout. [HTTP API →](/api-reference/sandboxes/set-timeout)

<ParamField body="timeout" type="int" required>
  New timeout in seconds
</ParamField>

**Returns:** `None`

### `await sandbox.create_checkpoint(name, kind=None, retention_policy=None)`

Create a named checkpoint. [HTTP API →](/api-reference/checkpoints/create)

<ParamField body="retention_policy" type="dict">
  Optional retention policy. Use `{"mode": "delete_oldest", "maxCount": 10}`
  for full checkpoints or `{"mode": "delete_oldest", "maxCount": 100}` with
  `kind="disk_only"` to delete the oldest eligible checkpoint of the same type
  before creating a new one.
</ParamField>

**Returns:** `dict`

### `await sandbox.list_checkpoints()`

[HTTP API →](/api-reference/checkpoints/list) — **Returns:** `list[dict]`

### `await sandbox.restore_checkpoint(checkpoint_id)`

Revert in-place to a checkpoint. [HTTP API →](/api-reference/checkpoints/restore)

**Returns:** `None`

### `await sandbox.delete_checkpoint(checkpoint_id)`

[HTTP API →](/api-reference/checkpoints/delete) — **Returns:** `None`

### `await sandbox.download_url(path, *, expires_in=3600)`

Generate a signed download URL. [HTTP API →](/api-reference/files/generate-download-url) · [Guide →](/sandboxes/signed-urls)

<ParamField body="path" type="str" required>
  Absolute path to the file
</ParamField>

<ParamField body="expires_in" type="int" default="3600">
  URL lifetime in seconds (max: 86400)
</ParamField>

**Returns:** `str`

### `await sandbox.upload_url(path, *, expires_in=3600)`

Generate a signed upload URL. [HTTP API →](/api-reference/files/generate-upload-url) · [Guide →](/sandboxes/signed-urls)

<ParamField body="path" type="str" required>
  Absolute path for the destination file
</ParamField>

<ParamField body="expires_in" type="int" default="3600">
  URL lifetime in seconds (max: 86400)
</ParamField>

**Returns:** `str`

### `await sandbox.create_preview_url(port, domain?, auth_config?)`

[HTTP API →](/api-reference/preview/create)

<ParamField body="port" type="int" required>
  Container port (1–65535)
</ParamField>

<ParamField body="domain" type="str">
  Custom domain
</ParamField>

<ParamField body="auth_config" type="dict">
  Auth configuration
</ParamField>

**Returns:** `dict`

### `await sandbox.list_preview_urls()`

[HTTP API →](/api-reference/preview/list) — **Returns:** `list[dict]`

### `await sandbox.delete_preview_url(port)`

[HTTP API →](/api-reference/preview/delete) — **Returns:** `None`

### `await sandbox.get_allowed_hosts()`

Return the egress allowlist + per-secret allowed hosts the sandbox's secrets proxy enforces. Useful for debugging "why is my outbound HTTP call being blocked" without having to cross-reference the [secret store](/sandboxes/secrets) config separately.

Sandboxes created without a `secret_store` option return an empty allowlist with `secretStore` omitted — the sandbox has no per-store egress restriction.

**Returns:** `dict` with the shape:

```python
{
    "sandboxID": "sb-...",
    "secretStore": "my-store",                   # omitted if no store
    "baseSecretStore": "parent-store",           # omitted unless this is a layered fork
    "egressAllowlist": ["api.openai.com", ...],
    "perSecretAllowedHosts": {
        "OPENAI_API_KEY": ["api.openai.com"],
    },
}
```

For forks that layered an additional `secret_store` on top of an inherited one, `egressAllowlist` is the **union** of both stores' allowlists (matches what the runtime proxy enforces) and `baseSecretStore` is populated with the inherited parent's name. For sandboxes with a single store (or none), `baseSecretStore` is omitted.

```python
info = await sandbox.get_allowed_hosts()
print(info["egressAllowlist"])          # ['api.openai.com', 'api.anthropic.com']
print(info["perSecretAllowedHosts"])    # {'OPENAI_API_KEY': ['api.openai.com']}
```

### `await sandbox.close()`

Close HTTP clients. Called automatically by the context manager.

***

## Not Available in Python

These features are TypeScript-only. Use the [HTTP API](/api-reference/overview) directly:

* `hibernate()` / `wake()`
* `cpuCount` / `memoryMB` on create

***

## Properties

<ParamField body="sandbox_id" type="str">
  Sandbox ID
</ParamField>

<ParamField body="status" type="str">
  Current status
</ParamField>

<ParamField body="agent" type="Agent">
  [Agent sessions](/reference/python-sdk/agent)
</ParamField>

<ParamField body="exec" type="Exec">
  [Command execution](/reference/python-sdk/exec)
</ParamField>

<ParamField body="files" type="Filesystem">
  [File operations](/reference/python-sdk/filesystem)
</ParamField>

<ParamField body="pty" type="Pty">
  [Terminal sessions](/reference/python-sdk/pty)
</ParamField>

<ParamField body="commands" type="Exec">
  **Deprecated** — alias for `exec`
</ParamField>


---
title: Scaling
url: "https://docs-v1.opencomputer.dev/reference/python-sdk/scaling.md"
description: "Resize a sandbox manually, autoscale on memory pressure, or freeze its resources."
---

OpenComputer sandboxes can change size at runtime. There are three knobs:

| What you want                       | Use                                                      |
| ----------------------------------- | -------------------------------------------------------- |
| Resize once, predictably            | [`scale()`](#sandbox-scale-memory-mb)                    |
| Track memory pressure automatically | [`set_autoscale()`](#sandbox-set-autoscale)              |
| Freeze the current size             | [`set_scaling_lock()`](#sandbox-set-scaling-lock-locked) |

CPU follows memory per the platform's tier table. You don't pick CPU separately.

## How the three interact

* **Manual `scale()`** disables autoscale on the sandbox as a side effect — explicit user intent overrides the loop. Re-enable with `set_autoscale(enabled=True, ...)` after if you want.
* **Setting a scaling lock** disables autoscale at the same time (single knob: "I don't want this scaling, period"). While locked, both `scale()` and `set_autoscale(enabled=True)` raise `ScalingLockedError`. Unlocking does NOT auto-re-enable autoscale.
* **Plan caps** apply everywhere. Free-tier orgs are capped at 4 GB. Calls above the cap raise `PlanLimitError`.

***

## `sandbox.scale(memory_mb)`

Manually resize the sandbox. [HTTP API →](/api-reference/sandboxes/scale)

<ParamField body="memory_mb" type="int" required>
  Target memory in MB.
</ParamField>

**Returns:** `dict` with `sandboxID`, `memoryMB`, `cpuPercent`.

**Raises:**

* `ScalingLockedError` — sandbox has a scaling lock active.
* `PlanLimitError` — `memory_mb` exceeds the org's plan cap.

```python
from opencomputer import Sandbox, ScalingLockedError, PlanLimitError

sandbox = await Sandbox.connect("sb-abc123")

try:
    result = await sandbox.scale(memory_mb=8192)
    print(f"scaled to {result['memoryMB']}MB / {result['cpuPercent']}% CPU")
except ScalingLockedError:
    print("sandbox is locked — unlock to scale")
except PlanLimitError:
    print("upgrade required for larger instances")
```

***

## `sandbox.set_autoscale(enabled, *, min_memory_mb=None, max_memory_mb=None)`

Enable or disable per-sandbox autoscale.

<ParamField body="enabled" type="bool" required>
  Whether autoscale should be active.
</ParamField>

<ParamField body="min_memory_mb" type="int">
  Lower bound when `enabled=True`.
</ParamField>

<ParamField body="max_memory_mb" type="int">
  Upper bound when `enabled=True`. Must be ≥ `min_memory_mb`.
</ParamField>

**Returns:** `dict` with `sandboxID`, `enabled`, `minMemoryMB`, `maxMemoryMB`.

**Raises:**

* `ScalingLockedError` — sandbox has a scaling lock active.
* `PlanLimitError` — `max_memory_mb` exceeds the org's plan cap.

When `enabled=True`, the platform watches the sandbox's memory pressure and resizes it within the bounds:

* **Scale up** on a single 1-min sample above 75% memory utilization. Cooldown 60s between up-scales.
* **Scale down** only when 1-min, 5-min, AND 15-min averages all sit below 25%. Cooldown 5 min between down-scales.

The asymmetry matches user perception: rapid response when the user notices lag, conservative shrink after sustained idle. The 15-min window is the dominant constraint on shrink, so a sandbox that briefly idles and resumes won't sawtooth.

```python
await sandbox.set_autoscale(
    enabled=True,
    min_memory_mb=1024,
    max_memory_mb=16384,
)
```

To turn off:

```python
await sandbox.set_autoscale(enabled=False)
```

***

## `sandbox.get_autoscale()`

Get the current autoscale configuration.

**Returns:** `dict` with `sandboxID`, `enabled`, `minMemoryMB`, `maxMemoryMB`.

```python
cfg = await sandbox.get_autoscale()
if cfg["enabled"]:
    print(f"autoscale {cfg['minMemoryMB']}–{cfg['maxMemoryMB']} MB")
```

***

## `sandbox.set_scaling_lock(locked)`

Lock or unlock the sandbox's resources against any size change.

<ParamField body="locked" type="bool" required>
  `True` to freeze, `False` to allow scaling again.
</ParamField>

**Returns:** `dict` with `sandboxID`, `locked`.

While locked:

* `scale()` raises `ScalingLockedError`.
* `set_autoscale(enabled=True)` raises `ScalingLockedError`.
* The platform autoscaler skips this sandbox entirely.

Locking ALSO disables autoscale (single knob — "I don't want this scaling"). Unlocking does **not** re-enable autoscale; call `set_autoscale(enabled=True, ...)` explicitly if you want it back.

```python
# Pin a sandbox at its current size during a critical workload
await sandbox.set_scaling_lock(True)

# ...

# Allow scaling again after
await sandbox.set_scaling_lock(False)
```

***

## `sandbox.get_scaling_lock()`

Get the current scaling-lock state.

**Returns:** `dict` with `sandboxID`, `locked`.

***

## Errors

### `ScalingLockedError`

Raised when the sandbox has a scaling lock active. Has a class attribute `code = "scaling_locked"` for parity with the HTTP API's error code.

```python
from opencomputer import ScalingLockedError

try:
    await sandbox.scale(memory_mb=8192)
except ScalingLockedError:
    # unlock first, or surface to user
    ...
```

### `PlanLimitError`

Raised when the requested size exceeds the org's plan cap. The HTTP API returns 402 Payment Required for this case.


---
title: Exec
url: "https://docs-v1.opencomputer.dev/reference/python-sdk/exec.md"
description: Run commands and manage exec sessions
---

Accessed via `sandbox.exec`.

## `await sandbox.exec.run(command, ...)`

Run a command synchronously via `sh -c`. [HTTP API →](/api-reference/exec/run)

<ParamField body="command" type="str" required>
  Shell command
</ParamField>

<ParamField body="timeout" type="int" default="60">
  Timeout in seconds
</ParamField>

<ParamField body="env" type="dict[str, str]">
  Environment variables
</ParamField>

<ParamField body="cwd" type="str">
  Working directory
</ParamField>

**Returns:** `ProcessResult`

```python
result = await sandbox.exec.run("npm test", cwd="/app")
```

***

## `await sandbox.exec.start(command, ...)`

Start a long-running command with streaming I/O. [HTTP API →](/api-reference/exec/create-session)

<ParamField body="command" type="str" required>
  Command
</ParamField>

<ParamField body="args" type="list[str]">
  Arguments
</ParamField>

<ParamField body="env" type="dict[str, str]">
  Environment variables
</ParamField>

<ParamField body="cwd" type="str">
  Working directory
</ParamField>

<ParamField body="timeout" type="int">
  Timeout in seconds
</ParamField>

<ParamField body="max_run_after_disconnect" type="int">
  Seconds to keep running after disconnect
</ParamField>

<ParamField body="on_stdout" type="Callable[[bytes], None]">
  Stdout callback
</ParamField>

<ParamField body="on_stderr" type="Callable[[bytes], None]">
  Stderr callback
</ParamField>

<ParamField body="on_exit" type="Callable[[int], None]">
  Exit callback
</ParamField>

**Returns:** `ExecSession`

```python
session = await sandbox.exec.start(
    "node", args=["server.js"], cwd="/app",
    on_stdout=lambda b: print(b.decode(), end=""),
    on_exit=lambda code: print(f"exited {code}"),
)
exit_code = await session.done
```

***

## `await sandbox.exec.background(command, ...)`

Alias for [`start`](#await-sandbox-exec-start-command). Same signature, same return type. Use when the intent is "run this in the background and observe it".

```python
server = await sandbox.exec.background("npm", args=["run", "dev"])
```

***

## `await sandbox.exec.shell(...)`

Open a stateful shell session. Subsequent `.run()` calls share the same bash process, so `cwd`, exported env vars, and shell functions persist — the ergonomics of a terminal tab.

Backed by a long-running `bash --noprofile --norc` session. Foreground-only: concurrent `.run()` raises `ShellBusyError`.

<ParamField body="cwd" type="str">
  Initial working directory
</ParamField>

<ParamField body="env" type="dict[str, str]">
  Initial environment variables
</ParamField>

**Returns:** `Shell`

```python
sh = await sandbox.exec.shell(cwd="/app")

await sh.run("npm install")
await sh.run("export NODE_ENV=test")
r = await sh.run("npm test", on_stdout=lambda b: print(b.decode(), end=""))
print(r.exit_code)

await sh.close()
```

### Shell

| Member                               | Type            | Description                                 |
| ------------------------------------ | --------------- | ------------------------------------------- |
| `session_id`                         | `str`           | Underlying exec session ID                  |
| `run(cmd, on_stdout=?, on_stderr=?)` | `ProcessResult` | Run a command, blocking until it exits      |
| `close()`                            | `None`          | Write `exit` and wait for bash to terminate |

<Note>
  Per-call `cwd`, `env`, and `timeout` are intentionally not supported in v1. Use inline shell syntax (`cd /x && cmd`, `FOO=bar cmd`) — the shell state carries across calls.
</Note>

Errors:

* `ShellBusyError` — another `run()` is in flight (shell is foreground-only).
* `ShellClosedError` — bash has exited (explicit `close()`, a command ran `exit`, or the session dropped).

***

## `await sandbox.exec.attach(session_id, ...)`

Reconnect to a running exec session over WebSocket.

<ParamField body="session_id" type="str" required>
  Session ID
</ParamField>

<ParamField body="on_stdout" type="Callable[[bytes], None]">
  Stdout callback
</ParamField>

<ParamField body="on_stderr" type="Callable[[bytes], None]">
  Stderr callback
</ParamField>

<ParamField body="on_exit" type="Callable[[int], None]">
  Exit callback
</ParamField>

<ParamField body="on_scrollback_end" type="Callable[[], None]">
  Scrollback replay done
</ParamField>

**Returns:** `ExecSession`

***

## `await sandbox.exec.list()`

List all exec sessions. [HTTP API →](/api-reference/exec/list-sessions)

**Returns:** `list[ExecSessionInfo]`

***

## `await sandbox.exec.kill(session_id, signal=9)`

Kill an exec session. [HTTP API →](/api-reference/exec/kill-session)

**Returns:** `None`

***

## ExecSession

| Member             | Type                  | Description                              |
| ------------------ | --------------------- | ---------------------------------------- |
| `session_id`       | `str`                 | Session ID                               |
| `done`             | `asyncio.Future[int]` | Resolves with exit code                  |
| `send_stdin(data)` | coroutine             | Send input (`str` or `bytes`)            |
| `kill(signal=9)`   | coroutine             | Kill process                             |
| `close()`          | coroutine             | Detach WebSocket (process keeps running) |

***

## Types

<AccordionGroup>
  <Accordion title="ProcessResult">
    ```python
    @dataclass
    class ProcessResult:
        exit_code: int
        stdout: str
        stderr: str
    ```
  </Accordion>
</AccordionGroup>

<AccordionGroup>
  <Accordion title="ExecSessionInfo">
    ```python
    @dataclass
    class ExecSessionInfo:
        session_id: str
        sandbox_id: str
        command: str
        args: list[str]
        running: bool
        exit_code: int | None
        started_at: str
        attached_clients: int
    ```
  </Accordion>
</AccordionGroup>


---
title: Filesystem
url: "https://docs-v1.opencomputer.dev/reference/python-sdk/filesystem.md"
description: "Read, write, and manage files"
---

Accessed via `sandbox.files`.

## `await sandbox.files.read(path)`

Read a file as a UTF-8 string. [HTTP API →](/api-reference/files/read)

<ParamField body="path" type="str" required>
  Absolute path to the file
</ParamField>

**Returns:** `str`

***

## `await sandbox.files.read_bytes(path)`

Read a file as raw bytes.

<ParamField body="path" type="str" required>
  Absolute path to the file
</ParamField>

**Returns:** `bytes`

***

## `await sandbox.files.write(path, content)`

Write content to a file. [HTTP API →](/api-reference/files/write)

<ParamField body="path" type="str" required>
  Absolute path for the destination file
</ParamField>

<ParamField body="content" type="str | bytes" required>
  File content
</ParamField>

**Returns:** `None`

***

## `await sandbox.files.list(path="/")`

List directory contents. [HTTP API →](/api-reference/files/list-directory)

<ParamField body="path" type="str" default="/">
  Directory path
</ParamField>

**Returns:** `list[EntryInfo]`

***

## `await sandbox.files.make_dir(path)`

Create a directory. [HTTP API →](/api-reference/files/mkdir)

<ParamField body="path" type="str" required>
  Directory path
</ParamField>

**Returns:** `None`

***

## `await sandbox.files.remove(path)`

Delete a file or directory. [HTTP API →](/api-reference/files/delete)

<ParamField body="path" type="str" required>
  Path to remove
</ParamField>

**Returns:** `None`

***

## `await sandbox.files.exists(path)`

Check if a path exists. Client-side wrapper — attempts a read and returns `False` on error.

<ParamField body="path" type="str" required>
  Path to check
</ParamField>

**Returns:** `bool`

***

## `await sandbox.files.read_stream(path)`

Stream a file as async byte chunks. Avoids buffering the full file in memory — ideal for large files (50MB+).

<ParamField body="path" type="str" required>
  Absolute path to the file
</ParamField>

**Returns:** `AsyncIterator[bytes]`

***

## `await sandbox.files.write_stream(path, stream)`

Stream content to a file from an async iterator. Avoids buffering the full file in memory.

<ParamField body="path" type="str" required>
  Absolute path for the destination file
</ParamField>

<ParamField body="stream" type="AsyncIterator[bytes]" required>
  Async iterator yielding byte chunks
</ParamField>

**Returns:** `None`

***

## Types

<AccordionGroup>
  <Accordion title="EntryInfo">
    ```python
    @dataclass
    class EntryInfo:
        name: str
        is_dir: bool
        path: str
        size: int = 0
    ```
  </Accordion>
</AccordionGroup>


---
title: PTY
url: "https://docs-v1.opencomputer.dev/reference/python-sdk/pty.md"
description: Interactive terminal sessions
---

Accessed via `sandbox.pty`.

## `await sandbox.pty.create(cols=80, rows=24, on_output=None)`

Create an interactive terminal session. [HTTP API →](/api-reference/pty/create)

<ParamField body="cols" type="int" default="80">
  Terminal columns
</ParamField>

<ParamField body="rows" type="int" default="24">
  Terminal rows
</ParamField>

<ParamField body="on_output" type="Callable[[bytes], None]">
  Output callback
</ParamField>

**Returns:** `PtySession`

***

## PtySession

| Member          | Type   | Description                          |
| --------------- | ------ | ------------------------------------ |
| `session_id`    | `str`  | Session ID                           |
| `sandbox_id`    | `str`  | Sandbox ID                           |
| `send(data)`    | method | Send input (str or bytes)            |
| `await recv()`  | method | Receive output bytes (Python-unique) |
| `await close()` | method | Close the PTY                        |

<Note>
  `recv()` is Python-unique — a pull-based alternative to the `on_output` callback. Returns raw `bytes`.
</Note>


---
title: Image
url: "https://docs-v1.opencomputer.dev/reference/python-sdk/image.md"
description: Declarative sandbox image builder
---

Fluent, immutable builder for declarative sandbox images. Each method returns a new `Image` instance.

## `Image.base()`

Start from the default OpenSandbox environment (Ubuntu 22.04, Python, Node.js, build tools).

```python
from opencomputer import Image

image = (
    Image.base()
    .apt_install(["curl", "jq"])
    .pip_install(["requests", "pandas"])
    .env({"PROJECT_ROOT": "/workspace"})
    .workdir("/workspace")
)
```

**Returns:** `Image`

***

## Builder Methods

### `image.apt_install(packages)`

<ParamField body="packages" type="list[str]" required>
  System packages to install via apt-get
</ParamField>

**Returns:** `Image`

### `image.pip_install(packages)`

<ParamField body="packages" type="list[str]" required>
  Python packages to install via pip
</ParamField>

**Returns:** `Image`

### `image.run_commands(*cmds)`

<ParamField body="cmds" type="str (variadic)" required>
  Shell commands to run during build
</ParamField>

**Returns:** `Image`

### `image.env(vars)`

<ParamField body="vars" type="dict[str, str]" required>
  Environment variables to set
</ParamField>

**Returns:** `Image`

### `image.workdir(path)`

<ParamField body="path" type="str" required>
  Default working directory
</ParamField>

**Returns:** `Image`

### `image.add_file(remote_path, content)`

<ParamField body="remote_path" type="str" required>
  Destination path in the image
</ParamField>

<ParamField body="content" type="str" required>
  File content
</ParamField>

**Returns:** `Image`

### `image.add_local_file(local_path, remote_path)`

<ParamField body="local_path" type="str" required>
  Path to the local file
</ParamField>

<ParamField body="remote_path" type="str" required>
  Destination path in the image
</ParamField>

**Returns:** `Image`

### `image.add_local_dir(local_path, remote_path)`

<ParamField body="local_path" type="str" required>
  Path to the local directory
</ParamField>

<ParamField body="remote_path" type="str" required>
  Destination path in the image
</ParamField>

**Returns:** `Image`

### `image.builder_memory(mb)`

Sets the RAM (in MB) used during the build phase. Raise this when a build OOMs
(heavy `apt`/`pip`/`npm`). Defaults to **4096**. Does **not** affect the resulting
sandbox's memory — size that at create time via `memory_mb`.

<ParamField body="mb" type="int" required>
  Build-phase memory in MB
</ParamField>

**Returns:** `Image`

***

## Utility Methods

### `image.to_dict()`

Returns the image manifest as a plain dict.

**Returns:** `dict`

### `image.cache_key()`

Computes a deterministic SHA-256 hash of the manifest for cache lookups.

**Returns:** `str`


---
title: Snapshots
url: "https://docs-v1.opencomputer.dev/reference/python-sdk/snapshots.md"
description: Pre-built sandbox environments
---

Standalone class — not a property on Sandbox.

## Constructor

```python
from opencomputer import Snapshots

snapshots = Snapshots()
# or with explicit config:
snapshots = Snapshots(api_key="...", api_url="...")
```

<ParamField body="api_key" type="str">
  API key (falls back to `OPENCOMPUTER_API_KEY` env var)
</ParamField>

<ParamField body="api_url" type="str">
  API URL (falls back to `OPENCOMPUTER_API_URL` env var)
</ParamField>

***

## `await snapshots.create(name, image, on_build_logs=None)`

Create a pre-built snapshot. [HTTP API →](/api-reference/snapshots/create)

<ParamField body="name" type="str" required>
  Unique snapshot name
</ParamField>

<ParamField body="image" type="Image" required>
  Declarative image definition
</ParamField>

<ParamField body="on_build_logs" type="Callable[[str], None]">
  Build log streaming callback
</ParamField>

**Returns:** `dict`

***

## `await snapshots.list()`

[HTTP API →](/api-reference/snapshots/list) — **Returns:** `list[dict]`

## `await snapshots.get(name)`

[HTTP API →](/api-reference/snapshots/get)

<ParamField body="name" type="str" required>
  Snapshot name
</ParamField>

**Returns:** `dict`

## `await snapshots.delete(name)`

Delete a snapshot. Existing sandboxes are not affected. [HTTP API →](/api-reference/snapshots/delete)

**Returns:** `None`

***

## SnapshotInfo

Snapshot methods return dicts with these fields:

<AccordionGroup>
  <Accordion title="SnapshotInfo fields">
    ```python
    {
        "id": str,           # Unique snapshot identifier
        "name": str,         # Snapshot name
        "status": str,       # "building" | "ready" | "failed"
        "contentHash": str,  # SHA-256 hash of the image manifest
        "checkpointId": str, # Linked checkpoint ID
        "manifest": dict,    # The declarative image manifest
        "createdAt": str,    # ISO 8601 creation timestamp
        "lastUsedAt": str,   # ISO 8601 last usage timestamp
    }
    ```
  </Accordion>
</AccordionGroup>


---
title: Secret Stores
url: "https://docs-v1.opencomputer.dev/reference/python-sdk/secrets.md"
description: Manage secrets and egress control
---

## `await SecretStore.create(**kwargs)`

Create a new secret store.

```python
from opencomputer import SecretStore

store = await SecretStore.create(
    name='my-agent-secrets',
    egress_allowlist=['api.anthropic.com'],
)
```

<ParamField body="name" type="str" required>
  Store name (unique per organization)
</ParamField>

<ParamField body="egress_allowlist" type="list[str] | None">
  Allowed egress hosts
</ParamField>

**Returns:** `dict`

***

## `await SecretStore.list(**kwargs)`

**Returns:** `list[dict]`

## `await SecretStore.get(store_id, **kwargs)`

<ParamField body="store_id" type="str" required>
  UUID of the secret store
</ParamField>

**Returns:** `dict`

***

## `await SecretStore.update(store_id, **kwargs)`

Partial updates — only the fields you pass are changed.

```python
updated = await SecretStore.update(
    'store-uuid',
    name='new-name',
    egress_allowlist=['api.anthropic.com', '*.openai.com'],
)
```

<ParamField body="store_id" type="str" required>
  UUID of the store to update
</ParamField>

<ParamField body="name" type="str">
  New store name
</ParamField>

<ParamField body="egress_allowlist" type="list[str] | None">
  New allowed egress hosts
</ParamField>

**Returns:** `dict`

***

## `await SecretStore.delete(store_id, **kwargs)`

Deletes the store and all its secrets. Running sandboxes are not affected.

**Returns:** `None`

***

## `await SecretStore.set_secret(store_id, name, value, **kwargs)`

Set a secret. Encrypted at rest, never returned by API.

```python
await SecretStore.set_secret('store-uuid', 'ANTHROPIC_API_KEY', 'sk-ant-...')

# Optionally restrict to specific hosts:
await SecretStore.set_secret(
    'store-uuid', 'ANTHROPIC_API_KEY', 'sk-ant-...',
    allowed_hosts=['api.anthropic.com'],
)
```

<ParamField body="store_id" type="str" required>
  UUID of the secret store
</ParamField>

<ParamField body="name" type="str" required>
  Secret name (env var name in sandboxes)
</ParamField>

<ParamField body="value" type="str" required>
  Secret value
</ParamField>

<ParamField body="allowed_hosts" type="list[str] | None">
  Restrict to specific hosts
</ParamField>

**Returns:** `None`

***

## `await SecretStore.list_secrets(store_id, **kwargs)`

Returns metadata only. Values are never exposed.

**Returns:** `list[dict]`

## `await SecretStore.delete_secret(store_id, name, **kwargs)`

**Returns:** `None`

***

## Using Secrets with Sandboxes

```python
sandbox = await Sandbox.create(
    secret_store='my-agent-secrets',
    timeout=600,
)

# Secrets are available as sealed env vars
result = await sandbox.commands.run('echo $ANTHROPIC_API_KEY')
# stdout: "osb_sealed_abc123..." (sealed, not the real key)

# Outbound HTTPS requests get the real value substituted by the proxy
api_result = await sandbox.commands.run(
    'curl -s https://api.anthropic.com/v1/messages -H "x-api-key: $ANTHROPIC_API_KEY" ...'
)
```

## Using Secrets with Snapshots and Checkpoints

### Snapshot template with secrets

Attach a secret store when creating a sandbox from a pre-built snapshot — even if the snapshot was built without one:

```python
from opencomputer import Sandbox, Snapshots, Image

# Pre-build a snapshot with dependencies
snapshots = Snapshots()
await snapshots.create(
    name='data-pipeline',
    image=Image.base().apt_install(['python3-pip']).pip_install(['pandas']),
)

# Create sandboxes from the snapshot, each with their own credentials
worker1 = await Sandbox.create(
    snapshot='data-pipeline',
    secret_store='worker-1-keys',
)
worker2 = await Sandbox.create(
    snapshot='data-pipeline',
    secret_store='worker-2-keys',
)
```

### Checkpoint fork with secrets

Attach or layer a secret store when forking from a checkpoint:

```python
# Bake a base environment with git credentials
base = await Sandbox.create(secret_store='git-creds')
await base.exec.run('git clone https://github.com/org/repo /app')
cp = await base.create_checkpoint('repo-cloned')

# Fork with different credentials per sandbox (layered on top of git-creds)
worker = await Sandbox.create_from_checkpoint(
    cp['id'],
    secret_store='worker-keys',  # layered on top of git-creds
)
```

When layered, secrets merge (fork's store wins on collision) and egress allowlists aggregate.


---
title: CLI Reference
url: "https://docs-v1.opencomputer.dev/reference/cli/overview.md"
description: "Every command, subcommand, and flag"
---

## Global Flags

These flags apply to all commands.

<ParamField query="--api-key" type="string">
  API key for authentication. Env: `OPENCOMPUTER_API_KEY`
</ParamField>

<ParamField query="--api-url" type="string">
  Control plane URL. Env: `OPENCOMPUTER_API_URL`
</ParamField>

<ParamField query="--json" type="boolean">
  Output as JSON
</ParamField>

## Commands

<CardGroup cols={2}>
  <Card title="oc sandbox" icon="box" href="/reference/cli/sandbox">
    Manage sandbox lifecycle
  </Card>

  <Card title="oc exec" icon="terminal" href="/reference/cli/exec">
    Execute commands inside a sandbox
  </Card>

  <Card title="oc shell" icon="terminal" href="/reference/cli/shell">
    Interactive terminal access
  </Card>

  <Card title="oc checkpoint" icon="camera" href="/reference/cli/checkpoint">
    Manage sandbox checkpoints
  </Card>

  <Card title="oc patch" icon="file-code" href="/reference/cli/patch">
    Manage checkpoint patches
  </Card>

  <Card title="oc preview" icon="globe" href="/reference/cli/preview">
    Manage preview URLs
  </Card>

  <Card title="oc agent" icon="robot" href="/reference/cli/agent">
    Invoke agents and manage Hook URLs
  </Card>

  <Card title="oc login" icon="right-to-bracket" href="/reference/cli/auth">
    Sign in, inspect identity, and log out
  </Card>

  <Card title="oc config" icon="gear" href="/reference/cli/config">
    CLI configuration
  </Card>
</CardGroup>


---
title: oc sandbox
url: "https://docs-v1.opencomputer.dev/reference/cli/sandbox.md"
description: Manage sandbox lifecycle
---

## `oc sandbox create`

Create a new sandbox. **Alias:** `oc create`

[HTTP API →](/api-reference/sandboxes/create)

<ParamField query="--timeout" type="int" default="300">
  Idle timeout in seconds
</ParamField>

<ParamField query="--cpu" type="int" default="0">
  CPU cores (0 = platform default)
</ParamField>

<ParamField query="--memory" type="int" default="0">
  Memory in MB (0 = platform default)
</ParamField>

<ParamField query="--env" type="string">
  Environment variable `KEY=VALUE` (repeatable)
</ParamField>

<ParamField query="--metadata" type="string">
  Metadata `KEY=VALUE` (repeatable)
</ParamField>

<ParamField query="--preview-auth" type="bool">
  Require a bearer token on the sandbox's preview URLs. The server generates a 256-bit random token and prints it once. See [Preview-URL authentication](/sandboxes/preview-urls#authentication).
</ParamField>

<ParamField query="--preview-auth-token" type="string">
  Bring your own preview-URL bearer token (≥16 characters). Implies `--preview-auth`. Useful when your gateway already has a shared secret.
</ParamField>

```bash
oc create --timeout 600 --cpu 2 --memory 1024 --env NODE_ENV=production
oc create --preview-auth                                    # server-generated token
oc create --preview-auth-token "$GATEWAY_TOKEN"             # bring your own
```

***

## `oc sandbox list`

List all running sandboxes. **Alias:** `oc ls`

[HTTP API →](/api-reference/sandboxes/list)

Output columns: `ID`, `TEMPLATE`, `STATUS`, `CPU`, `MEM`, `AGE`

```bash
oc ls
oc ls --json
```

***

## `oc sandbox get <id>`

Show detailed information for a sandbox. [HTTP API →](/api-reference/sandboxes/get)

Output: ID, Template, Status, CPU, Memory, Started, Ends.

***

## `oc sandbox kill <id>`

Terminate and remove a sandbox. [HTTP API →](/api-reference/sandboxes/delete)

***

## `oc sandbox hibernate <id>`

Snapshot VM state and stop the sandbox. Displays snapshot size on success. [HTTP API →](/api-reference/sandboxes/hibernate)

***

## `oc sandbox wake <id>`

Resume a hibernated sandbox. [HTTP API →](/api-reference/sandboxes/wake)

<ParamField query="--timeout" type="int" default="300">
  Idle timeout in seconds after wake
</ParamField>

***

## `oc sandbox set-timeout <id> <seconds>`

Update the idle timeout for a running sandbox. [HTTP API →](/api-reference/sandboxes/set-timeout)

```bash
oc sandbox set-timeout sb-abc123 600
```

***

For commands that resize a sandbox or freeze its size — `scale`, `autoscale`, `lock`, `unlock`, `lock-status` — see [Scaling](/reference/cli/scaling).

***

## `oc sandbox allowed-hosts <id>`

Show the egress allowlist + per-secret allowed hosts the sandbox's secrets proxy enforces. Useful for debugging "why is my outbound HTTP call being blocked" without having to cross-reference the [secret store](/sandboxes/secrets) config separately.

Sandboxes created without `--secret-store` return an empty allowlist — the sandbox has no per-store egress restriction.

```bash
oc sandbox allowed-hosts sb-abc123
# Sandbox sb-abc123 (secret store: my-store)
#
# Egress allowlist:
#   • api.openai.com
#   • api.anthropic.com
#
# Per-secret allowed hosts:
#   OPENAI_API_KEY:
#     • api.openai.com
```

Use `--json` for the structured form:

```bash
oc sandbox allowed-hosts sb-abc123 --json
```

```json
{
  "sandboxID": "sb-abc123",
  "secretStore": "my-store",
  "egressAllowlist": ["api.openai.com", "api.anthropic.com"],
  "perSecretAllowedHosts": {
    "OPENAI_API_KEY": ["api.openai.com"]
  }
}
```

### Layered forks

When a fork layers an additional `--secret-store` on top of an inherited one, `egressAllowlist` is the **union** of both stores' allowlists (matches what the runtime proxy enforces). The response includes both store names — `secretStore` is the primary (whose secrets shadow the base on env-name collisions), `baseSecretStore` is the inherited parent:

```json
{
  "sandboxID": "sb-fork456",
  "secretStore": "fork-override",
  "baseSecretStore": "fork-parent",
  "egressAllowlist": ["api.openai.com", "api.anthropic.com", "api.example.com"],
  "perSecretAllowedHosts": {
    "OPENAI_API_KEY": ["api.openai.com"]
  }
}
```

`baseSecretStore` is omitted in the single-store case (no layering).


---
title: oc sandbox scaling
url: "https://docs-v1.opencomputer.dev/reference/cli/scaling.md"
description: "Resize sandboxes, configure autoscale, and manage scaling locks from the CLI"
---

The CLI groups three related actions for sizing a sandbox:

| Command                                                                                                                    | What it does                                               |
| -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| [`oc sandbox autoscale`](#oc-sandbox-autoscale-id)                                                                         | Turn the platform autoscaler on/off and inspect its config |
| [`oc sandbox scale`](#oc-sandbox-scale-id-memory-mb)                                                                       | Manually resize once                                       |
| [`oc sandbox lock`](#oc-sandbox-lock-id) / [`unlock`](#oc-sandbox-unlock-id) / [`lock-status`](#oc-sandbox-lock-status-id) | Freeze or unfreeze the current size                        |

CPU follows memory per the platform's tier table; you don't pick CPU separately.

For the underlying concepts and how these three modes interact, see the [Elasticity](/sandboxes/elasticity) guide.

***

## `oc sandbox autoscale <id>`

Configure or inspect per-sandbox autoscale. [HTTP API →](/api-reference/sandboxes/autoscale)

Run with no flags to print the current configuration:

```bash
oc sandbox autoscale sb-abc123
# Autoscale enabled for sb-abc123 (1024–16384 MB)
```

Enable with bounds:

```bash
oc sandbox autoscale sb-abc123 --on --min 1024 --max 16384
```

Disable:

```bash
oc sandbox autoscale sb-abc123 --off
```

When enabled, the platform watches memory pressure and resizes the sandbox between `--min` and `--max`:

* **Scale up** on a single 1-min sample above 75 % memory utilization. Cooldown 60 s between up-scales.
* **Scale down** only when the 1-min, 5-min, AND 15-min averages all sit below 25 %. Cooldown 5 min between down-scales.

The asymmetry is deliberate: rapid response when the user notices lag, conservative shrink after sustained idle.

**Flags**

| Flag      | Description                                        |
| --------- | -------------------------------------------------- |
| `--on`    | Enable autoscale (requires `--min` and `--max`).   |
| `--off`   | Disable autoscale. Mutually exclusive with `--on`. |
| `--min N` | Minimum memory in MB.                              |
| `--max N` | Maximum memory in MB. Must be ≥ `--min`.           |

**Errors**

* `scaling_locked` — the sandbox has a scaling lock active. Run `oc sandbox unlock <id>` first.
* `402 Payment Required` — `--max` exceeds your plan cap.

***

## `oc sandbox scale <id> <memory-mb>`

Manually resize a sandbox to a specific memory tier. [HTTP API →](/api-reference/sandboxes/scale)

```bash
oc sandbox scale sb-abc123 8192
# Scaled sb-abc123 to 8192MB / 400% CPU
```

A manual scale **disables autoscale** on this sandbox as a side effect — explicit intent overrides the loop. Re-enable with `oc sandbox autoscale --on` if you want size to track load again.

**Errors**

* `scaling_locked` — the sandbox has a scaling lock active. Run `oc sandbox unlock <id>` first.
* `402 Payment Required` — requested size exceeds your plan cap.

***

## `oc sandbox lock <id>`

Pin a sandbox at its current size. [HTTP API →](/api-reference/sandboxes/scaling-lock)

```bash
oc sandbox lock sb-abc123
# Sandbox sb-abc123 locked (scaling disabled)
```

While locked:

* `oc sandbox scale` is rejected with `scaling_locked`.
* `oc sandbox autoscale --on` is rejected with `scaling_locked`.
* The platform autoscaler skips the sandbox entirely.

Locking **also** disables autoscale (single knob — "I don't want this scaling, period"). Unlocking does NOT re-enable autoscale; run `oc sandbox autoscale --on` explicitly if you want it back.

***

## `oc sandbox unlock <id>`

Clear the scaling lock.

```bash
oc sandbox unlock sb-abc123
# Sandbox sb-abc123 unlocked
```

***

## `oc sandbox lock-status <id>`

Print the current scaling-lock state.

```bash
oc sandbox lock-status sb-abc123
# Sandbox sb-abc123 is unlocked
```


---
title: oc exec
url: "https://docs-v1.opencomputer.dev/reference/cli/exec.md"
description: Execute commands inside a sandbox
---

## `oc exec <id> [flags] -- <command...>`

By default, creates an **exec session** and prints the session ID with attach instructions. Use `--wait` to run synchronously.

[HTTP API →](/api-reference/exec/run)

<ParamField query="--wait" type="boolean" default="false">
  Wait for completion, stream output, mirror exit code
</ParamField>

<ParamField query="--cwd" type="string">
  Working directory
</ParamField>

<ParamField query="--timeout" type="int" default="0">
  Timeout in seconds (0 = none)
</ParamField>

<ParamField query="--env" type="string">
  Environment variable `KEY=VALUE` (repeatable)
</ParamField>

```bash
# Synchronous — streams output, exits with process exit code
oc exec sb-abc --wait -- npm run build

# Asynchronous — creates session, prints session ID
oc exec sb-abc -- node server.js
# → Session es-xyz created. Attach with: oc exec attach sb-abc es-xyz
```

***

## `oc exec list <id>`

List active exec sessions for a sandbox. [HTTP API →](/api-reference/exec/list-sessions)

Output: session ID, status (`running` or `exited (code)`), command, client count.

***

## `oc exec attach <id> <session-id>`

<Warning>
  Not yet implemented. The command exists but prints guidance to use the SDK or a WebSocket client (`websocat`).
</Warning>

***

## `oc exec kill <id> <session-id>`

Kill an exec session. [HTTP API →](/api-reference/exec/kill-session)

<ParamField query="--signal" type="int" default="9">
  Signal number (default: SIGKILL)
</ParamField>


---
title: oc shell
url: "https://docs-v1.opencomputer.dev/reference/cli/shell.md"
description: Interactive terminal access
---

## `oc shell <id>`

Open an interactive terminal (PTY) session via WebSocket. [HTTP API →](/api-reference/pty/create)

<ParamField query="--shell" type="string" default="/bin/bash">
  Shell to use
</ParamField>

Terminal size is auto-detected and resize events are forwarded. Press `Ctrl+D` or type `exit` to disconnect. The PTY session is cleaned up automatically on exit.

```bash
oc shell sb-abc123
oc shell sb-abc123 --shell /bin/zsh
```


---
title: oc checkpoint
url: "https://docs-v1.opencomputer.dev/reference/cli/checkpoint.md"
description: Manage sandbox checkpoints
---

**Alias:** `oc cp`

## `oc checkpoint create <sandbox-id>`

Create a named checkpoint of a running sandbox. [HTTP API →](/api-reference/checkpoints/create)

<ParamField query="--name" type="string" required>
  Checkpoint name
</ParamField>

<ParamField query="--retention-policy" type="string" default="none">
  Optional retention policy. Use `delete_oldest` to delete the oldest eligible checkpoint before creating this one.
</ParamField>

<ParamField query="--retention-max-count" type="int" default="10">
  Maximum checkpoints of this type to keep when `--retention-policy delete_oldest` is set. Must be between 1 and 10 for full checkpoints, or 1 and 100 for disk-only checkpoints.
</ParamField>

```bash
oc cp create sb-abc --name before-migration
```

```bash
oc cp create sb-abc \
  --name autosave \
  --kind disk_only \
  --retention-policy delete_oldest \
  --retention-max-count 100
```

***

## `oc checkpoint list <sandbox-id>`

List checkpoints for a sandbox. [HTTP API →](/api-reference/checkpoints/list)

Output columns: `ID`, `NAME`, `STATUS`, `SIZE`, `CREATED`

***

## `oc checkpoint restore <sandbox-id> <checkpoint-id>`

Revert a sandbox in-place to a checkpoint. All changes since the checkpoint are lost. [HTTP API →](/api-reference/checkpoints/restore)

***

## `oc checkpoint spawn <checkpoint-id>`

Create a new sandbox from a checkpoint (fork). [HTTP API →](/api-reference/checkpoints/fork)

<ParamField query="--timeout" type="int" default="300">
  Idle timeout for the new sandbox
</ParamField>

```bash
ID=$(oc cp spawn cp-xyz --json | jq -r '.sandboxID')
oc exec "$ID" --wait -- echo "forked sandbox ready"
```

***

## `oc checkpoint delete <sandbox-id> <checkpoint-id>`

Delete a checkpoint. [HTTP API →](/api-reference/checkpoints/delete)


---
title: oc patch
url: "https://docs-v1.opencomputer.dev/reference/cli/patch.md"
description: Manage checkpoint patches
---

Patches are scripts that run when a sandbox is spawned from a checkpoint.

## `oc patch create <checkpoint-id>`

[HTTP API →](/api-reference/patches/create)

<ParamField query="--script" type="string" required>
  Path to script file, or `-` for stdin
</ParamField>

<ParamField query="--description" type="string">
  Patch description
</ParamField>

```bash
oc patch create cp-xyz --script=./setup.sh --description="Install deps"
echo "apt install -y curl" | oc patch create cp-xyz --script=-
```

***

## `oc patch list <checkpoint-id>`

[HTTP API →](/api-reference/patches/list)

Output columns: `ID`, `SEQ`, `DESCRIPTION`, `STRATEGY`, `CREATED`

***

## `oc patch delete <checkpoint-id> <patch-id>`

[HTTP API →](/api-reference/patches/delete)


---
title: oc preview
url: "https://docs-v1.opencomputer.dev/reference/cli/preview.md"
description: Manage preview URLs
---

Expose sandbox ports to the internet.

## `oc preview create <sandbox-id>`

[HTTP API →](/api-reference/preview/create)

<ParamField query="--port" type="int" required>
  Container port to expose
</ParamField>

<ParamField query="--domain" type="string">
  Custom domain
</ParamField>

```bash
oc preview create sb-abc --port 3000
```

***

## `oc preview list <sandbox-id>`

[HTTP API →](/api-reference/preview/list)

Output columns: `PORT`, `HOSTNAME`, `SSL`, `CREATED`

***

## `oc preview delete <sandbox-id> <port>`

[HTTP API →](/api-reference/preview/delete)

***

## `oc preview rotate-auth <sandbox-id>`

[HTTP API →](/api-reference/preview/rotate-auth)

Mint a new bearer token for the sandbox's preview URLs. The old token stops working immediately. The new plaintext is printed once — capture it before moving on; the server will not return it again.

```bash
oc preview rotate-auth sb-abc
# New preview auth token (shown once): qx2sSi5IYX...
```

Calling this on a sandbox that was created without `--preview-auth` installs a token and starts enforcing the gate from that point on.

See [`oc sandbox create --preview-auth`](/reference/cli/sandbox#oc-sandbox-create) to enable at create time, or [`--preview-auth-token`](/reference/cli/sandbox#oc-sandbox-create) to bring your own.


---
title: Agent URLs and Hooks
url: "https://docs-v1.opencomputer.dev/reference/cli/agent.md"
description: Invoke agents and manage scoped Hook URLs
---

These commands use the Durable Agent Sessions API and the API key configured by
`oc login`, `OPENCOMPUTER_API_KEY`, or `--api-key`.

## oc agent invoke

Start a durable session through an agent's canonical URL:

```bash
oc agent invoke <id|name> \
  [--data '<json>' | --file <path> | --stdin] \
  [--idempotency-key <key>] \
  [--json]
```

`--data` defaults to `{}`. The CLI rejects non-JSON input locally, calls exact
`POST /` with the configured org key, and prints the accepted session id plus
follow/steer commands. It generates one idempotency key and keeps it stable
across automatic retries; supply your own key when separate command runs must
converge.

With `--json`, the output is the complete acceptance receipt, including the
one-hour session client token and canonical event/message links. Treat that
output as a session-scoped credential.

Use `oc session create` instead when you need model, source, destination, or
per-session limit overrides.

## oc agent hook create

```bash
oc agent hook create <name> --agent <id|name> \
  [--expires-at <rfc3339>] [--json]
```

Creates a named Hook and prints its complete secret URL once. If `--agent` is
omitted, the CLI resolves the agent from `agent.toml` in the current directory.

## oc agent hooks

```bash
oc agent hooks --agent <id|name> \
  [--include-revoked] [--cursor <cursor>] [--limit <n>] [--json]
```

Lists secret-free Hook metadata. Pagination is newest-first and the server caps
`--limit` at 100.

## oc agent hook get

```bash
oc agent hook get <hook-id|name> --agent <id|name> [--json]
```

Shows status, expiry, secret last four, and revoke reason. It cannot recover the
complete URL.

## oc agent hook revoke

```bash
oc agent hook revoke <hook-id|name> --agent <id|name> [--yes] [--json]
```

Irreversibly revokes the Hook after confirmation. Create and install a
replacement first if the external integration must stay available.


---
title: Login and identity
url: "https://docs-v1.opencomputer.dev/reference/cli/auth.md"
description: Sign the local CLI in to OpenComputer
---

## `oc login`

```bash
oc login [--no-browser] [--force]
```

The CLI prints a short confirmation code and a complete HTTPS sign-in URL,
then waits for approval. It tries to open the URL on a local machine; browser
failure does not stop the flow.

* `--no-browser` only prints the URL, which is useful over SSH or when a coding
  agent is running the command.
* `--force` replaces an existing saved credential. A prior CLI-created key is
  revoked only after the replacement is saved and verified. If its previous API
  is unreachable, replacement continues with a warning and that old key remains
  available for manual revocation. A manually configured key is replaced
  locally but never remotely revoked.

The credential is stored in `~/.oc/config.json` with private permissions and is
never printed, including under `--json`. If `--api-key` or
`OPENCOMPUTER_API_KEY` is active, unset that override before logging in.
When an API URL override differs from a saved login, the CLI requires an
explicit replacement rather than reporting the old host as logged in. Remote
login endpoints should use HTTPS; the CLI warns before sending credentials to
a non-loopback HTTP URL.

`oc login` is for a person's local CLI. CI, servers, and SDK applications
should keep using an explicit org API key supplied through their secret
manager.

## `oc whoami`

```bash
oc whoami
oc whoami --json
```

Shows the authenticated user, workspace, org id, and API URL for the effective
credential.

## `oc logout`

```bash
oc logout
oc logout --local
```

For a CLI-managed login, `oc logout` first revokes that exact key and only then
clears it from the config file. If remote revocation is ambiguous, the local
credential is retained and the command fails so it cannot claim a still-live
key was revoked.

`--local` explicitly clears local login state without remote revocation.
It remains available when an environment or flag key currently overrides the
saved login. Manually configured and environment-provided keys are never
remotely revoked by this command.


---
title: oc config
url: "https://docs-v1.opencomputer.dev/reference/cli/config.md"
description: CLI configuration
---

Manage CLI configuration. Settings are persisted to a local config file.

For an interactive local setup, prefer [`oc login`](/reference/cli/auth). Use
`oc config set api-key` for a separately managed key.

## `oc config set <key> <value>`

Supported keys: `api-key`, `api-url`.

```bash
oc config set api-key sk-1234...
oc config set api-url https://app.opencomputer.dev
```

Setting `api-key` explicitly clears CLI-login management metadata, so a later
`oc logout` cannot revoke a manually supplied key.

***

## `oc config show`

Display current configuration. The API key is masked (first 4 and last 4 characters shown).


---
title: API Reference
url: "https://docs-v1.opencomputer.dev/api-reference/overview.md"
description: Complete REST API reference for OpenComputer
---

## Base URL

```
https://app.opencomputer.dev/api
```

## Authentication

All requests require an API key passed via the `X-API-Key` header.

```bash
curl https://app.opencomputer.dev/api/sandboxes \
  -H "X-API-Key: $OPENCOMPUTER_API_KEY"
```

## WebSocket Authentication

WebSocket endpoints accept auth via query parameter:

```
wss://app.opencomputer.dev/api/sandboxes/{id}/exec/{sessionID}?api_key=<key>
```

## Error Format

All errors use a consistent JSON envelope:

```json
{
  "error": "descriptive error message"
}
```

| Status Code | Meaning                                        |
| ----------- | ---------------------------------------------- |
| `400`       | Invalid request (missing fields, bad values)   |
| `401`       | Missing or invalid authentication              |
| `403`       | Insufficient permissions                       |
| `404`       | Resource not found                             |
| `409`       | Conflict (duplicate resource)                  |
| `429`       | Quota exceeded                                 |
| `500`       | Internal server error                          |
| `503`       | Feature unavailable in current deployment mode |


---
title: Create Sandbox
url: "https://docs-v1.opencomputer.dev/api-reference/sandboxes/create.md"
description: "Create a new sandbox. The 1 GB tier provides 1 vCPU on a best-effort basis. For guaranteed CPU allocation, use the 4 GB tier or above. If both cpuCount and..."
---

Create a new sandbox.

<ParamField body="templateID" type="string">
  Template name (default: `"base"`)
</ParamField>

<ParamField body="timeout" type="integer">
  Idle timeout in seconds (default: `300`)
</ParamField>

<ParamField body="cpuCount" type="integer">
  CPU cores. If omitted but `memoryMB` is set, inferred automatically.
</ParamField>

<ParamField body="memoryMB" type="integer">
  Memory in MB. If omitted but `cpuCount` is set, inferred automatically.
</ParamField>

<ParamField body="burst" type="boolean">
  Create a [Burst Sandbox](/sandboxes/burst-sandboxes). Disk is preserved across infrastructure restarts; processes may restart.
</ParamField>

The 1 GB tier provides 1 vCPU on a best-effort basis. For guaranteed CPU allocation, use the 4 GB tier or above.

If both `cpuCount` and `memoryMB` are provided, they must match a platform tier.

<ParamField body="envs" type="object">
  Environment variables as key-value pairs
</ParamField>

<ParamField body="metadata" type="object">
  Arbitrary key-value pairs
</ParamField>

<ParamField body="image" type="object">
  Declarative image manifest (see [Image builder](/sandboxes/templates#image-configuration))
</ParamField>

<ParamField body="snapshot" type="string">
  Name of a pre-built snapshot for instant boot
</ParamField>

<ParamField body="webhooks" type="object[]">
  Register [webhook](/sandboxes/webhooks) destination(s) for this sandbox's lifecycle events, pinned to this sandbox. Registering inline (rather than via a separate call) means the endpoints exist **before** `sandbox.created` / `sandbox.ready` are relayed, so you don't miss the first events.

  * `url` *(string, required)*: HTTPS endpoint.
  * `secret` *(string)*: signing secret; omit and one is generated and returned on the response (`webhooks[].secret`), also re-fetchable later.
  * `eventTypes` *(string\[])*: event-type allow-list (default all).

  Each spec is validated up-front (same rules as [`POST /api/webhooks`](/api-reference/webhooks/create)): a non-HTTPS `url` or unknown `eventTypes` is rejected with `400`. Registration is otherwise **best-effort** — if a spec fails to register downstream, the sandbox is still created; inspect the echoed `webhooks: [{ id, url, secret? }]` to see what registered.
</ParamField>

<ParamField body="previewAuth" type="object">
  Opt in to bearer-token authentication on the sandbox's preview URLs. When set, every request to `https://sb-<id>-p<port>.<domain>` must include an `Authorization: Bearer <token>` (or `X-OC-Preview-Token: <token>`) header; missing or wrong → 401.

  * `scheme` *(string)*: must be `"bearer"`. Reserved for HMAC/JWT later.
  * `token` *(string)*: `"auto"` (or omitted) → server generates a 256-bit random token. An explicit string of at least 16 characters lets you bring your own.

  The plaintext is returned exactly once in the response as `previewAuthToken`; only its SHA-256 hash is stored. Use [`POST /api/sandboxes/{id}/preview/rotate`](/api-reference/preview/rotate-auth) to mint a new one.

  Omit this field for the legacy open behavior — preview URLs respond to anyone who can reach the hostname.
</ParamField>

<Aside>
  <ResponseExample>
    ```json 201
    {
      "sandboxID": "sb-abc123",
      "status": "running",
      "region": "use2",
      "workerID": "w-use2-abc123",
      "previewAuthToken": "qx2sSi5IYXWBvnnRqwK9Ky_cIAI-x0Vx1bPCt0XMxsI",
      "webhooks": [
        { "id": "whk_3f9a2c", "url": "https://app.example.com/oc-webhook", "secret": "whsec_Hk9…" }
      ]
    }
    ```

    `previewAuthToken` is only present when `previewAuth` was set in the request — read it once and store it durably; the server will not return it again. `webhooks` is present only when `webhooks` were requested; each `secret` (`whsec_…`) is returned here and stays re-fetchable via [`GET /api/webhooks/{id}/secret`](/api-reference/webhooks/get).
  </ResponseExample>
</Aside>


---
title: List Sandboxes
url: "https://docs-v1.opencomputer.dev/api-reference/sandboxes/list.md"
description: "List all running sandboxes. Each entry carries tags (always present, empty object when unset) and tagsLastUpdatedAt (null when unset). To filter by tag, use..."
---

List all running sandboxes.

<Aside>
  <ResponseExample>
    ```json 200
    [
      {
        "sandboxID": "sb-abc123",
        "status": "running",
        "region": "use2",
        "workerID": "w-use2-abc123",
        "tags": { "env": "prod" },
        "tagsLastUpdatedAt": "2026-04-19T14:02:00Z"
      }
    ]
    ```
  </ResponseExample>
</Aside>

Each entry carries `tags` (always present, empty object when unset)
and `tagsLastUpdatedAt` (null when unset). To filter by tag, use
[`GET /api/usage?groupBy=sandbox&filter[tag:<key>]=<value>`](/api-reference/usage/get-usage).


---
title: Get Sandbox
url: "https://docs-v1.opencomputer.dev/api-reference/sandboxes/get.md"
description: Get sandbox details by ID. Available on both control plane and worker. tags and tagsLastUpdatedAt are always present — empty object and null respectively when...
---

Get sandbox details by ID. Available on both control plane and worker.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<Aside>
  <ResponseExample>
    ```json 200
    {
      "sandboxID": "sb-abc123",
      "status": "running",
      "region": "use2",
      "workerID": "w-use2-abc123",
      "tags": { "env": "prod", "team": "payments" },
      "tagsLastUpdatedAt": "2026-04-19T14:02:00Z"
    }
    ```
  </ResponseExample>
</Aside>

`tags` and `tagsLastUpdatedAt` are always present — empty object and
`null` respectively when no tags are set. Update them with
[`PUT /api/sandboxes/{id}/tags`](/api-reference/sandboxes/set-tags).


---
title: Kill Sandbox
url: "https://docs-v1.opencomputer.dev/api-reference/sandboxes/delete.md"
description: Terminate and remove a sandbox.
---

Terminate and remove a sandbox.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<Aside>
  <ResponseExample>
    ```json 204
    {}
    ```
  </ResponseExample>
</Aside>


---
title: Set Timeout
url: "https://docs-v1.opencomputer.dev/api-reference/sandboxes/set-timeout.md"
description: Update the idle timeout for a sandbox.
---

Update the idle timeout for a sandbox.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<ParamField body="timeout" type="integer" required>
  New timeout in seconds (must be > 0)
</ParamField>

<Aside>
  <ResponseExample>
    ```json 204
    {}
    ```
  </ResponseExample>
</Aside>


---
title: Hibernate Sandbox
url: "https://docs-v1.opencomputer.dev/api-reference/sandboxes/hibernate.md"
description: Snapshot VM state and stop the sandbox. Control plane only.
---

Snapshot VM state and stop the sandbox. Control plane only.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<Aside>
  <ResponseExample>
    ```json 200
    {
      "sandboxID": "sb-abc123",
      "hibernationKey": "checkpoints/sb-abc123/1234567890.tar.zst",
      "sizeBytes": 134217728,
      "status": "hibernated"
    }
    ```
  </ResponseExample>
</Aside>


---
title: Wake Sandbox
url: "https://docs-v1.opencomputer.dev/api-reference/sandboxes/wake.md"
description: Resume a hibernated sandbox. Control plane only.
---

Resume a hibernated sandbox. Control plane only.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<ParamField body="timeout" type="integer">
  Idle timeout after wake (default: `300`)
</ParamField>

<Aside>
  <ResponseExample>
    ```json 200
    {
      "sandboxID": "sb-abc123",
      "status": "running",
      "region": "use2",
      "workerID": "w-use2-abc123"
    }
    ```
  </ResponseExample>
</Aside>


---
title: Get Sandbox Tags
url: "https://docs-v1.opencomputer.dev/api-reference/sandboxes/get-tags.md"
description: "Read the current tag set for a sandbox. A sandbox with no tags returns \"tags\": {} and \"tagsLastUpdatedAt\": null."
---

Read the current tag set for a sandbox.

<Note>
  **Preview:** Usage and tag APIs are new. Endpoints, response fields,
  and SDK method names may change before GA; temporary inaccuracies or
  rough edges are possible while the surface settles.
</Note>

<ParamField path="id" type="string" required>
  Sandbox ID.
</ParamField>

<Aside>
  <ResponseExample>
    ```json 200
    {
      "tags": { "env": "prod", "team": "payments" },
      "tagsLastUpdatedAt": "2026-04-19T14:02:00Z"
    }
    ```
  </ResponseExample>
</Aside>

A sandbox with no tags returns `"tags": {}` and `"tagsLastUpdatedAt":
null`.


---
title: Set Sandbox Tags
url: "https://docs-v1.opencomputer.dev/api-reference/sandboxes/set-tags.md"
description: "Replace the full tag set for a sandbox. Partial updates are not supported — GET, modify, PUT. {} clears all tags."
---

Replace the full tag set for a sandbox. Partial updates are not
supported — GET, modify, PUT. `{}` clears all tags.

<Note>
  **Preview:** Usage and tag APIs are new. Endpoints, response fields,
  and SDK method names may change before GA; temporary inaccuracies or
  rough edges are possible while the surface settles.
</Note>

<ParamField path="id" type="string" required>
  Sandbox ID.
</ParamField>

<ParamField body="<tagKey>" type="string">
  Each top-level key in the body is a tag name; the value is a
  string. Flat map only — nested objects are rejected.
</ParamField>

### Validation

* At most 50 tag keys per sandbox.
* Keys: 1–128 characters, `A–Z a–z 0–9 _ . - :`. `:` is allowed as
  a user namespace separator.
* Values: 0–256 UTF-8 characters.
* The `oc:` key prefix is reserved for future system-set tags.

<Aside>
  <ResponseExample>
    ```json 200
    {
      "tags": { "env": "staging", "team": "growth" },
      "tagsLastUpdatedAt": "2026-04-22T20:31:14Z"
    }
    ```
  </ResponseExample>
</Aside>

<Note>
  Retagging rewrites attribution going forward: a sandbox re-tagged
  from `team=payments` to `team=growth` will appear under `growth` in
  every subsequent /usage query, including for time before the retag.
  Surface `tagsLastUpdatedAt` alongside usage in dashboards so readers
  can see when tags changed.
</Note>


---
title: Create Webhook
url: "https://docs-v1.opencomputer.dev/api-reference/webhooks/create.md"
description: "Register a webhook destination for sandbox lifecycle events. See Webhooks. Without an Idempotency-Key, each call creates a new destination and returns 201 —..."
---

Register a webhook destination for sandbox lifecycle events. See [Webhooks](/sandboxes/webhooks).

<ParamField body="url" type="string" required>
  HTTPS endpoint to deliver to. (SSRF protection is applied by the delivery provider at send time, not at registration.)
</ParamField>

<ParamField body="eventTypes" type="string[]">
  Event-type allow-list — exact (`sandbox.stopped`) or prefix (`sandbox.*`). Default: all event types. Types outside the sandbox taxonomy are rejected with `400`.
</ParamField>

<ParamField body="sandboxId" type="string">
  Scope to a single sandbox. Omit to receive events for all of the org's sandboxes.
</ParamField>

<ParamField body="secret" type="string">
  Signing secret. Omit and one is generated (`whsec_…`). The secret is returned in the create response and is re-fetchable any time via [`GET /api/webhooks/{id}/secret`](/api-reference/webhooks/get).
</ParamField>

<ParamField body="name" type="string">
  Optional display name for the destination.
</ParamField>

<ParamField body="enabled" type="boolean">
  Whether the destination is active. Default `true`. `false` pauses delivery.
</ParamField>

<ParamField header="Idempotency-Key" type="string">
  Optional. A retried create with the same key **and same body** returns the **same** destination (`200`) instead of a duplicate. Reusing the key with a **different** body is a `409` conflict.
</ParamField>

Without an `Idempotency-Key`, each call creates a **new** destination and returns **`201`** — there is no get-or-create by `name`. With one, a retried call returns the same destination with **`200`**.

**Validation (all `400`):** `url` must be HTTPS; each `eventTypes` entry must be a known sandbox event type or a `prefix.*` wildcard.

<Aside>
  <ResponseExample>
    ```json 201
    {
      "id": "whk_3f9a2c",
      "name": "prod",
      "url": "https://app.example.com/oc-webhook",
      "eventTypes": ["sandbox.stopped"],
      "sandboxId": null,
      "enabled": true,
      "hasSecret": true,
      "secret": "whsec_Hk9…",
      "createdAt": "2026-06-24T12:00:00Z",
      "updatedAt": "2026-06-24T12:00:00Z"
    }
    ```

    The `secret` is returned here and stays re-fetchable via [`GET /api/webhooks/{id}/secret`](/api-reference/webhooks/get) — store it to verify deliveries.
  </ResponseExample>
</Aside>


---
title: List Webhooks
url: "https://docs-v1.opencomputer.dev/api-reference/webhooks/list.md"
description: "List the org's webhook destinations. Deleted destinations are excluded. See Webhooks."
---

List the org's webhook destinations. Deleted destinations are excluded. See [Webhooks](/sandboxes/webhooks).

<Aside>
  <ResponseExample>
    ```json 200
    {
      "data": [
        {
          "id": "whk_3f9a2c",
          "name": "prod",
          "url": "https://app.example.com/oc-webhook",
          "eventTypes": ["sandbox.stopped"],
          "sandboxId": null,
          "enabled": true,
          "hasSecret": true,
          "createdAt": "2026-06-24T12:00:00Z",
          "updatedAt": "2026-06-24T12:00:00Z"
        }
      ]
    }
    ```
  </ResponseExample>
</Aside>


---
title: Get Webhook
url: "https://docs-v1.opencomputer.dev/api-reference/webhooks/get.md"
description: "Fetch one webhook destination. The response carries hasSecret (not the secret itself); fetch the secret value any time via GET /api/webhooks/{id}/secret → {..."
---

Fetch one webhook destination. The response carries `hasSecret` (not the secret itself); fetch the secret value any time via `GET /api/webhooks/{id}/secret` → `{ "secret": "whsec_…" }`. See [Webhooks](/sandboxes/webhooks).

<ParamField path="id" type="string" required>
  Destination ID (`whk_…`).
</ParamField>

<Aside>
  <ResponseExample>
    ```json 200
    {
      "id": "whk_3f9a2c",
      "name": "prod",
      "url": "https://app.example.com/oc-webhook",
      "eventTypes": ["sandbox.stopped"],
      "sandboxId": null,
      "enabled": true,
      "hasSecret": true,
      "createdAt": "2026-06-24T12:00:00Z",
      "updatedAt": "2026-06-24T12:00:00Z"
    }
    ```
  </ResponseExample>
</Aside>


---
title: Update Webhook
url: "https://docs-v1.opencomputer.dev/api-reference/webhooks/update.md"
description: "Update a destination — pause/resume, retune filters, change the URL, or rotate the secret. See Webhooks. sandboxId (scope) is immutable — set it at create; it..."
---

Update a destination — pause/resume, retune filters, change the URL, or rotate the secret. See [Webhooks](/sandboxes/webhooks).

<ParamField path="id" type="string" required>
  Destination ID (`whk_…`).
</ParamField>

<ParamField body="url" type="string">
  New HTTPS endpoint.
</ParamField>

<ParamField body="eventTypes" type="string[]">
  Replace the event-type allow-list (unknown types are rejected with `400`). Pass `null` to clear it (deliver all types).
</ParamField>

<ParamField body="enabled" type="boolean">
  Pause (`false`) or resume (`true`) delivery. Pausing does **not** drop events — events that occur while paused are queued and delivered when you re-enable.
</ParamField>

<ParamField body="rotateSecret" type="boolean">
  Rotate to a **new** generated signing secret, returned as `secret` in the response. The previous secret stays valid for a short rollover window so in-flight deliveries still verify. (To set a specific secret, create a new destination.)
</ParamField>

<ParamField body="name" type="string">
  Rename the destination.
</ParamField>

`sandboxId` (scope) is **immutable** — set it at create; it can't be changed here. The current signing secret is re-fetchable any time via [`GET /api/webhooks/{id}/secret`](/api-reference/webhooks/get).

<Aside>
  <ResponseExample>
    ```json 200
    {
      "id": "whk_3f9a2c",
      "name": "prod",
      "url": "https://app.example.com/oc-webhook",
      "eventTypes": ["sandbox.stopped", "sandbox.ready"],
      "sandboxId": null,
      "enabled": true,
      "hasSecret": true,
      "createdAt": "2026-06-24T12:00:00Z",
      "updatedAt": "2026-06-24T12:05:00Z"
    }
    ```
  </ResponseExample>
</Aside>


---
title: Delete Webhook
url: "https://docs-v1.opencomputer.dev/api-reference/webhooks/delete.md"
description: "Delete a destination. It stops receiving events, drops out of list, and its delivery-provider endpoint is removed. Delivery history for a deleted destination..."
---

Delete a destination. It stops receiving events, drops out of [list](/api-reference/webhooks/list), and its delivery-provider endpoint is removed. Delivery history for a deleted destination is no longer queryable. See [Webhooks](/sandboxes/webhooks).

<ParamField path="id" type="string" required>
  Destination ID (`whk_…`).
</ParamField>

<Aside>
  <ResponseExample>
    ```json 204
    ```
  </ResponseExample>
</Aside>


---
title: Test Webhook
url: "https://docs-v1.opencomputer.dev/api-reference/webhooks/test.md"
description: "Enqueue a sample event to exercise your endpoint. It sends a real, signed message of a concrete type the destination is subscribed to (the first of its..."
---

Enqueue a sample event to exercise your endpoint. It sends a real, signed message of a concrete type
the destination is subscribed to (the first of its `eventTypes`, defaulting to `sandbox.created`),
carrying the **normal delivery envelope** with `event.data.test = true` — so you exercise your real
verifier and parser. Delivery is **asynchronous** — this returns once the message is accepted; check
[deliveries](/api-reference/webhooks/deliveries-list) for the outcome.

<Note>
  The sample is published through your org's webhook app, so it is **not** isolated to this one
  destination: any other destination subscribed to the same event type (and matching scope) also
  receives it (likewise marked `event.data.test = true`).
</Note>

See [Webhooks](/sandboxes/webhooks).

<ParamField path="id" type="string" required>
  Destination ID (`whk_…`).
</ParamField>

<Aside>
  <ResponseExample>
    ```json 200
    {
      "ok": true,
      "eventType": "sandbox.created",
      "messageId": "msg_2aB…"
    }
    ```
  </ResponseExample>
</Aside>

`messageId` is the delivery's message id — find its attempt under [deliveries](/api-reference/webhooks/deliveries-list).


---
title: List Deliveries
url: "https://docs-v1.opencomputer.dev/api-reference/webhooks/deliveries-list.md"
description: "List a destination's most recent delivery attempts (up to 50). Each entry is a delivery-provider attempt record. The attempt index lags actual delivery by a..."
---

List a destination's most recent delivery attempts (up to 50). Each entry is a delivery-provider
attempt record. The attempt index lags actual delivery by a few seconds, and resolving a single
message ([get](/api-reference/webhooks/delivery-get)) / [redeliver](/api-reference/webhooks/redeliver)
can lag longer (\~30s+) — retry with backoff. See [Webhooks](/sandboxes/webhooks#deliveries).

<ParamField path="id" type="string" required>
  Destination ID (`whk_…`).
</ParamField>

<ResponseField name="data[].id" type="string">
  The message id — the key for [get](/api-reference/webhooks/delivery-get) / [redeliver](/api-reference/webhooks/redeliver); equals the `svix-id` header on the delivery.
</ResponseField>

<ResponseField name="data[].attemptId" type="string">
  The id of this specific attempt.
</ResponseField>

<ResponseField name="data[].status" type="string">
  One of `success`, `pending`, or `failed`.
</ResponseField>

<ResponseField name="data[].responseStatusCode" type="integer">
  The consumer's HTTP response code, when the attempt reached it.
</ResponseField>

<Aside>
  <ResponseExample>
    ```json 200
    {
      "data": [
        {
          "id": "msg_2aB…",
          "attemptId": "atmpt_9fE…",
          "status": "success",
          "responseStatusCode": 200,
          "timestamp": "2026-06-24T12:00:01Z"
        }
      ]
    }
    ```
  </ResponseExample>
</Aside>


---
title: Get Delivery
url: "https://docs-v1.opencomputer.dev/api-reference/webhooks/delivery-get.md"
description: Fetch one delivered message by id. See Webhooks.
---

Fetch one delivered message by id. See [Webhooks](/sandboxes/webhooks#deliveries).

<Note>
  The delivery provider's attempt index and message store are eventually consistent: a message id can
  appear in [list deliveries](/api-reference/webhooks/deliveries-list) **before** this endpoint (and
  [redeliver](/api-reference/webhooks/redeliver)) can resolve it — observed up to \~30s+ after the
  attempt. So a `404` shortly after a delivery is transient — **retry with backoff** rather than
  treating it as final.
</Note>

<ParamField path="id" type="string" required>
  Destination ID (`whk_…`).
</ParamField>

<ParamField path="deliveryId" type="string" required>
  Message id (`msg_…`, the `id` from [list deliveries](/api-reference/webhooks/deliveries-list); equals the `svix-id` header).
</ParamField>

<Aside>
  <ResponseExample>
    ```json 200
    {
      "id": "msg_2aB…",
      "eventType": "sandbox.stopped",
      "eventId": "sb-3f9a…:sandbox.stopped",
      "payload": {
        "type": "sandbox.stopped",
        "sandboxId": "sb-3f9a…",
        "eventId": "sb-3f9a…:sandbox.stopped",
        "event": { "id": "sb-3f9a…:sandbox.stopped", "ts": "2026-06-24T12:00:00Z", "orgId": "org_…", "sandboxId": "sb-3f9a…", "type": "sandbox.stopped", "data": { "reason": "user_requested" } }
      },
      "timestamp": "2026-06-24T12:00:00Z"
    }
    ```
  </ResponseExample>
</Aside>


---
title: Redeliver
url: "https://docs-v1.opencomputer.dev/api-reference/webhooks/redeliver.md"
description: "Re-send a message to the endpoint. The redelivery carries the same svix-id, so a receiver that dedupes treats it as the same message. Use it when the original..."
---

Re-send a message to the endpoint. The redelivery carries the **same** `svix-id`, so a receiver that dedupes treats it as the same message. Use it when the original never landed. See [Webhooks](/sandboxes/webhooks#deliveries).

<ParamField path="id" type="string" required>
  Destination ID (`whk_…`).
</ParamField>

<ParamField path="deliveryId" type="string" required>
  Message id (`msg_…`, from [list deliveries](/api-reference/webhooks/deliveries-list)).
</ParamField>

<Aside>
  <ResponseExample>
    ```json 200
    {
      "ok": true
    }
    ```
  </ResponseExample>
</Aside>

The new attempt appears under [deliveries](/api-reference/webhooks/deliveries-list).


---
title: Usage Aggregator
url: "https://docs-v1.opencomputer.dev/api-reference/usage/get-usage.md"
description: Aggregate usage grouped by sandbox or by tag. Dollars are not exposed — the platform returns the same physical quantities the invoice is computed from. For...
---

Aggregate usage grouped by sandbox or by tag. Dollars are not exposed
— the platform returns the same physical quantities the invoice is
computed from. For per-sandbox time-series drilldown (memory
utilization over time, 1-minute resolution), see
[`GET /api/sandboxes/:id/usage`](/api-reference/usage/get-sandbox-usage).

<Note>
  **Preview:** Usage and tag APIs are new. Endpoints, response fields,
  and SDK method names may change before GA; temporary inaccuracies or
  rough edges are possible while the surface settles.
</Note>

<Note>
  All `*GbSeconds` fields are **GiB-seconds** (binary, 2<sup>30</sup>
  bytes/GiB), not decimal GB. For a sandbox provisioned at 1024 MiB
  running for one second, `memoryGbSeconds` is `1`.
</Note>

<Note>
  Freshness: accurate to the minute under normal conditions. A sandbox
  that exited uncleanly continues to accrue against its provisioned
  tier until the platform reconciles state — the same behavior the
  billing pipeline uses, so the numbers here match the invoice.
</Note>

## Request

<ParamField query="groupBy" type="string" required>
  `sandbox` for top sandboxes, or `tag:<key>` for usage by tag value.
  Keys may contain `:` — everything after the first `:` is the tag
  key (so `groupBy=tag:team:payments` groups by the key
  `team:payments`).
</ParamField>

<ParamField query="from" type="string">
  Inclusive lower bound. Accepts either an ISO date (`2026-05-27`,
  interpreted as UTC midnight) or an RFC3339 timestamp. Default: now
  minus 30 days. Max window: 90 days.
</ParamField>

<ParamField query="to" type="string">
  Exclusive upper bound. Same accepted formats as `from`. Default:
  now.
</ParamField>

<ParamField query="filter[tag:<key>]" type="string">
  One param per dimension. Comma-separated values are OR'd within
  that dimension; different `filter[...]` params are AND'd across
  dimensions. Passing the same `filter[...]` key twice returns 400 —
  put multiple OR values in one comma-separated string instead. Empty
  value (`filter[tag:team]=`) matches sandboxes without the `team`
  key.
</ParamField>

<ParamField query="sort" type="string">
  `-memoryGbSeconds` (default) or `-diskOverageGbSeconds`.
</ParamField>

<ParamField query="limit" type="integer">
  Max items per page. Default 50, max 500.
</ParamField>

<ParamField query="cursor" type="string">
  Opaque cursor from a prior response's `nextCursor`.
</ParamField>

## Response

<ResponseField name="from" type="string" required>
  RFC3339 echo of the effective window lower bound.
</ResponseField>

<ResponseField name="to" type="string" required>
  RFC3339 echo of the effective window upper bound.
</ResponseField>

<ResponseField name="groupBy" type="string" required>
  Echo of the requested `groupBy` value.
</ResponseField>

<ResponseField name="total" type="object" required>
  Aggregate across **all rows matching the window and filters**, not
  just the current page. Use this for headline numbers; sum `items[]`
  if you need a per-page subtotal instead.

  <Expandable title="properties">
    <ResponseField name="memoryGbSeconds" type="number" required>
      Total provisioned memory × time across matching sandboxes, in
      GiB-seconds.
    </ResponseField>

    <ResponseField name="diskOverageGbSeconds" type="number" required>
      Total disk-overage × time. Disk usage within the free
      allowance does not contribute; this is the billable excess.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="items" type="object[]" required>
  Paginated rows, sorted by `sort` (default: largest first).

  When `groupBy=sandbox`, each row is one sandbox:

  <Expandable title="groupBy=sandbox row">
    <ResponseField name="sandboxId" type="string" required>
      The sandbox identifier.
    </ResponseField>

    <ResponseField name="alias" type="string">
      Display name set at create time. Absent when none was provided.
    </ResponseField>

    <ResponseField name="status" type="string">
      Current sandbox status (e.g., `running`, `hibernated`,
      `stopped`). Absent when no session exists.
    </ResponseField>

    <ResponseField name="tags" type="object" required>
      Current tag set on the sandbox. Empty object when none.
    </ResponseField>

    <ResponseField name="tagsLastUpdatedAt" type="string">
      RFC3339 timestamp of the most recent tag change, or `null` if
      no tags exist.
    </ResponseField>

    <ResponseField name="memoryGbSeconds" type="number" required>
      Provisioned memory × time for this sandbox in the window.
    </ResponseField>

    <ResponseField name="diskOverageGbSeconds" type="number" required>
      Disk overage × time for this sandbox.
    </ResponseField>
  </Expandable>

  When `groupBy=tag:<key>`, each row is one tag value:

  <Expandable title="groupBy=tag row">
    <ResponseField name="tagKey" type="string" required>
      The tag key from the request (e.g., `team`).
    </ResponseField>

    <ResponseField name="tagValue" type="string" required>
      The tag value for this group.
    </ResponseField>

    <ResponseField name="memoryGbSeconds" type="number" required>
      Total provisioned memory × time across sandboxes carrying
      `tagKey=tagValue`.
    </ResponseField>

    <ResponseField name="diskOverageGbSeconds" type="number" required>
      Total disk overage × time for the same group.
    </ResponseField>

    <ResponseField name="sandboxCount" type="integer" required>
      Distinct sandboxes contributing to this row.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="untagged" type="object">
  Only present when `groupBy=tag:<key>`. Sibling bucket for sandboxes
  that lack the grouping key — without it, untagged sandboxes would
  silently disappear from the rollup. Same shape as a tag row minus
  `tagKey`/`tagValue`.

  <Expandable title="properties">
    <ResponseField name="memoryGbSeconds" type="number" required>
      Total provisioned memory × time across untagged sandboxes.
    </ResponseField>

    <ResponseField name="diskOverageGbSeconds" type="number" required>
      Total disk overage × time.
    </ResponseField>

    <ResponseField name="sandboxCount" type="integer" required>
      Distinct sandboxes in the untagged bucket.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="nextCursor" type="string">
  Opaque cursor to pass as `cursor=` on the next request, or `null`
  when no further pages exist.
</ResponseField>

<Aside>
  <ResponseExample>
    ```json groupBy=sandbox
    {
      "from": "2026-03-23T00:00:00Z",
      "to":   "2026-04-22T00:00:00Z",
      "groupBy": "sandbox",
      "total": { "memoryGbSeconds": 20000, "diskOverageGbSeconds": 360 },
      "items": [
        {
          "sandboxId": "sb-abc",
          "alias": "my-agent",
          "status": "running",
          "tags": { "env": "prod", "team": "payments" },
          "tagsLastUpdatedAt": "2026-04-19T14:02:00Z",
          "memoryGbSeconds": 8000,
          "diskOverageGbSeconds": 120
        }
      ],
      "nextCursor": null
    }
    ```

    ```json groupBy=tag:team
    {
      "from": "2026-03-23T00:00:00Z",
      "to":   "2026-04-22T00:00:00Z",
      "groupBy": "tag:team",
      "total":    { "memoryGbSeconds": 19000, "diskOverageGbSeconds": 340 },
      "untagged": { "memoryGbSeconds": 1000,  "diskOverageGbSeconds":  20, "sandboxCount": 2 },
      "items": [
        { "tagKey": "team", "tagValue": "payments",
          "memoryGbSeconds": 8000, "diskOverageGbSeconds": 120, "sandboxCount": 12 },
        { "tagKey": "team", "tagValue": "growth",
          "memoryGbSeconds": 4000, "diskOverageGbSeconds":  50, "sandboxCount":  5 }
      ],
      "nextCursor": null
    }
    ```
  </ResponseExample>
</Aside>


---
title: Sandbox Usage
url: "https://docs-v1.opencomputer.dev/api-reference/usage/get-sandbox-usage.md"
description: "Per-sandbox memory usage over a window, as 1-minute points plus envelope totals. Allocated memory is the tier the sandbox was provisioned at, integrated over..."
---

Per-sandbox memory usage over a window, as 1-minute points plus
envelope totals. Allocated memory is the tier the sandbox was
provisioned at, integrated over time; used memory is the actual
resident memory consumed by the sandbox process, sampled every 60s.

<Note>
  **Preview:** Usage and tag APIs are new. Endpoints, response fields,
  and SDK method names may change before GA; temporary inaccuracies or
  rough edges are possible while the surface settles.
</Note>

## Request

<ParamField path="id" type="string" required>
  Sandbox ID.
</ParamField>

<ParamField query="from" type="string">
  Lower bound. Accepts either an ISO date (`2026-05-27`, interpreted
  as UTC midnight) or an RFC3339 timestamp. Default: now minus 1 hour.
</ParamField>

<ParamField query="to" type="string">
  Upper bound. Same accepted formats as `from`. Default: now. Window
  must be ≤ 30 days.
</ParamField>

## Response

<ResponseField name="sandboxId" type="string" required>
  Echo of the requested sandbox ID.
</ResponseField>

<ResponseField name="alias" type="string">
  The sandbox's display name (set at create time). Absent if no alias
  was provided.
</ResponseField>

<ResponseField name="from" type="string" required>
  RFC3339 lower bound of the window the response covers — either the
  caller's `from`, or the server default (`now - 1h`) when omitted.
</ResponseField>

<ResponseField name="to" type="string" required>
  RFC3339 upper bound. Either the caller's `to`, or the server
  default (`now`) when omitted.
</ResponseField>

<ResponseField name="totals" type="object" required>
  Envelope totals for the full window. Each additive field equals the
  sum of the matching field across `points[]` — useful for getting
  the headline numbers without iterating.

  <Expandable title="properties">
    <ResponseField name="memoryAllocatedGbSeconds" type="number" required>
      Total GiB-seconds the sandbox was provisioned for. This is the
      physical quantity the bill is computed from. For a sandbox at
      1 GiB running for 1 hour, this is `3600`.
    </ResponseField>

    <ResponseField name="memoryUsedGbSeconds" type="number" required>
      Total GiB-seconds the sandbox actually consumed (integrated
      resident memory). Compare against `memoryAllocatedGbSeconds` to
      see headroom — `1 - used/allocated` is wasted provisioning.
    </ResponseField>

    <ResponseField name="uptimeSeconds" type="integer" required>
      Number of seconds the sandbox was provisioned (i.e., had an
      open scale event) within the window. A 1-hour window where the
      sandbox ran the whole time returns `3600`.
    </ResponseField>

    <ResponseField name="memoryAllocatedPeakMb" type="integer" required>
      Maximum provisioned memory tier (MiB) seen in the window.
      Reflects the highest size the sandbox was resized to.
    </ResponseField>

    <ResponseField name="memoryUsedPeakMb" type="integer" required>
      Maximum measured resident memory (MiB) in any sample during the
      window. Useful for detecting workloads that briefly approached
      their tier ceiling.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="points" type="object[]" required>
  Time-ordered array of 1-minute buckets covering `[from, to)`.
  Buckets are minute-aligned in UTC. Boundary buckets are clamped to
  the original window — a mid-minute `from` produces a partial first
  bucket whose integrals reflect only the overlap with the requested
  range.

  Minutes where the sandbox was not provisioned (before first start,
  after final stop) appear with all fields at `0` rather than as
  missing entries — gaps read as continuous flat zero on a chart.

  <Expandable title="properties">
    <ResponseField name="ts" type="string" required>
      RFC3339 timestamp of the bucket start (minute-aligned, UTC).
      The bucket covers `[ts, ts + 1 minute)`.
    </ResponseField>

    <ResponseField name="memoryAllocatedGbSeconds" type="number" required>
      Provisioned memory integrated over this minute, in GiB-seconds.
      Summing this across all points reproduces
      `totals.memoryAllocatedGbSeconds` exactly.
    </ResponseField>

    <ResponseField name="memoryUsedGbSeconds" type="number" required>
      Measured resident memory integrated over this minute, in
      GiB-seconds. Same compositional property — summing reproduces
      `totals.memoryUsedGbSeconds`.
    </ResponseField>

    <ResponseField name="uptimeSeconds" type="integer" required>
      Seconds within this minute the sandbox was provisioned. Usually
      `60`; `0` when not running; a fractional value (e.g., `30`)
      appears in the resize or start/stop bucket where the sandbox
      began or ended part-way through the minute.
    </ResponseField>

    <ResponseField name="allocatedMemoryMb" type="integer" required>
      Time-weighted average of the provisioned memory tier (MiB)
      across this bucket. When a resize falls inside the bucket, this
      shows the blend (e.g., 30s at 2048 + 30s at 4096 → `3072`).
      Designed to render directly as a step line on a chart.
    </ResponseField>

    <ResponseField name="usedMemoryMbAvg" type="integer" required>
      Average measured resident memory (MiB) across samples that fell
      in this bucket. The natural Y value for a "memory used" chart.
    </ResponseField>

    <ResponseField name="usedMemoryMbPeak" type="integer" required>
      Maximum measured resident memory (MiB) across samples in this
      bucket. Catches sub-minute spikes that the average smooths out.
    </ResponseField>
  </Expandable>
</ResponseField>

### Units

All `*GbSeconds` fields are **GiB-seconds** (binary, 2<sup>30</sup>
bytes/GiB), not decimal GB. For a sandbox provisioned at 1024 MiB
running for one minute, `memoryAllocatedGbSeconds` is
`1024/1024 × 60 = 60`. For a measured RSS of 612 MiB,
`memoryUsedGbSeconds` is `612/1024 × 60 ≈ 35.86`.

<Aside>
  <ResponseExample>
    ```json 200
    {
      "sandboxId": "sb-abc",
      "alias": "my-agent",
      "from":   "2026-05-27T00:00:00Z",
      "to":     "2026-05-28T00:00:00Z",
      "totals": {
        "memoryAllocatedGbSeconds":  86400,
        "memoryUsedGbSeconds":       51640,
        "uptimeSeconds":             86400,
        "memoryAllocatedPeakMb":      1024,
        "memoryUsedPeakMb":            742
      },
      "points": [
        {
          "ts":                       "2026-05-27T00:00:00Z",
          "memoryAllocatedGbSeconds":      60.0,
          "memoryUsedGbSeconds":           35.86,
          "uptimeSeconds":                 60,
          "allocatedMemoryMb":           1024,
          "usedMemoryMbAvg":              612,
          "usedMemoryMbPeak":             720
        },
        {
          "ts":                       "2026-05-27T00:01:00Z",
          "memoryAllocatedGbSeconds":      60.0,
          "memoryUsedGbSeconds":           40.02,
          "uptimeSeconds":                 60,
          "allocatedMemoryMb":           1024,
          "usedMemoryMbAvg":              683,
          "usedMemoryMbPeak":             742
        }
      ]
    }
    ```
  </ResponseExample>
</Aside>

### Use the aggregator for cross-sandbox questions

To rank sandboxes by usage, use [`GET /api/usage`](/api-reference/usage/get-usage)
— that endpoint serves cross-sandbox aggregates and supports
filter/sort/cursor. The per-sandbox endpoint here is for "look at
this one sandbox in detail."


---
title: List Tag Keys
url: "https://docs-v1.opencomputer.dev/api-reference/usage/list-tags.md"
description: "List all tag keys set on any sandbox in the org, with per-key counts of tagged sandboxes and distinct values. Useful for building group-by pickers without..."
---

List all tag keys set on any sandbox in the org, with per-key counts
of tagged sandboxes and distinct values. Useful for building
group-by pickers without querying the full tag set.

<Note>
  **Preview:** Usage and tag APIs are new. Endpoints, response fields,
  and SDK method names may change before GA; temporary inaccuracies or
  rough edges are possible while the surface settles.
</Note>

<Aside>
  <ResponseExample>
    ```json 200
    {
      "keys": [
        { "key": "team", "sandboxCount": 17, "valueCount": 4 },
        { "key": "env",  "sandboxCount": 23, "valueCount": 3 }
      ]
    }
    ```
  </ResponseExample>
</Aside>

<Note>
  Tag keys on torn-down sandboxes are retained so historical drilldowns
  keep resolving. As a result `sandboxCount` may include sandboxes
  that are no longer active.
</Note>


---
title: Get Calendar
url: "https://docs-v1.opencomputer.dev/api-reference/capacity/get-calendar.md"
description: "Return a planning snapshot of 15-minute UTC intervals in the requested window. One row per interval with current reservedGb, reservableGb, and the per-org..."
---

Return a planning snapshot of 15-minute UTC intervals in the requested window.
One row per interval with current `reservedGb`, `reservableGb`, and the
per-org `reservationLimitGb`. See
[Reading the calendar](/reserved-capacity/calendar) for field semantics.

<ParamField query="from" type="string" required>
  Window start. RFC 3339, aligned to 15 minutes, UTC.
</ParamField>

<ParamField query="to" type="string" required>
  Window end. RFC 3339, aligned to 15 minutes, UTC.
</ParamField>

<Aside>
  <ResponseExample>
    ```json 200
    {
      "from":      "2026-04-29T02:00:00Z",
      "to":        "2026-04-29T04:00:00Z",
      "resource":  "memory_gb",
      "intervals": [
        {
          "startsAt": "2026-04-29T02:00:00Z",
          "endsAt":   "2026-04-29T02:15:00Z",
          "reservationLimitGb": 300,
          "reservedGb": 80,
          "reservableGb": 220
        }
      ]
    }
    ```
  </ResponseExample>
</Aside>


---
title: Create Reservation
url: "https://docs-v1.opencomputer.dev/api-reference/capacity/create-reservations.md"
description: Commit capacity across one or more 15-minute UTC intervals in a single atomic write. The result is one reservation event with a server-generated reservationId...
---

Commit capacity across one or more 15-minute UTC intervals in a single
atomic write. The result is one reservation event with a server-generated
`reservationId` covering all intervals in the request. See
[Reserving capacity](/reserved-capacity/reserving) for the full write
contract.

Send an `Idempotency-Key` header so retries return the original result.
Same key with a different body returns `idempotency_key_conflict`.

Reservations are non-refundable — there is no cancellation, modification,
or transfer.

<ParamField body="intervals" type="array" required>
  One entry per 15-minute interval to reserve.
</ParamField>

<ParamField body="intervals[].startsAt" type="string" required>
  Interval start. RFC 3339, UTC, aligned to 15 minutes (`:00`, `:15`, `:30`, `:45`).
</ParamField>

<ParamField body="intervals[].endsAt" type="string" required>
  Interval end. Must equal `startsAt + 15 minutes`. Multi-interval spans are rejected.
</ParamField>

<ParamField body="intervals[].capacityGb" type="integer" required>
  Positive multiple of 4 (the grain is 1 GB-hour = 4 GB × 15 min). Other
  values are rejected.
</ParamField>

<Aside>
  <ResponseExample>
    ```json 200
    {
      "reservationId": "9f67b8f7-7b91-4d2d-b1cb-19d0d0a14562",
      "createdAt": "2026-04-28T18:00:05Z",
      "intervals": [
        {
          "startsAt": "2026-04-29T02:00:00Z",
          "endsAt":   "2026-04-29T02:15:00Z",
          "capacityGb": 16
        },
        {
          "startsAt": "2026-04-29T02:15:00Z",
          "endsAt":   "2026-04-29T02:30:00Z",
          "capacityGb": 16
        }
      ]
    }
    ```

    ```json 409 capacity_not_available
    {
      "error": "capacity_not_available",
      "intervals": [
        {
          "startsAt": "2026-04-29T02:00:00Z",
          "requestedGb": 80,
          "reservableGb": 28,
          "reason": "insufficient_capacity"
        }
      ]
    }
    ```
  </ResponseExample>
</Aside>


---
title: List Reservations
url: "https://docs-v1.opencomputer.dev/api-reference/capacity/list-reservations.md"
description: "Return reservation events made by the authenticated org, in reverse-chronological order by createdAt. Each reservation row includes all the intervals it..."
---

Return reservation events made by the authenticated org, in
reverse-chronological order by `createdAt`. Each reservation row includes
all the intervals it committed to. Useful for end-of-month reconciliation
and audit.

<ParamField query="from" type="string" required>
  Return reservations whose `createdAt >= from`. RFC 3339, UTC.
</ParamField>

<ParamField query="to" type="string" required>
  Return reservations whose `createdAt < to`. RFC 3339, UTC.
</ParamField>

<ParamField query="cursor" type="string">
  Pagination cursor from a previous response's `nextCursor`.
</ParamField>

<ParamField query="limit" type="integer">
  Maximum reservations per page. Server-defined ceiling applies.
</ParamField>

<Aside>
  <ResponseExample>
    ```json 200
    {
      "from": "2026-04-01T00:00:00Z",
      "to":   "2026-05-01T00:00:00Z",
      "reservations": [
        {
          "reservationId": "9f67b8f7-7b91-4d2d-b1cb-19d0d0a14562",
          "createdAt": "2026-04-28T18:00:05Z",
          "intervals": [
            {
              "startsAt": "2026-04-29T02:00:00Z",
              "endsAt":   "2026-04-29T02:15:00Z",
              "capacityGb": 16
            },
            {
              "startsAt": "2026-04-29T02:15:00Z",
              "endsAt":   "2026-04-29T02:30:00Z",
              "capacityGb": 16
            }
          ]
        }
      ],
      "nextCursor": null
    }
    ```
  </ResponseExample>
</Aside>


---
title: Run Command
url: "https://docs-v1.opencomputer.dev/api-reference/exec/run.md"
description: Execute a command synchronously and return the result.
---

Execute a command synchronously and return the result.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<ParamField body="cmd" type="string" required>
  Command to execute
</ParamField>

<ParamField body="args" type="string[]">
  Command arguments
</ParamField>

<ParamField body="envs" type="object">
  Environment variables
</ParamField>

<ParamField body="cwd" type="string">
  Working directory
</ParamField>

<ParamField body="timeout" type="integer">
  Timeout in seconds (default: `60`)
</ParamField>

<Aside>
  <ResponseExample>
    ```json 200
    {
      "exitCode": 0,
      "stdout": "Hello, World!\n",
      "stderr": ""
    }
    ```
  </ResponseExample>
</Aside>


---
title: Create Exec Session
url: "https://docs-v1.opencomputer.dev/api-reference/exec/create-session.md"
description: Start a long-running command as a session. Attach via WebSocket to stream I/O.
---

Start a long-running command as a session. Attach via WebSocket to stream I/O.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<ParamField body="cmd" type="string" required>
  Command to execute
</ParamField>

<ParamField body="args" type="string[]">
  Command arguments
</ParamField>

<ParamField body="envs" type="object">
  Environment variables
</ParamField>

<ParamField body="cwd" type="string">
  Working directory
</ParamField>

<ParamField body="timeout" type="integer">
  Timeout in seconds
</ParamField>

<ParamField body="maxRunAfterDisconnect" type="integer">
  Seconds to keep running after all clients disconnect
</ParamField>

<Aside>
  <ResponseExample>
    ```json 201
    {
      "sessionID": "es-abc123",
      "sandboxID": "sb-abc123",
      "command": "node",
      "args": ["server.js"],
      "running": true,
      "exitCode": null,
      "startedAt": "2025-01-15T10:30:00Z",
      "attachedClients": 0
    }
    ```
  </ResponseExample>
</Aside>


---
title: List Exec Sessions
url: "https://docs-v1.opencomputer.dev/api-reference/exec/list-sessions.md"
description: List all exec sessions for a sandbox.
---

List all exec sessions for a sandbox.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<Aside>
  <ResponseExample>
    ```json 200
    [
      {
        "sessionID": "es-abc123",
        "sandboxID": "sb-abc123",
        "command": "node",
        "args": ["server.js"],
        "running": true,
        "exitCode": null,
        "startedAt": "2025-01-15T10:30:00Z",
        "attachedClients": 0
      }
    ]
    ```
  </ResponseExample>
</Aside>


---
title: Kill Exec Session
url: "https://docs-v1.opencomputer.dev/api-reference/exec/kill-session.md"
description: Kill an exec session.
---

Kill an exec session.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<ParamField path="sessionID" type="string" required>
  Exec session ID
</ParamField>

<ParamField body="signal" type="integer">
  Signal number (default: `9` / SIGKILL)
</ParamField>

<Aside>
  <ResponseExample>
    ```json 204
    {}
    ```
  </ResponseExample>
</Aside>


---
title: Read File
url: "https://docs-v1.opencomputer.dev/api-reference/files/read.md"
description: Returns file content as plain text (not JSON).
---

Returns file content as plain text (not JSON).

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<ParamField query="path" type="string" required>
  Absolute path to the file
</ParamField>

<Aside>
  <ResponseExample>
    ```text 200
    Hello, World!
    ```
  </ResponseExample>
</Aside>


---
title: Write File
url: "https://docs-v1.opencomputer.dev/api-reference/files/write.md"
description: Write content to a file. The request body is written as raw file content (not JSON).
---

Write content to a file. The request body is written as raw file content (not JSON).

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<ParamField query="path" type="string" required>
  Absolute path for the destination file
</ParamField>

<Aside>
  <ResponseExample>
    ```json 204
    {}
    ```
  </ResponseExample>
</Aside>


---
title: List Directory
url: "https://docs-v1.opencomputer.dev/api-reference/files/list-directory.md"
description: List contents of a directory.
---

List contents of a directory.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<ParamField query="path" type="string">
  Directory path (default: `/`)
</ParamField>

<Aside>
  <ResponseExample>
    ```json 200
    [
      { "name": "app", "isDir": true, "size": 4096, "path": "/app" },
      { "name": "README.md", "isDir": false, "size": 1234, "path": "/README.md" }
    ]
    ```
  </ResponseExample>
</Aside>


---
title: Create Directory
url: "https://docs-v1.opencomputer.dev/api-reference/files/mkdir.md"
description: Create a new directory.
---

Create a new directory.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<ParamField query="path" type="string" required>
  Path for the new directory
</ParamField>

<Aside>
  <ResponseExample>
    ```json 204
    {}
    ```
  </ResponseExample>
</Aside>


---
title: Delete File or Directory
url: "https://docs-v1.opencomputer.dev/api-reference/files/delete.md"
description: Delete a file or directory.
---

Delete a file or directory.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<ParamField query="path" type="string" required>
  Path to the file or directory to delete
</ParamField>

<Aside>
  <ResponseExample>
    ```json 204
    {}
    ```
  </ResponseExample>
</Aside>


---
title: Generate Download URL
url: "https://docs-v1.opencomputer.dev/api-reference/files/generate-download-url.md"
description: Generates a signed URL for downloading a file without an API key. See Signed URLs for usage guide.
---

Generates a signed URL for downloading a file without an API key. See [Signed URLs](/sandboxes/signed-urls) for usage guide.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<ParamField body="path" type="string" required>
  Absolute path to the file
</ParamField>

<ParamField body="expiresIn" type="number" default="3600">
  URL lifetime in seconds (max: 86400)
</ParamField>

<Aside>
  <ResponseExample>
    ```json 200
    {
      "url": "https://app.opencomputer.dev/api/sandboxes/sb-xxx/files/download?path=%2Fapp%2Foutput.zip&expires=1773906434&signature=abc123",
      "expiresAt": "2025-01-01T01:00:00Z"
    }
    ```
  </ResponseExample>
</Aside>


---
title: Generate Upload URL
url: "https://docs-v1.opencomputer.dev/api-reference/files/generate-upload-url.md"
description: Generates a signed URL for uploading a file without an API key. See Signed URLs for usage guide.
---

Generates a signed URL for uploading a file without an API key. See [Signed URLs](/sandboxes/signed-urls) for usage guide.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<ParamField body="path" type="string" required>
  Absolute path for the destination file
</ParamField>

<ParamField body="expiresIn" type="number" default="3600">
  URL lifetime in seconds (max: 86400)
</ParamField>

<Aside>
  <ResponseExample>
    ```json 200
    {
      "url": "https://app.opencomputer.dev/api/sandboxes/sb-xxx/files/upload?path=%2Fapp%2Finput.csv&expires=1773906434&signature=abc123",
      "expiresAt": "2025-01-01T01:00:00Z"
    }
    ```
  </ResponseExample>
</Aside>


---
title: Signed Download
url: "https://docs-v1.opencomputer.dev/api-reference/files/signed-download.md"
description: Download a file using a signed URL. No authentication required — the signature and expiry are validated from query parameters. Generate a signed URL first via...
---

Download a file using a signed URL. No authentication required — the signature and expiry are validated from query parameters.

Generate a signed URL first via [Generate Download URL](/api-reference/files/generate-download-url).

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<ParamField query="path" type="string" required>
  Absolute path to the file
</ParamField>

<ParamField query="expires" type="string" required>
  Unix timestamp when the URL expires
</ParamField>

<ParamField query="signature" type="string" required>
  HMAC-SHA256 signature
</ParamField>

<Aside>
  <ResponseExample>
    ```text 200
    (file content with Content-Length header)
    ```

    ```json 403
    {
      "error": "signed URL has expired"
    }
    ```
  </ResponseExample>
</Aside>


---
title: Signed Upload
url: "https://docs-v1.opencomputer.dev/api-reference/files/signed-upload.md"
description: Upload a file using a signed URL. No authentication required — the signature and expiry are validated from query parameters. Send file content as the request...
---

Upload a file using a signed URL. No authentication required — the signature and expiry are validated from query parameters. Send file content as the request body.

Generate a signed URL first via [Generate Upload URL](/api-reference/files/generate-upload-url).

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<ParamField query="path" type="string" required>
  Absolute path for the destination file
</ParamField>

<ParamField query="expires" type="string" required>
  Unix timestamp when the URL expires
</ParamField>

<ParamField query="signature" type="string" required>
  HMAC-SHA256 signature
</ParamField>

<Aside>
  <ResponseExample>
    ```text 204
    (no content)
    ```

    ```json 403
    {
      "error": "invalid signature"
    }
    ```
  </ResponseExample>
</Aside>


---
title: Create Checkpoint
url: "https://docs-v1.opencomputer.dev/api-reference/checkpoints/create.md"
description: "Create a checkpoint of the sandbox state. Each sandbox can have up to 10 full checkpoints and up to 100 disk-only checkpoints. By default, creating past the..."
---

Create a checkpoint of the sandbox state. Each sandbox can have up to 10 full
checkpoints and up to 100 disk-only checkpoints. By default, creating past the
limit for that checkpoint type returns an error. Set `retentionPolicy.mode` to
`delete_oldest` to delete the oldest eligible checkpoint of the same type first
so the new checkpoint can be created.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<ParamField body="name" type="string" required>
  Checkpoint name (unique per sandbox)
</ParamField>

<ParamField body="kind" type="string" default="full">
  Checkpoint type. Use `full` to preserve disk, memory, and CPU state, or
  `disk_only` to preserve only disk state with a larger per-sandbox limit.
</ParamField>

<ParamField body="retentionPolicy" type="object">
  Optional retention policy. Use `{ "mode": "delete_oldest", "maxCount": 10 }`
  for full checkpoints or `{ "mode": "delete_oldest", "maxCount": 100 }` for
  disk-only checkpoints to delete the oldest eligible checkpoint of the same
  type before creating a new one.
</ParamField>

<Aside>
  <ResponseExample>
    ```json 201
    {
      "id": "cp-abc123",
      "sandboxID": "sb-abc123",
      "name": "before-migration",
      "status": "processing",
      "sizeBytes": 0,
      "createdAt": "2025-01-15T10:30:00Z"
    }
    ```
  </ResponseExample>
</Aside>


---
title: List Checkpoints
url: "https://docs-v1.opencomputer.dev/api-reference/checkpoints/list.md"
description: List all checkpoints for a sandbox.
---

List all checkpoints for a sandbox.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<Aside>
  <ResponseExample>
    ```json 200
    [
      {
        "id": "cp-abc123",
        "sandboxID": "sb-abc123",
        "name": "before-migration",
        "status": "ready",
        "sizeBytes": 134217728,
        "createdAt": "2025-01-15T10:30:00Z"
      }
    ]
    ```
  </ResponseExample>
</Aside>


---
title: Restore Checkpoint
url: "https://docs-v1.opencomputer.dev/api-reference/checkpoints/restore.md"
description: Revert the sandbox in-place to a checkpoint. All changes since the checkpoint are lost.
---

Revert the sandbox in-place to a checkpoint. All changes since the checkpoint are lost.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<ParamField path="checkpointId" type="string" required>
  Checkpoint ID
</ParamField>

<Aside>
  <ResponseExample>
    ```json 200
    {}
    ```
  </ResponseExample>
</Aside>


---
title: Fork from Checkpoint
url: "https://docs-v1.opencomputer.dev/api-reference/checkpoints/fork.md"
description: Create a new sandbox from a checkpoint.
---

Create a new sandbox from a checkpoint.

<ParamField path="checkpointId" type="string" required>
  Checkpoint ID
</ParamField>

<ParamField body="timeout" type="integer">
  Idle timeout for the new sandbox (default: `300`)
</ParamField>

<ParamField body="envs" type="object">
  Environment variables to override on the fork. Keys that match the checkpoint's stored envs are replaced; new keys are added.
</ParamField>

<ParamField body="secretStore" type="string">
  Name of a secret store to attach. If the checkpoint already has a store, secrets are merged — the new store's values win on collision and egress allowlists are aggregated.
</ParamField>

<ParamField body="memoryMB" type="integer">
  Memory for the forked sandbox, in MB. Clamped to a valid range: the floor is the checkpoint's own memory (a fork can't start smaller than the snapshot it restores, so a smaller value is ignored) and the ceiling is 16384 (16 GB; larger values are capped). The `memoryMB` field in the response reports the effective value after clamping.
</ParamField>

<Aside>
  <ResponseExample>
    ```json 201
    {
      "sandboxID": "sb-def456",
      "status": "running",
      "region": "use2",
      "workerID": "w-use2-abc123",
      "memoryMB": 4096
    }
    ```
  </ResponseExample>
</Aside>


---
title: Delete Checkpoint
url: "https://docs-v1.opencomputer.dev/api-reference/checkpoints/delete.md"
description: Delete a checkpoint.
---

Delete a checkpoint.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<ParamField path="checkpointId" type="string" required>
  Checkpoint ID
</ParamField>

<Aside>
  <ResponseExample>
    ```json 204
    {}
    ```
  </ResponseExample>
</Aside>


---
title: Create Patch
url: "https://docs-v1.opencomputer.dev/api-reference/patches/create.md"
description: Create a patch script that runs when a sandbox is spawned from a checkpoint. Patches apply in sequence order.
---

Create a patch script that runs when a sandbox is spawned from a checkpoint. Patches apply in sequence order.

<ParamField path="checkpointId" type="string" required>
  Checkpoint ID
</ParamField>

<ParamField body="script" type="string" required>
  Bash script to execute on spawn
</ParamField>

<ParamField body="description" type="string">
  Human-readable description
</ParamField>

<Aside>
  <ResponseExample>
    ```json 201
    {
      "patch": {
        "id": "pa-abc123",
        "checkpointId": "cp-abc123",
        "script": "apt install -y curl",
        "description": "Install curl",
        "strategy": "on_wake",
        "sequence": 1,
        "createdAt": "2025-01-15T10:30:00Z"
      }
    }
    ```
  </ResponseExample>
</Aside>


---
title: List Patches
url: "https://docs-v1.opencomputer.dev/api-reference/patches/list.md"
description: List all patches for a checkpoint.
---

List all patches for a checkpoint.

<ParamField path="checkpointId" type="string" required>
  Checkpoint ID
</ParamField>

<Aside>
  <ResponseExample>
    ```json 200
    [
      {
        "id": "pa-abc123",
        "checkpointId": "cp-abc123",
        "script": "apt install -y curl",
        "description": "Install curl",
        "strategy": "on_wake",
        "sequence": 1,
        "createdAt": "2025-01-15T10:30:00Z"
      }
    ]
    ```
  </ResponseExample>
</Aside>


---
title: Delete Patch
url: "https://docs-v1.opencomputer.dev/api-reference/patches/delete.md"
description: Delete a patch.
---

Delete a patch.

<ParamField path="checkpointId" type="string" required>
  Checkpoint ID
</ParamField>

<ParamField path="patchId" type="string" required>
  Patch ID
</ParamField>

<Aside>
  <ResponseExample>
    ```json 204
    {}
    ```
  </ResponseExample>
</Aside>


---
title: Create Preview URL
url: "https://docs-v1.opencomputer.dev/api-reference/preview/create.md"
description: Create a preview URL to expose a port from the sandbox.
---

Create a preview URL to expose a port from the sandbox.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<ParamField body="port" type="integer" required>
  Container port (1–65535)
</ParamField>

<ParamField body="domain" type="string">
  Custom domain
</ParamField>

<ParamField body="authConfig" type="object">
  Authentication configuration
</ParamField>

<Aside>
  <ResponseExample>
    ```json 201
    {
      "id": "pv-abc123",
      "sandboxId": "sb-abc123",
      "hostname": "sb-abc123-p3000.workers.opencomputer.dev",
      "port": 3000,
      "sslStatus": "active",
      "createdAt": "2025-01-15T10:30:00Z"
    }
    ```
  </ResponseExample>
</Aside>


---
title: List Preview URLs
url: "https://docs-v1.opencomputer.dev/api-reference/preview/list.md"
description: List all preview URLs for a sandbox.
---

List all preview URLs for a sandbox.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<Aside>
  <ResponseExample>
    ```json 200
    [
      {
        "id": "pv-abc123",
        "sandboxId": "sb-abc123",
        "hostname": "sb-abc123-p3000.workers.opencomputer.dev",
        "port": 3000,
        "sslStatus": "active",
        "createdAt": "2025-01-15T10:30:00Z"
      }
    ]
    ```
  </ResponseExample>
</Aside>


---
title: Delete Preview URL
url: "https://docs-v1.opencomputer.dev/api-reference/preview/delete.md"
description: Delete a preview URL.
---

Delete a preview URL.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<ParamField path="port" type="string" required>
  Port number
</ParamField>

<Aside>
  <ResponseExample>
    ```json 204
    {}
    ```
  </ResponseExample>
</Aside>


---
title: Rotate Preview-URL Auth Token
url: "https://docs-v1.opencomputer.dev/api-reference/preview/rotate-auth.md"
description: "Mint a new bearer token for the sandbox's preview-URL auth gate. The old token stops working immediately — there is no zero-downtime dual-token mode, so roll..."
---

Mint a new bearer token for the sandbox's preview-URL auth gate. The old token stops working immediately — there is no zero-downtime dual-token mode, so roll the new token out to your caller before discarding the old one.

If the sandbox was originally created without [`previewAuth`](/api-reference/sandboxes/create), this call installs a token and starts enforcing the gate from that point on.

The plaintext is returned exactly once in the response. Only the SHA-256 hash is persisted server-side; the server cannot show you the token again later.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<Aside>
  <ResponseExample>
    ```json 200
    {
      "previewAuthToken": "qx2sSi5IYXWBvnnRqwK9Ky_cIAI-x0Vx1bPCt0XMxsI",
      "scheme": "bearer"
    }
    ```
  </ResponseExample>
</Aside>

### Errors

| Status | When                                                  |
| ------ | ----------------------------------------------------- |
| `404`  | Sandbox not found, or owned by a different org        |
| `410`  | Sandbox is `stopped` or `error`                       |
| `503`  | Database not configured (combined-mode CP without PG) |


---
title: Create Snapshot
url: "https://docs-v1.opencomputer.dev/api-reference/snapshots/create.md"
description: Create a pre-built sandbox environment from a declarative image manifest.
---

Create a pre-built sandbox environment from a declarative image manifest.

<ParamField body="name" type="string" required>
  Unique snapshot name
</ParamField>

<ParamField body="image" type="object" required>
  Declarative image manifest (see [Image builder](/sandboxes/templates#image-configuration))
</ParamField>

<Aside>
  <ResponseExample>
    ```json 201
    {
      "id": "snap-abc123",
      "name": "data-science",
      "status": "building",
      "contentHash": "sha256:...",
      "checkpointId": "",
      "manifest": { "steps": [] },
      "createdAt": "2025-01-15T10:30:00Z",
      "lastUsedAt": ""
    }
    ```
  </ResponseExample>
</Aside>


---
title: List Snapshots
url: "https://docs-v1.opencomputer.dev/api-reference/snapshots/list.md"
description: List all snapshots.
---

List all snapshots.

<Aside>
  <ResponseExample>
    ```json 200
    [
      {
        "id": "snap-abc123",
        "name": "data-science",
        "status": "ready",
        "contentHash": "sha256:...",
        "checkpointId": "cp-abc123",
        "manifest": { "steps": [] },
        "createdAt": "2025-01-15T10:30:00Z",
        "lastUsedAt": "2025-01-16T08:00:00Z"
      }
    ]
    ```
  </ResponseExample>
</Aside>


---
title: Get Snapshot
url: "https://docs-v1.opencomputer.dev/api-reference/snapshots/get.md"
description: Get snapshot details by name.
---

Get snapshot details by name.

<ParamField path="name" type="string" required>
  Snapshot name
</ParamField>

<Aside>
  <ResponseExample>
    ```json 200
    {
      "id": "snap-abc123",
      "name": "data-science",
      "status": "ready",
      "contentHash": "sha256:...",
      "checkpointId": "cp-abc123",
      "manifest": { "steps": [] },
      "createdAt": "2025-01-15T10:30:00Z",
      "lastUsedAt": "2025-01-16T08:00:00Z"
    }
    ```
  </ResponseExample>
</Aside>


---
title: Delete Snapshot
url: "https://docs-v1.opencomputer.dev/api-reference/snapshots/delete.md"
description: Delete a snapshot.
---

Delete a snapshot.

<ParamField path="name" type="string" required>
  Snapshot name
</ParamField>

<Aside>
  <ResponseExample>
    ```json 204
    {}
    ```
  </ResponseExample>
</Aside>


---
title: Create PTY Session
url: "https://docs-v1.opencomputer.dev/api-reference/pty/create.md"
description: Create an interactive terminal session.
---

Create an interactive terminal session.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<ParamField body="cols" type="integer">
  Terminal columns (default: `80`)
</ParamField>

<ParamField body="rows" type="integer">
  Terminal rows (default: `24`)
</ParamField>

<ParamField body="shell" type="string">
  Shell path (default: `/bin/bash`)
</ParamField>

<Aside>
  <ResponseExample>
    ```json 201
    {
      "sessionID": "ps-abc123",
      "sandboxID": "sb-abc123"
    }
    ```
  </ResponseExample>
</Aside>


---
title: Resize PTY
url: "https://docs-v1.opencomputer.dev/api-reference/pty/resize.md"
description: Resize a PTY session.
---

Resize a PTY session.

<Note>
  PTY resize is HTTP-only — not exposed in the TypeScript or Python SDKs. The SDKs handle resize automatically for interactive sessions.
</Note>

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<ParamField path="sessionID" type="string" required>
  PTY session ID
</ParamField>

<ParamField body="cols" type="integer" required>
  New column count
</ParamField>

<ParamField body="rows" type="integer" required>
  New row count
</ParamField>

<Aside>
  <ResponseExample>
    ```json 200
    {}
    ```
  </ResponseExample>
</Aside>


---
title: Kill PTY Session
url: "https://docs-v1.opencomputer.dev/api-reference/pty/kill.md"
description: Terminate a PTY session.
---

Terminate a PTY session.

<ParamField path="id" type="string" required>
  Sandbox ID
</ParamField>

<ParamField path="sessionID" type="string" required>
  PTY session ID
</ParamField>

<Aside>
  <ResponseExample>
    ```json 204
    {}
    ```
  </ResponseExample>
</Aside>


---
title: Troubleshooting
url: "https://docs-v1.opencomputer.dev/troubleshooting.md"
description: Common errors and debugging tips
---

## Common Errors

### `401 Unauthorized`

Your API key is missing or invalid.

* Check that `OPENCOMPUTER_API_KEY` is set: `echo $OPENCOMPUTER_API_KEY`
* Verify the key in your code matches what's in the dashboard
* CLI users: run `oc config show` to check the stored key

### `Sandbox not found`

The sandbox may have been killed or timed out (auto-hibernated or stopped).

```bash
oc ls                     # list all sandboxes
oc sandbox get sb-abc123  # check a specific sandbox
```

In code:

```typescript
const running = await sandbox.isRunning();
```

### `Connection refused`

The sandbox is still starting up, or it's hibernated and hasn't finished waking.

* Check the sandbox status — if `hibernated`, call `wake()` or `oc sandbox wake`
* If just created, wait a moment for the VM to boot (\~150ms typical)

### Timeout errors

**Command timeout:** Increase the `timeout` parameter on `exec.run()` or use `--timeout` with `oc exec`. Default is 60s for SDK `exec.run()`, unlimited for CLI without `--wait`.

**Idle timeout:** Sandboxes auto-hibernate after 300s of inactivity. Increase with `sandbox.setTimeout()` or `oc sandbox set-timeout`. The timeout resets on every operation.

### Agent exits immediately

* Check that the sandbox has the Claude Agent SDK installed (the default template includes it)
* Verify `ANTHROPIC_API_KEY` is available — pass it via `envs` on sandbox create if the agent needs it
* Use the `onError` callback to capture stderr from the agent process

## Debugging Tips

### Check sandbox status

```bash
oc sandbox get sb-abc123
```

```typescript
console.log(sandbox.status);
const running = await sandbox.isRunning();
```

### List running processes

```bash
oc exec sb-abc123 --wait -- ps aux
```

```typescript
const sessions = await sandbox.exec.list();
for (const s of sessions) {
  console.log(s.sessionID, s.running, s.command);
}
```

### Capture agent errors

```typescript
const session = await sandbox.agent.start({
  prompt: "...",
  onEvent: (event) => {
    if (event.type === "error") console.error("Agent error:", event.message);
  },
  onError: (data) => console.error("Stderr:", data),
});
```

### Read sandbox logs

```bash
oc exec sb-abc123 --wait -- cat /var/log/syslog
oc exec sb-abc123 --wait -- journalctl --no-pager -n 50
```

## Getting Help

* **GitHub Issues:** [github.com/diggerhq/opencomputer/issues](https://github.com/diggerhq/opencomputer/issues)
* **API Reference:** [HTTP API](/reference/api) · [TypeScript SDK](/reference/typescript-sdk) · [Python SDK](/reference/python-sdk) · [CLI](/reference/cli)


---
title: Self-hosting
url: "https://docs-v1.opencomputer.dev/self-hosting/overview.md"
description: Understand the OpenComputer architecture and choose a development or scalable deployment shape.
---

OpenComputer can run inside your own cloud account. A self-hosted deployment
combines an API control plane with Linux hosts that use QEMU/KVM to run isolated
sandbox VMs.

Self-hosting is an operator workflow. You are responsible for provisioning the
cloud resources, securing the API, operating the data stores, and upgrading the
OpenComputer services.

<CardGroup cols={2}>
  <Card title="GCP development cell" icon="google" href="/self-hosting/gcp-development">
    Run a complete single-cell development environment on one nested-KVM Compute Engine VM.
  </Card>

  <Card title="Configuration reference" icon="sliders" href="https://github.com/diggerhq/opencomputer/blob/main/deploy/server.env.example">
    Review the control-plane environment-variable shape.
  </Card>
</CardGroup>

## Architecture

OpenComputer has three runtime tiers:

```text
Client, SDK, CLI, or dashboard
                |
                | HTTP
                v
+---------------+----------------+
| Control plane                  |
|                                |
| API and authentication         |
| Sandbox lifecycle and routing  |
| Worker coordination            |
+---------------+----------------+
                |
                | worker RPC
                v
+---------------+----------------+
| Data plane worker              |
|                                |
| QEMU/KVM VM lifecycle          |
| Networking and storage         |
| Checkpoint and hibernation     |
+---------------+----------------+
                |
                | virtio serial / vsock
                v
+---------------+----------------+
| Sandbox VM                     |
|                                |
| Full Linux filesystem          |
| OpenComputer in-VM agent       |
| User commands and processes    |
+--------------------------------+
```

### Control plane

The control plane exposes the OpenComputer HTTP API. It authenticates requests,
tracks sandbox state, chooses a worker, and routes operations to the worker that
owns each sandbox.

The control-plane binary is named `opensandbox-server`. The historical
`opensandbox` name also appears in environment variables as the
`OPENSANDBOX_*` prefix.

### Data plane

Data-plane workers run on Linux machines with KVM. Each worker starts QEMU
microVMs, configures their networking and disks, and maintains the connection
to the agent inside each VM.

The worker binary is named `opensandbox-worker`. A worker registers itself with
the control plane through Redis and advertises its capacity and RPC address.

### In-VM agent

Every sandbox image contains `osb-agent`. It receives command, file, terminal,
and process operations from the worker over a VM-local transport. User
workloads do not need direct access to the worker host.

## Request lifecycle

When a client creates a sandbox:

1. The control plane authenticates the request and selects a worker with
   capacity.
2. The worker creates a copy-on-write disk from the selected rootfs image.
3. QEMU starts the sandbox with KVM acceleration.
4. The worker waits for `osb-agent`, applies sandbox networking and environment
   configuration, and reports the sandbox as running.
5. Subsequent API calls are routed to the owning worker and then to the
   in-VM agent.

OpenComputer can keep paused VMs in a warm pool. Claiming a prepared VM avoids
most cold-start work while preserving VM isolation.

## Deployment shapes

### Single-cell development

A development cell can run every component on one VM:

```text
+--------------------------------------------------+
| One development VM                               |
|                                                  |
| opensandbox-server :8080                         |
| opensandbox-worker                               |
| PostgreSQL + Redis                               |
| QEMU/KVM sandbox VMs                             |
| local rootfs images and sandbox disks            |
+--------------------------------------------------+
```

This is the fastest way to test real VM behavior. It is not highly available,
and local storage can be ephemeral. Use the
[GCP development guide](/self-hosting/gcp-development) for this topology.

### Scalable hosting

A scalable deployment separates the control plane from a pool of worker
machines:

```text
                         +----------------------+
                         | HTTPS load balancer  |
                         +----------+-----------+
                                    |
                         +----------v-----------+
                         | Control plane        |
                         | one or more replicas |
                         +----+-------------+---+
                              |             |
                    +---------v--+       +--v-----------+
                    | PostgreSQL |       | Redis        |
                    +------------+       +--------------+
                              |
                    +---------v-------------------------+
                    | Worker pool                       |
                    | KVM host | KVM host | KVM host    |
                    +------------------+----------------+
                                       |
                              +--------v---------+
                              | Object storage   |
                              | durable VM data  |
                              +------------------+
```

A production-shaped deployment should include:

* Multiple control-plane replicas behind HTTPS.
* A worker pool spread across failure domains.
* Durable PostgreSQL and Redis services.
* S3-compatible object storage for checkpoints, hibernation archives, and
  durable image artifacts.
* A secrets manager for database credentials, signing keys, storage
  credentials, and third-party integrations.
* Monitoring, centralized logs, backups, and tested recovery procedures.
* Private worker networking. Only the control-plane or edge endpoint should be
  public.

The control plane can launch cloud workers using the supported provider
integrations, or workers can be provisioned independently and configured to
register with the cell.

## State and storage

Different data has different durability requirements:

| Data                                     | Recommended storage                          |
| ---------------------------------------- | -------------------------------------------- |
| Organizations, API keys, sandbox records | PostgreSQL                                   |
| Worker registry and coordination         | Redis                                        |
| Checkpoints and hibernation archives     | S3-compatible object storage                 |
| Golden rootfs images                     | Worker image or durable object storage       |
| Active sandbox disks                     | Fast local NVMe or provisioned block storage |

Fast local disks are a good fit for active copy-on-write sandbox disks, but they
must not be the only copy of data that needs to survive a worker replacement.

## Networking and security

At minimum:

* Terminate public traffic with HTTPS.
* Keep worker RPC, Redis, PostgreSQL, and worker HTTP ports on private networks.
* Use a randomly generated API key for development and a managed
  authentication flow for shared environments.
* Store secrets outside source control.
* Restrict administrative SSH access to known source addresses or an identity
  aware access layer.
* Treat preview URLs and sandbox egress as security boundaries, not ordinary
  application routes.

The single-cell development guides intentionally trade availability and
durability for simplicity. Do not expose one directly to untrusted traffic.

## Moving from development to scalable hosting

Use the single-cell environment to validate:

* KVM and sandbox boot.
* The unified rootfs and in-VM agent.
* Command, file, and terminal APIs.
* Checkpoint and wake behavior after object storage is configured.
* Your client and SDK integration.

Before serving production traffic, move PostgreSQL, Redis, and durable artifacts
off the worker host; place the API behind HTTPS; provision replaceable workers;
and test worker loss while sandboxes are running.


---
title: GCP development cell
url: "https://docs-v1.opencomputer.dev/self-hosting/gcp-development.md"
description: Run a complete OpenComputer development environment on a nested-KVM Compute Engine VM.
---

This guide deploys a complete OpenComputer development cell to one Google
Compute Engine VM. The VM runs the control plane, worker, PostgreSQL, Redis, and
QEMU/KVM sandboxes together.

<Warning>
  This topology is for development and performance testing. It has one host, no
  high availability, an HTTP API, and ephemeral Local SSD storage. For the
  production shape, start with the [self-hosting architecture](/self-hosting/overview).
</Warning>

## What gets created

```text
Developer machine
   |
   | SSH and HTTP, restricted by a GCP firewall rule
   v
+------------------------------------------------------+
| Compute Engine VM                                    |
|                                                      |
| opensandbox-server :8080                             |
| opensandbox-worker                                   |
| PostgreSQL + Redis                                   |
| QEMU/KVM sandbox VMs                                 |
|                                                      |
| 50 GB persistent boot disk                           |
| 375 GiB Local NVMe SSD mounted at /data              |
+------------------------------------------------------+
```

The default configuration uses:

| Resource          | Default                                |
| ----------------- | -------------------------------------- |
| Machine type      | `n2-standard-8`                        |
| CPU and memory    | 8 vCPU, 32 GiB                         |
| Operating system  | Ubuntu 24.04 LTS                       |
| Boot disk         | 50 GB `pd-ssd`                         |
| Sandbox data disk | One 375 GiB Local SSD over NVMe        |
| Sandbox runtime   | QEMU with nested KVM                   |
| Sandbox default   | 2 vCPU with dynamically managed memory |
| API port          | `8080`                                 |

Google documents the supported machine families and expected overhead in its
[nested virtualization overview](https://cloud.google.com/compute/docs/instances/nested-virtualization/overview).
Do not substitute an E2, AMD, or Arm machine for this guide.

## Prerequisites

You need:

* A GCP project with billing enabled.
* Permission to enable APIs and create Compute Engine instances and firewall
  rules.
* The current [Google Cloud CLI](https://cloud.google.com/sdk/docs/install-sdk).
* `git`, `curl`, `jq`, `ssh`, `rsync`, and `openssl` on your development
  machine.
* A clone of the OpenComputer repository.

Authenticate and enable Compute Engine:

```bash
export GCP_PROJECT="<your-project-id>"
export GCP_ZONE="us-east4-c"

gcloud auth login
gcloud config set project "$GCP_PROJECT"
gcloud services enable compute.googleapis.com
```

Run the remaining commands from the OpenComputer repository root.

## Configure the deployment

Choose a machine and instance name, then generate a development API key:

```bash
export MACHINE_TYPE="n2-standard-8"
export INSTANCE_NAME="opensandbox-qemu-dev"
export API_KEY="$(openssl rand -hex 32)"

printf 'Save this development API key now: %s\n' "$API_KEY"
```

Keep the API key in a password manager or a local secret file outside the
repository. The deploy script stores only its hash in PostgreSQL.

### Restrict ingress before creating the VM

The deployment script looks for a firewall rule named
`opensandbox-dev-allow`. Create that rule first so the development API and SSH
are not exposed to the internet.

Replace the CIDR below with your current public IP:

```bash
export ADMIN_CIDR="<your-public-ip>/32"

gcloud compute firewall-rules create opensandbox-dev-allow \
  --direction=INGRESS \
  --action=ALLOW \
  --rules=tcp:22,tcp:8080 \
  --source-ranges="$ADMIN_CIDR" \
  --target-tags=opensandbox-dev \
  --description="OpenComputer development cell: SSH and API"
```

If the rule already exists, inspect and tighten it:

```bash
gcloud compute firewall-rules update opensandbox-dev-allow \
  --rules=tcp:22,tcp:8080 \
  --source-ranges="$ADMIN_CIDR"
```

<Warning>
  The cell serves plain HTTP. Keep port `8080` restricted to trusted source
  addresses. Do not expose worker port `8081`, PostgreSQL, Redis, or worker RPC.
</Warning>

## Create the host

Create the VM, enable nested virtualization, format the Local SSD as
reflink-enabled XFS, and install the host dependencies:

```bash
./deploy/gcp/deploy-qemu-dev.sh create
```

The first run:

1. Creates an SSH key if the configured key does not exist.
2. Creates an Ubuntu N2 instance with nested virtualization enabled.
3. Attaches one Local NVMe SSD.
4. Mounts the SSD at `/data`.
5. Installs QEMU/KVM, Docker, Go, PostgreSQL, Redis, and the systemd units.

Confirm KVM before deploying:

```bash
./deploy/gcp/deploy-qemu-dev.sh ssh -- \
  sh -lc 'test -c /dev/kvm && echo "KVM ready" || echo "KVM missing"'
```

Expected output:

```text
KVM ready
```

## Deploy OpenComputer

Build the server, worker, and in-VM agent; build the rootfs images; install the
environment files; start the services; and seed the development API key:

```bash
./deploy/gcp/deploy-qemu-dev.sh deploy
```

The first rootfs build takes several minutes. It emits:

* `default.ext4`: the legacy 4 GiB split-disk base.
* `default-merged.ext4`: the unified 20 GiB base used by new sandboxes.

The unified layout keeps `/`, `/home/sandbox`, and `/workspace` on one virtual
disk. The underlying image is sparse, so its logical 20 GiB size does not
consume 20 GiB of Local SSD space immediately.

### Wait for worker registration

The HTTP health endpoint can become ready before the worker has created its
golden snapshot and registered with the control plane. On a fresh host, allow
approximately one minute after deployment.

Inspect the server log:

```bash
./deploy/gcp/deploy-qemu-dev.sh ssh -- \
  sudo journalctl -u opensandbox-server -n 100 --no-pager
```

Continue after you see a message similar to:

```text
new worker registered: w-qemu-dev-1
```

If sandbox creation returns `503 no workers available`, wait for registration
and retry.

## Validate the cell

Get the public IP and configure your shell:

```bash
export PUBLIC_IP="$(
  gcloud compute instances describe "$INSTANCE_NAME" \
    --zone="$GCP_ZONE" \
    --format='value(networkInterfaces[0].accessConfigs[0].natIP)'
)"
export OPENCOMPUTER_URL="http://${PUBLIC_IP}:8080"
export OPENCOMPUTER_API_KEY="$API_KEY"
```

Check control-plane health:

```bash
curl -fsS "$OPENCOMPUTER_URL/health"
```

Create a sandbox:

```bash
export SANDBOX_ID="$(
  curl -fsS -X POST "$OPENCOMPUTER_URL/api/sandboxes" \
    -H "Content-Type: application/json" \
    -H "X-API-Key: $OPENCOMPUTER_API_KEY" \
    -d '{"templateID":"default"}' |
  jq -r .sandboxID
)"

printf 'Sandbox: %s\n' "$SANDBOX_ID"
```

Run a command inside the VM and verify the unified disk:

```bash
curl -fsS -X POST \
  "$OPENCOMPUTER_URL/api/sandboxes/$SANDBOX_ID/exec/run" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $OPENCOMPUTER_API_KEY" \
  -d '{
    "cmd": "sh",
    "args": [
      "-lc",
      "uname -a; nproc; free -h; df -h / /home/sandbox /workspace; lsblk"
    ],
    "timeout": 30
  }' |
jq
```

For the default unified rootfs, `lsblk` should show one approximately 20 GiB
`vda`, and all three paths should resolve to the same root filesystem.

## Use the dashboard during development

The development deployment enables the explicit `single-tenant` dashboard
mode. On startup, the control plane creates or reuses one local organization
and administrator in PostgreSQL and keeps that development organization on the
Pro plan so hosted free-tier size gates do not restrict local testing. The
dashboard therefore shows the live cell's sandboxes without requiring an
external identity provider.

<Warning>
  Single-tenant mode supplies a persistent local identity but does not
  authenticate individual browser users. Reach the development UI through SSH
  port forwarding and do not expose the Vite port publicly.
</Warning>

### Start the development UI

The deploy command syncs the dashboard source to the VM. In one terminal, run
Vite there through the existing Docker installation:

```bash
gcloud compute ssh "$INSTANCE_NAME" \
  --project="$GCP_PROJECT" \
  --zone="$GCP_ZONE" \
  --command='
    sudo docker run --rm --network host \
      -v "$HOME/opensandbox/web:/app" \
      -w /app \
      node:22-bookworm-slim \
      sh -lc "npm ci && npm run dev -- --host 127.0.0.1"
  '
```

Vite's development proxy reaches the control plane at `localhost:8080` on the
VM, so UI requests use the single-tenant identity and operate on the live
development cell.

### Forward it to your machine

Keep Vite running. In a second local terminal, forward the VM's loopback-only
UI port to port `3000` on your machine:

```bash
gcloud compute ssh "$INSTANCE_NAME" \
  --project="$GCP_PROJECT" \
  --zone="$GCP_ZONE" \
  -- -N \
  -L 127.0.0.1:3000:127.0.0.1:3000 \
  -o ExitOnForwardFailure=yes \
  -o ServerAliveInterval=30
```

Open [http://127.0.0.1:3000](http://127.0.0.1:3000).

Every dashboard action is attributed to the development administrator. You can
customize that identity before deploying:

```bash
export OPENSANDBOX_DASHBOARD_AUTH_MODE="single-tenant"
export OPENSANDBOX_DASHBOARD_USER_EMAIL="admin@example.internal"
export OPENSANDBOX_DASHBOARD_USER_NAME="Development Admin"
export OPENSANDBOX_DASHBOARD_TENANT_NAME="Development"
export OPENSANDBOX_DASHBOARD_TENANT_SLUG="development"

./deploy/gcp/deploy-qemu-dev.sh deploy
```

## Operations

Check status:

```bash
./deploy/gcp/deploy-qemu-dev.sh status
```

Follow logs:

```bash
./deploy/gcp/deploy-qemu-dev.sh ssh -- \
  sudo journalctl -u opensandbox-worker -f
```

Redeploy the current checkout:

```bash
./deploy/gcp/deploy-qemu-dev.sh deploy
```

The deploy command synchronizes the current local checkout to the VM and
restarts the services.

### Rebuild the rootfs

Normal deploys reuse existing rootfs images. When you intentionally need to
test a rootfs change, remove both cached bases and deploy again:

```bash
./deploy/gcp/deploy-qemu-dev.sh ssh -- \
  sudo rm -f \
    /data/firecracker/images/default.ext4 \
    /data/firecracker/images/default-merged.ext4

./deploy/gcp/deploy-qemu-dev.sh deploy
```

This affects only future sandboxes. Delete or recreate existing development
sandboxes separately if you need them to use the new base.

## Storage and shutdown

The 50 GB boot disk is a persistent block disk. The 375 GiB Local SSD mounted
at `/data` holds active sandbox disks and rootfs images.

Google documents that Local SSD data is discarded by default when an instance
is stopped and recommends durable block storage for data that must survive
host replacement. See [About Local SSD disks](https://cloud.google.com/compute/docs/disks/local-ssd).

<Warning>
  Treat `stop`, instance deletion, and host replacement as destructive for
  everything under `/data`. Configure object storage before testing durable
  checkpoints or hibernation, and never keep the only copy of important data on
  the Local SSD.
</Warning>

Delete the development VM when you are finished:

```bash
./deploy/gcp/deploy-qemu-dev.sh destroy
```

The script asks for confirmation before deleting the instance.

## Troubleshooting

### `no workers available`

The server became healthy before worker registration completed. Wait for the
worker registration log, then retry sandbox creation.

### `/dev/kvm` is missing

Confirm that the VM is using a supported Intel machine type, nested
virtualization was enabled at instance creation, and your organization policy
allows nested virtualization.

```bash
gcloud compute instances describe "$INSTANCE_NAME" \
  --zone="$GCP_ZONE" \
  --format='yaml(machineType,advancedMachineFeatures)'
```

### The control-plane root redirects to `localhost:3000`

No built dashboard bundle is installed. Run Vite using one of the dashboard
modes above, or build `web/dist` before packaging the control plane.

### API requests return `401`

Use the same API key that was passed to `deploy`:

```bash
curl -fsS "$OPENCOMPUTER_URL/api/sandboxes" \
  -H "X-API-Key: $OPENCOMPUTER_API_KEY"
```

### The public IP changed

Run `status` to refresh the deployment state, update
`OPENCOMPUTER_URL`, and confirm that the firewall still allows your current
source address.
