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
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 |
regionis[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.widthandheightare the PNG’s.modifiersis any ofshift,ctrl,altandsuper, joined by+:'ctrl+shift'. The keys are held during the action.keysis a string or an array of up to 50. A key is a name such asReturn,Escape,Tab,BackSpace,Leftora, 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
Originheader. A request with anOriginthat is not in the token’soriginsis refused, and a token minted withoutoriginsrefuses every browser. - A request without an
Originheader, such as one from your server, is not checked againstorigins. Tokens withoutoriginswork 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,forkordestroy.
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. |
toolis{ type: 'computer_toolset_20260801' }. Put it intoolsas it is.run(content)takes an assistant message’s content. It runs everytool_useblock withtoolset_name: 'computer', in order, and resolves with onetool_resultper call, each withtoolset_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.withis_error: true. - It handles all 17 members.
screenshotandzoomanswer a PNG.cursor_positionanswersX=512, Y=384.waitwaits up to 300 seconds and makes no API call. The others answerOK. - A
screenshotwhile 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))
}
computerToolis{ type: 'computer' }, thetoolsentry for the Responses API.computerCallOutput(desktop, call, options?)runs the call’sactions, or its singleaction, in order, then takes a screenshot and resolves with thecomputer_call_outputitem.- Actions:
click,double_click,drag,keypress,move,screenshot,scroll,typeandwait. - Key names in
keypressare translated:CTRLtoctrl,ENTERtoReturn,ARROWLEFTtoLeft.
| 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.
mountScreenreturns{ setMode(mode), disconnect() }.- In
<SandboxScreen>, a newtoken,sandbox,baseUrlorbackgroundreconnects. A newmodekeeps the connection. - With a
viewtoken, the API ignores keyboard and mouse input whatevermodesays. To let the person take control, give the page acontroltoken.
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. |