# TypeScript SDK

> Reference for @divergentlabs/sandbox, with the sandbox client, desktop methods, sandbox tokens, computer-use adapters for Claude, OpenAI and the AI SDK, the embeddable screen, and the HTTP routes they call.

`@divergentlabs/sandbox` creates sandboxes, drives their desktop, runs a model's computer-use tool calls on it, and shows the screen live in your own page.

## Install

```sh
npm install @divergentlabs/sandbox
```

The source is at [github.com/divergentlabsxyz/sandbox](https://github.com/divergentlabsxyz/sandbox), under Apache-2.0.

It has optional peer dependencies. Install the ones for the entry points you import.

| Import | Runs on | Provides | Peer dependency |
| --- | --- | --- | --- |
| `@divergentlabs/sandbox` | server | `Divergent`, `Sandbox`, `Desktop`, `ApiError` | none |
| `@divergentlabs/sandbox/anthropic` | server | `computerToolset` | `@anthropic-ai/sdk` |
| `@divergentlabs/sandbox/openai` | server | `computerTool`, `computerCallOutput` | `openai` |
| `@divergentlabs/sandbox/ai` | server | `computerTools` | `ai` |
| `@divergentlabs/sandbox/screen` | browser | `mountScreen` | `@novnc/novnc` |
| `@divergentlabs/sandbox/react` | browser | `<SandboxScreen>` | `@novnc/novnc`, `react` |

`Divergent.fromToken` from the main entry point also runs in a browser.

## Client

```ts
import { Divergent } from '@divergentlabs/sandbox'

const divergent = new Divergent({ apiKey: process.env.DIVERGENT_API_KEY! })
const sandbox = await divergent.sandboxes.create({ metadata: { user: '42' } })
await sandbox.desktop.start()
```

`new Divergent(options)`

| Option | Description | Default |
| --- | --- | --- |
| `apiKey` | An org API key with the `sandboxes:write` scope. A key from [`div login`](https://docs.divergentlabs.xyz/compute/cli/login.md) has it. Keep it on your server. | required |
| `baseUrl` | API URL. | `https://api.divergentlabs.xyz` |
| `fetch` | A `fetch` implementation. | `globalThis.fetch` |

`divergent.client` is the typed client underneath, for routes this package does not wrap.

### sandboxes.create

`divergent.sandboxes.create(options?)` creates a sandbox and resolves with a [`Sandbox`](#sandbox) once it is running.

| Option | Description | Default |
| --- | --- | --- |
| `size` | `small`, `standard`, `large` or `xl`. See [sizes and prices](https://docs.divergentlabs.xyz/sandboxes/pricing.md). | `small` |
| `name` | Lower case letters, digits and dashes, up to 63 characters. Unique in the org. | `sb-` and six hex digits |
| `metadata` | Labels of your own, such as the user the sandbox is for. See [Metadata](#metadata). | `{}` |
| `idleTimeoutSeconds` | Stop the sandbox after this many seconds without use, between 60 and 604800. `0` means never. | `0` |
| `idempotencyKey` | A retry with the same key resolves with the sandbox the first attempt created. Up to 255 characters. | none |

`create` has no `secrets` option. A token can only be minted for a sandbox without secrets, in any environment, because anyone with a `control` token, and any model that reads pages in the sandbox, can read everything in it.

### sandboxes.get and sandboxes.list

| Call | Resolves with |
| --- | --- |
| `sandboxes.get(idOrName)` | One `Sandbox`. |
| `sandboxes.list({ metadata?, all? })` | `Sandbox[]`, newest first. With `metadata`, only sandboxes that have every key and value given. With `all: true`, deleted sandboxes too. |

### Metadata

- Up to 16 keys.
- Keys are 1 to 40 characters of lowercase letters, digits, `_`, `.` and `-`.
- Values are strings of up to 256 characters.
- A fork has the same metadata as its source.

## Sandbox

| Property | Description |
| --- | --- |
| `id`, `name`, `status`, `metadata` | From the API's last answer. |
| `data` | The full sandbox object from the API's last answer. |
| `desktop` | The sandbox's [`Desktop`](#desktop). |

| Method | Description | Resolves with |
| --- | --- | --- |
| `exec(command, { cwd?, env?, timeoutSeconds? })` | Runs a shell command as root and waits for it. `timeoutSeconds` is up to 600, 60 by default. | `{ exit_code, stdout, stderr, truncated }` |
| `files.read(path)` | Reads one file, up to 64 MB. | `Uint8Array` |
| `files.write(path, contents)` | Writes one file from a string or `Uint8Array`, up to 64 MB. | `{ path, size_bytes }` |
| `createToken({ abilities, expiresIn?, origins? })` | Mints a [sandbox token](#sandbox-tokens). | `{ token, sandbox, abilities, origins, expires_at }` |
| `update({ metadata?, idleTimeoutSeconds? })` | Changes the sandbox. `metadata` replaces every key; `{}` removes them all. | the same `Sandbox` |
| `stop()` | Stops the sandbox. Files and processes are kept. | the same `Sandbox` |
| `resume()` | Resumes a stopped sandbox. | the same `Sandbox` |
| `fork({ name? })` | Creates a running copy with the same files and metadata. | a new `Sandbox` |
| `destroy()` | Deletes the sandbox and its files. | the same `Sandbox` |
| `refresh()` | Reads the sandbox again. | the same `Sandbox` |

## Desktop

`sandbox.desktop` with an API key, or `Divergent.fromToken(...).desktop` with a token.

Positions are screen pixels from the top left, the same pixels a screenshot has. The screen is 1280x800 unless the desktop was started at another resolution.

| Method | Description | Ability |
| --- | --- | --- |
| `status()` | `{ running: false }`, or `{ running: true, size, display, vnc_port, devtools, browser }`. | `view` |
| `screenshot({ region? })` | The screen as a PNG: `{ bytes, width, height }`. | `view` |
| `start({ resolution? })` | Starts the desktop and resolves when the browser is ready, within about 60 seconds. Resolves with the status. Resolution is `WIDTHxHEIGHT`, width 320 to 1920, height 240 to 1080. | `control` |
| `stop()` | Stops the desktop. | `control` |
| `click({ x?, y?, button?, count?, modifiers? })` | Clicks at `x, y`, or where the pointer is when both are left out. `button` is `left`, `middle` or `right`. `count` is 1, 2 or 3. | `control` |
| `move({ x, y })` | Moves the pointer. | `control` |
| `mouseDown()`, `mouseUp()` | Presses or releases the left button where the pointer is. | `control` |
| `drag({ from, to, modifiers? })` | Drags with the left button. `from` and `to` are `{ x, y }`. | `control` |
| `scroll({ direction, amount?, x?, y?, modifiers? })` | Scrolls `up`, `down`, `left` or `right` by `amount` notches, 1 to 100, 3 by default, at `x, y` or where the pointer is. | `control` |
| `type(text)` | Types the text where the keyboard focus is. Up to 10,000 bytes. | `control` |
| `key(keys, { repeat? })` | Presses each key or combination in turn, `repeat` times, 1 to 100. | `control` |
| `hold(keys, seconds)` | Holds the keys down for 0 to 300 seconds. | `control` |
| `cursor()` | The pointer's position: `{ x, y }`. | `control` |
| `open(urls)` | Opens 1 to 20 `http` or `https` URLs as tabs in Chromium. | `control` |
| `accessibility.tree({ app?, depth?, includeHidden? })` | The accessibility tree, in the [format `div sandbox a11y` prints](https://docs.divergentlabs.xyz/sandboxes/cli/sandbox.md#a11y). `depth` is 1 to 200. | `control` |
| `accessibility.act({ ref, action, text? })` | Acts on a node: `press`, `focus`, `set-text`, `read`, or an action the node lists. | `control` |

- `region` is `[x0, y0, x1, y1]`, the top-left and bottom-right corners in screen pixels. The PNG is that part of the screen scaled up to fit the screen size, keeping its aspect ratio. `width` and `height` are the PNG's.
- `modifiers` is any of `shift`, `ctrl`, `alt` and `super`, joined by `+`: `'ctrl+shift'`. The keys are held during the action.
- `keys` is a string or an array of up to 50. A key is a name such as `Return`, `Escape`, `Tab`, `BackSpace`, `Left` or `a`, or a combination joined by `+`: `ctrl+l`, `alt+Tab`.

Every method rejects with `ApiError` when the API refuses. Branch on its `code` and `status`. See [Errors](#errors).

```ts
import { ApiError } from '@divergentlabs/sandbox'

try {
  await sandbox.desktop.click({ x: 640, y: 400 })
} catch (error) {
  if (error instanceof ApiError && error.code === 'no_desktop') await sandbox.desktop.start()
  else throw error
}
```

## Sandbox tokens

A sandbox token opens one sandbox's desktop and screen, and no other route. Mint it on your server and hand it to a browser or a program that must not hold your API key.

```ts
const grant = await sandbox.createToken({
  abilities: ['view'],
  expiresIn: 900,
  origins: ['https://app.example.com'],
})
// Send { token: grant.token, sandbox: grant.sandbox } to the browser.
```

`sandbox.createToken(options)`

| Option | Description | Default |
| --- | --- | --- |
| `abilities` | `['view']` or `['control']`. `control` includes `view`. | required |
| `expiresIn` | Seconds until the token stops working, 60 to 3600. | `900` |
| `origins` | Pages that may use the token in a browser, as a scheme and host with no path: `https://app.example.com`, `http://localhost:5173`. Up to 10. | none |

| Ability | Allows |
| --- | --- |
| `view` | `status`, `screenshot`, and the screen with input ignored. |
| `control` | Every desktop method, and the screen with keyboard and mouse. |

- A browser sends an `Origin` header. A request with an `Origin` that is not in the token's `origins` is refused, and a token minted without `origins` refuses every browser.
- A request without an `Origin` header, such as one from your server, is not checked against `origins`. Tokens without `origins` work on every plan.
- A token stops working when it expires, when the API key that minted it is revoked, when the person who minted it leaves the org, and when the sandbox has secrets.
- No token reaches `exec`, files, SSH, coding agents, `stop`, `resume`, `fork` or `destroy`.

The CLI equivalent is [`div sandbox token`](https://docs.divergentlabs.xyz/sandboxes/cli/sandbox.md#token).

### Divergent.fromToken

```ts
const { desktop, screenUrl } = Divergent.fromToken({ token, sandbox })
const shot = await desktop.screenshot()
```

`Divergent.fromToken(grant, options?)` takes the object `createToken` resolved with, or the token string with `options.sandbox` set to the sandbox's id. `options` also takes `baseUrl` and `fetch`.

It returns a client for that one sandbox:

| Property | Description |
| --- | --- |
| `desktop` | A [`Desktop`](#desktop). Methods the token's ability does not allow reject with `insufficient_ability`. |
| `screenUrl` | The screen's WebSocket URL, with the token in it. |
| `token`, `sandbox`, `baseUrl` | As given. |

## Claude

```ts
import { computerToolset } from '@divergentlabs/sandbox/anthropic'

const toolset = computerToolset(sandbox.desktop, { onAction: (step) => console.log(step.name) })
// Pass toolset.tool in `tools`. After each response:
messages.push({ role: 'assistant', content: response.content })
messages.push({ role: 'user', content: await toolset.run(response.content) })
```

`computerToolset(desktop, options?)` returns `{ tool, run }` for Claude's `computer_toolset_20260801`. Keep one per conversation.

| Option | Description |
| --- | --- |
| `onAction(step)` | Called after each call runs, failures included. `step` is `{ name, input, screenshot?, error? }`. `screenshot` is `{ data, width, height }` with base64 PNG `data`, set for `screenshot` and `zoom`. |
| `resize(png, width, height)` | Shrinks a PNG. Needed only for a screen wider or taller than 2000 pixels on a runtime without `OffscreenCanvas`. |

- `tool` is `{ type: 'computer_toolset_20260801' }`. Put it in `tools` as it is.
- `run(content)` takes an assistant message's content. It runs every `tool_use` block with `toolset_name: 'computer'`, in order, and resolves with one `tool_result` per call, each with `toolset_name: 'computer'`.
- After the first failed call, every later call in the same message is not run and answers `Not executed: an earlier computer action in this turn failed.` with `is_error: true`.
- It handles all 17 members. `screenshot` and `zoom` answer a PNG. `cursor_position` answers `X=512, Y=384`. `wait` waits up to 300 seconds and makes no API call. The others answer `OK`.
- A `screenshot` while the desktop is not running starts it once.
- A screen up to 2000 pixels a side is sent at full size, so positions need no scaling. A larger screen is shrunk to fit, and positions are mapped back to the screen.

A complete loop is in [Add computer use to your product](https://docs.divergentlabs.xyz/sandboxes/computer-use.md#2-run-the-model-on-your-server).

## OpenAI

```ts
import { computerTool, computerCallOutput } from '@divergentlabs/sandbox/openai'

// Pass computerTool in `tools`. After each response:
for (const item of response.output) {
  if (item.type === 'computer_call') input.push(await computerCallOutput(sandbox.desktop, item))
}
```

- `computerTool` is `{ type: 'computer' }`, the `tools` entry for the Responses API.
- `computerCallOutput(desktop, call, options?)` runs the call's `actions`, or its single `action`, in order, then takes a screenshot and resolves with the `computer_call_output` item.
- Actions: `click`, `double_click`, `drag`, `keypress`, `move`, `screenshot`, `scroll`, `type` and `wait`.
- Key names in `keypress` are translated: `CTRL` to `ctrl`, `ENTER` to `Return`, `ARROWLEFT` to `Left`.

| Option | Description | Default |
| --- | --- | --- |
| `acknowledgedSafetyChecks` | The call's `pending_safety_checks` that a person approved. A call with an unapproved check is not run and throws. | none |
| `onAction(action)` | Called after each action runs. | none |
| `waitMs` | How long `wait` waits, in milliseconds. | `1000` |

## AI SDK

```ts
import { generateText } from 'ai'
import { computerTools } from '@divergentlabs/sandbox/ai'

await generateText({ model, tools: computerTools(sandbox.desktop), prompt })
```

`computerTools(desktop)` returns the tools `screenshot`, `click`, `type`, `key`, `scroll`, `move` and `drag`. `screenshot` reaches the model as an image.

## Screen

The screen shows the desktop live in an element of your page. It connects with a sandbox token, so your API key stays on your server. Start the desktop before you mount the screen.

```ts
import { mountScreen } from '@divergentlabs/sandbox/screen'

const screen = mountScreen(document.getElementById('screen')!, { token, sandbox, mode: 'view' })
screen.setMode('control')
screen.disconnect()
```

```tsx
import { SandboxScreen } from '@divergentlabs/sandbox/react'

<SandboxScreen token={token} sandbox={sandbox} mode="view" style={{ aspectRatio: '16 / 10' }} />
```

`mountScreen(element, options)` and the props of `<SandboxScreen>`:

| Option | Description | Default |
| --- | --- | --- |
| `token` | A sandbox token with `view` or `control`. | required |
| `sandbox` | The sandbox's id, as `createToken` resolved with it. | required |
| `mode` | `view` shows the screen. `control` also sends the page's keyboard and mouse, and needs a `control` token. | `view` |
| `baseUrl` | API URL. | `https://api.divergentlabs.xyz` |
| `background` | Colour around the screen where its aspect ratio leaves space. | none |
| `onConnect()` | Called when the screen connects. | none |
| `onDisconnect({ clean })` | Called once, when the connection closes: the token expired, the desktop stopped, or `disconnect` was called. | none |
| `className`, `style` | `<SandboxScreen>` only. The element has no size of its own, so give it one. | none |

- The screen scales to fit its element.
- `mountScreen` returns `{ setMode(mode), disconnect() }`.
- In `<SandboxScreen>`, a new `token`, `sandbox`, `baseUrl` or `background` reconnects. A new `mode` keeps the connection.
- With a `view` token, the API ignores keyboard and mouse input whatever `mode` says. To let the person take control, give the page a `control` token.

## HTTP routes

The SDK and `div` call these routes. Base URL: `https://api.divergentlabs.xyz/api`. Send the API key or the sandbox token as `Authorization: Bearer <key or token>`. Responses are JSON as `{ "data": ... }`, except the screenshot. `:id` is the sandbox's id or name.

| Route | Body or query | Answer | Token ability |
| --- | --- | --- | --- |
| `POST /sandboxes/:id/tokens` | `{ abilities, expires_in?, origins? }` | `201` with `{ token, sandbox, abilities, origins, expires_at }` | API key or dashboard session only |
| `GET /sandboxes/:id/desktop` | none | The status. | `view` |
| `POST /sandboxes/:id/desktop` | `{ action: "start" \| "stop", resolution? }` | The status. | `control` |
| `POST /sandboxes/:id/desktop/input` | `{ action, ... }`, see below | `{ done: "<action>" }`, or `{ x, y }` for `cursor` | `control` |
| `GET /sandboxes/:id/desktop/screenshot` | `?region=x0,y0,x1,y1` | `image/png`, with `x-image-width` and `x-image-height` headers | `view` |
| `GET /sandboxes/:id/desktop/accessibility` | `?app=&depth=&include_hidden=true` | The tree. | `control` |
| `POST /sandboxes/:id/desktop/accessibility` | `{ ref, action, text? }` | The node's result. | `control` |
| `GET /sandboxes/:id/screen?token=<token>` | none | A WebSocket carrying the screen as VNC (RFB) in binary frames. | `view` |

An API key needs the `sandboxes:write` scope for every route here. A sandbox token is refused with `401` on every other route.

**Input actions**

| `action` | Fields |
| --- | --- |
| `click` | `x?`, `y?` (both or neither), `button?` (`left`), `count?` (1 to 3, `1`), `modifiers?` |
| `move` | `x`, `y` |
| `down`, `up` | none |
| `drag` | `x`, `y`, `to_x`, `to_y`, `modifiers?` |
| `scroll` | `direction`, `amount?` (1 to 100, `3`), `x?`, `y?`, `modifiers?` |
| `type` | `text` |
| `key` | `keys` (array), `repeat?` (1 to 100, `1`) |
| `hold` | `keys` (array), `seconds` (0 to 300) |
| `cursor` | none |
| `open` | `urls` (array) |

```bash
curl https://api.divergentlabs.xyz/api/sandboxes/$SB/desktop/input \
  -H "Authorization: Bearer $DIVERGENT_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"action": "click", "x": 640, "y": 400, "count": 2}'
```

## Errors

Errors are `{ "error": { "code", "message" } }`. In the SDK they are `ApiError`, with `code`, `status` and `message`. The `code` does not change. The message says what to do next.

| Code | Status | When | What to do |
| --- | --- | --- | --- |
| `no_desktop` | 409 | The desktop is not running. | Call `desktop.start()`. |
| `desktop_outdated` | 409 | The sandbox was created before this action existed. | Create a new sandbox. |
| `desktop_refused` | 422 | The desktop could not do the action, for example a node that changed since the tree was read. | Read the message. For an accessibility action, read the tree again. |
| `not_running` | 409 | The sandbox is stopped. | Call `sandbox.resume()`. |
| `insufficient_ability` | 403 | A `view` token was used for a `control` method. | Mint a token with `abilities: ['control']`. |
| `origin_refused` | 403 | The page's origin is not in the token's `origins`, or the token has no `origins`. | Mint the token with this page's origin in `origins`. |
| `sandbox_has_secrets` | 409 | The sandbox has secrets. Minting is refused, and so is every request with a token for it. | Create a sandbox without secrets. |
| `invalid_request` | 422 | A field is missing or out of range. `param` names it. | Fix the field. |
| `unauthorized` | 401 | The key or token is missing or invalid, the token expired, it is for another sandbox, or the key that minted it was revoked. | Mint a new token. |
| `not_found` | 404 | No sandbox with this id in the org. | Check the id. |
