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

# Desktop

> Watch a machine's desktop, let an agent drive it, and share a link to it.

Machines from the `agent-browser` template (shown as **standard**) have a Linux desktop with a
browser. To watch it, open a machine in the dashboard and choose the **Computer** tab. Click the
desktop to take control. Control goes back to the agent after a minute without input, or 30 seconds after
you close the tab.

## Let an agent drive it

```ts theme={null}
import { Sandbox } from '@insforge/instabox-sdk'

const sbx = await Sandbox.create('agent-browser')
await sbx.desktop.start({ width: 1280, height: 800 })

await sbx.desktop.open('https://example.com')        // opens the browser
const png = await sbx.desktop.screenshot()           // PNG bytes, e.g. for a vision model
await sbx.desktop.click(640, 400)                    // { button: 'right' }, { double: true }
await sbx.desktop.press(['ctrl', 'l'])               // a key or a chord
await sbx.desktop.type('instabox.sh')                // any text, quotes and unicode included
await sbx.desktop.press('Enter')
await sbx.desktop.scroll({ direction: 'down', amount: 5 })
```

The other calls are `move(x, y)`, `drag([x1, y1], [x2, y2])`, `cursorPosition()`, and `status()`.
`start()` is safe to call again: it leaves a running desktop alone.

> [accordion] A minimal agent loop
>
> ```ts theme={null}
> for (let step = 0; step < 20; step++) {
>   const png = await sbx.desktop.screenshot()
>   const action = await yourModel.nextAction(png)     // { type: 'click', x, y } | { type: 'type', text } | { type: 'done' }
>   if (action.type === 'done') break
>   if (action.type === 'click') await sbx.desktop.click(action.x, action.y)
>   if (action.type === 'type') await sbx.desktop.type(action.text)
> }
> ```

## Share a link to the desktop

`streamUrl()` makes a link to a full-screen viewer of one machine's desktop. The link works without
signing in, and you can embed it in an `<iframe>`.

```ts theme={null}
const { url, expiresAt } = await sbx.desktop.streamUrl({
  viewOnly: true,        // the default: the viewer can watch, not type or click
  ttlSeconds: 3600,      // 60 to 86400; the default is one hour
})
```

```html theme={null}
<iframe src="LINK_URL" width="1280" height="800" style="border:0"></iframe>
```

* **One machine.** A link opens that machine's desktop only. It is not an API key and cannot call the API.
* **View-only is enforced by the server.** Keyboard, mouse, and clipboard input from a view-only link
  never reach the machine. Pass `viewOnly: false` for a link that can control it.
* **Expiry stops new connections.** A viewer that is already connected stays connected until it leaves.
* **Revoke ends everything.** `revokeStreamUrls()` invalidates every link for the machine and closes
  open viewers within a few seconds. Deleting the machine does the same.

> [!WARNING]
>
> Links belong to the machine's creator. Only the creator, signed in or with their personal key, may
> make or revoke them. A workspace API key, or another member of the workspace, gets
> `403 permission_denied`. A personal key reaches only the machines of your personal workspace.

Without a link, two ways work, and both work with a workspace key:

* Open the desktop over `GET /v1/machines/{id}/vnc`, VNC in a WebSocket for a viewer such as noVNC. It
  needs `machines:write`.
* Read the screen with `sbx.desktop.screenshot()`.

## Build a service for your own users

Keep your API key on your server, never in a browser. Make a fresh link each time a user opens their
desktop, and revoke it when their access ends. Links belong to the creator, so this server runs on the
personal key of the account that creates the machines, and it creates them with that key too.

```ts theme={null}
// your server, e.g. Express
app.get('/api/desktop-link', async (req, res) => {
  const machineId = await machineOf(req.user)       // your own user-to-machine mapping
  const sbx = await Sandbox.connect(machineId)
  res.json(await sbx.desktop.streamUrl({ viewOnly: !req.user.canControl, ttlSeconds: 600 }))
})

// when the user's access ends
await (await Sandbox.connect(machineId)).desktop.revokeStreamUrls()
```

To find machines later with `Sandbox.list({ metadata })`, tag them with metadata such as
`{ customer: 'acme' }`.

> [accordion] Relay the desktop with a workspace key
>
> Your server opens `GET /v1/machines/{id}/vnc`, a WebSocket, with a key that has `machines:write`. It
> passes the VNC stream to your user's viewer, or sends them `sbx.desktop.screenshot()` images. Your
> server then decides who sees what, and revoking means closing that connection.
>
> Each user has at most 8 open terminals and desktops at once, and a workspace key counts as its
> workspace's owner. A ninth `/vnc` connection gets a retryable `429 quota_exceeded`, so close a
> viewer's connection when they leave.

## Start and share the desktop over HTTP

```bash theme={null}
H="X-API-Key: $INSTABOX_API_KEY"
# universal and t3 only (no desktop in the image): install, then wait until installed (about half a minute)
curl -s -H "$H" -H 'Content-Type: application/json' \
  -d '{"action": "install"}' "$INSTABOX_API_URL/v1/machines/m_.../desktop"
for i in $(seq 60); do   # up to 5 minutes; stops early on a failed install, and start then says why
  curl -s -H "$H" -H 'Content-Type: application/json' -d '{"action": "status"}' \
    "$INSTABOX_API_URL/v1/machines/m_.../desktop" | jq -e '.installed or .failed' >/dev/null && break
  sleep 5
done
curl -s -H "$H" -H 'Content-Type: application/json' \
  -d '{"action": "start", "width": 1280, "height": 800}' "$INSTABOX_API_URL/v1/machines/m_.../desktop"
curl -s -H "$H" -H 'Content-Type: application/json' \
  -d '{"viewOnly": true, "ttlSeconds": 3600}' "$INSTABOX_API_URL/v1/machines/m_.../desktop/stream"   # {url, expiresAt}
curl -s -X DELETE -H "$H" "$INSTABOX_API_URL/v1/machines/m_.../desktop/stream"                     # revoke
```

Over HTTP, `start` does not install anything. On a template without the desktop (`universal`, `t3`), send
`{"action": "install"}` first, poll `{"action": "status"}` until `installed` is true (about half a minute),
then `start`. A `start` that started nothing answers with `installed` false and a `hint` naming the next call.

Both stream calls belong to the creator, as above: use a personal key with `machines:write`.

> [card] [Computer use](https://instabox.sh/docs/use-cases/computer-use.md)
>
> An agent that browses and clicks on its own machine.

> [card] [SDK quick start](https://instabox.sh/docs/integrations/sdk-quickstart.md)
>
> Create, run and pause machines from code.
