divergent/docs

@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

npm install @divergentlabs/sandbox

The source is at 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

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 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 once it is running.

Option Description Default
size small, standard, large or xl. See sizes and prices. 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. {}
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.
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. { 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. 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.

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.

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.

Divergent.fromToken

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

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.

OpenAI

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

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.

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

const screen = mountScreen(document.getElementById('screen')!, { token, sandbox, mode: 'view' })
screen.setMode('control')
screen.disconnect()
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)
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.
esc