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

# Browser

> An optional managed browser attached to the computer — domain-scoped, write-guarded, and driven through high-level actions.

A [computer](/docs/computer/index) can have an **optional managed browser** attached. It is a real browser operated by Vetta's browser automation interface and driven — by the agent's `browser` tool, or by you over [REST](/docs/api/browser), the [SDK](/docs/sdk/browser) and the [CLI](/docs/cli/browser) — through a small set of high-level actions. Every action is domain-scoped. A session bills its minutes from open to close on the `browser` line item; the model-driven actions add their own spend on the same line.

The agent never receives the underlying connect URL or the live-view URL — only the action results.

## Session creation

Attaching a browser opens a **session**. Creation is **two-phase and orphan-safe**: Vetta writes a `creating` row before asking the browser automation interface for an upstream session, then promotes it to `active` on success or marks it `error` on failure. If the process dies mid-create, the `creating` row is retired as `error` by the timeout sweep once its `timeout_minutes` elapse, and the upstream session — opened with the same timeout — expires on its own; nothing is billed for it.

<Steps>
  <Step title="creating">A row is written first, before any upstream session exists.</Step>
  <Step title="active">The upstream session is up; the row is promoted and the agent can drive it.</Step>
  <Step title="closed">Closed by `DELETE`, by the agent, or by the sweep once `timeout_minutes` elapse — billed either way.</Step>
  <Step title="error">Creation failed, or never finished; the row records it and bills nothing.</Step>
</Steps>

Over REST the first action opens the session lazily; `GET /v1/computers/{id}/browser` reads it and `DELETE` closes it.

## Attach a browser

Enable a browser when you create the computer. It reaches the open web unless you pass `allowed_domains`, which scopes every page it navigates to.

<CodeGroup>
  ```bash CLI theme={"system"}
  vetta computer create --name box \
    --allowed-domains "app.example.com,*.example.com" \
    --timeout-minutes 20 \
    --allow-writes
  ```

  ```typescript TypeScript theme={"system"}
  const computer = await vetta.computers.create({
    name: "box",
    browser: {
      allowed_domains: ["app.example.com", "*.example.com"],
      timeout_minutes: 20,
      allow_writes: true,
    },
  });
  ```

  ```bash cURL theme={"system"}
  curl https://api.vetta.sh/v1/computers \
    -H "Authorization: Bearer $VETTA_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "box",
      "browser": {
        "allowed_domains": ["app.example.com", "*.example.com"],
        "timeout_minutes": 20,
        "allow_writes": true
      }
    }'
  ```
</CodeGroup>

## Actions

Two kinds of action drive the page. **Deterministic** actions take a URL or a CSS selector and do exactly that. **Plain-language** actions take an `instruction`; a model reads the live page and carries it out.

| Action       | Kind           | What it does                                                                                                                                                                                     |
| ------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `goto`       | deterministic  | Go to a URL.                                                                                                                                                                                     |
| `click`      | deterministic  | Click the element a CSS selector names.                                                                                                                                                          |
| `type`       | deterministic  | Type text into the element a CSS selector names.                                                                                                                                                 |
| `links`      | deterministic  | List every link on the page as `{ href, text }`, optionally only those whose URL or text `contains` a string, at most `limit` (default 50, max 200).                                             |
| `screenshot` | deterministic  | Capture a PNG. It is stored as a [file](/docs/capabilities/files) and the agent is given its `fil_` id — and the picture itself, [as a link](#a-screenshot-the-agent-can-see).                        |
| `extract`    | either         | With a `selector` or nothing: the page's text (or that element's). With an `instruction`: what the instruction asks for, as `text` — or, given a JSON `schema`, as `data` shaped by it.          |
| `act`        | plain-language | Do one thing on the page ("open the pricing menu", "choose the second date"). Returns `performed: true`; if nothing could be done the action fails with `provider_error`, never a quiet success. |
| `observe`    | plain-language | Find the elements an instruction describes, as `elements: [{ selector, description }]`, without touching them.                                                                                   |

Every result carries the `url` the page is on afterwards.

### A screenshot the agent can see

A `screenshot` answers with two things. The **file** is the deliverable: the bytes go to storage and
the result's `output` carries `{ url, file_id, sha256, size_bytes }`, so the PNG survives the session
and is there to publish, attach or download. The **picture** is what the model is shown, and it
travels as a `ToolImage` — `{ file_id, url, media_type }`, where `url` is a short-lived signed read
of that same file.

The image is a link and never the bytes. A tool result lands in the session's history and is replayed
on every turn that follows, so an agent that browses for an hour would otherwise re-send every PNG it
ever took, over and over, at full size. A link costs a couple of hundred bytes and does not grow, and only the most recent tool results
send their pictures to the model — an older screenshot stays in the transcript as a link a person
can click, not as an image paid for on every later turn. A link is good for fifteen minutes, so an
agent that wants to look at something again takes a fresh screenshot rather than reaching back;
that is also why a picture does not survive a session going to sleep and waking up.

Without a file catalogue configured the action refuses rather than hand out an id that resolves to
nothing. If the file was written but the link could not be minted, the screenshot still answers with
its `file_id` — the deliverable is on disk either way, and only the looking is lost.

<Note>
  Plain-language actions need a model behind the browser. Where none is configured they fail with `feature_not_configured`; the deterministic actions keep working.
</Note>

### Write guard

Unless `allow_writes` is set, an `act`, `click` or `type` whose instruction, target or page looks like a write — submit, purchase, delete, send, pay, confirm, or a form submission on a non-search form — is refused with `forbidden` (`param: allow_writes`). Reading is always allowed; changing something is opt-in.

### Domain re-validation on every action

Every action **re-validates the current page URL** against `allowed_domains` before it runs and again after — not just at `goto`, and including after `act`. A redirect, a link or an instruction that lands off the allow-list is blocked and the page is parked on a blank tab. Regardless of the allow-list, the browser **always blocks private, loopback, and metadata hosts** (link-local, internal ranges, and cloud metadata endpoints), including hostnames that *resolve* into them.

<Warning>
  The allow-list is checked on the **page URL** — the document the browser navigates to. Subresources a page loads on its own (`fetch`/XHR, images, iframes) are **not** individually checked against `allowed_domains` today. Treat it as control over where the agent goes, not as an exfiltration boundary for a page that is already open.
</Warning>

<Warning>
  `"*"` disables the allow-list check but does **not** disable the private/loopback/metadata block, and every action under `"*"` is logged. Prefer a scoped list.
</Warning>

## Credentials & saved logins

Credentials are **never entered through `type` or `act`**. A guard refuses credential-shaped input — password, token and key patterns; vault values stay sealed and are never compared — with `validation_failed`, so the agent cannot type a password into a field via a normal action.

Instead, a login runs through the **dedicated `login` flow**: it reads the identity's [vault](/docs/identity/vault) credential `login:<service>` and fills the form through `%username%` and `%password%` placeholders. The agent drives the flow; the plaintext never enters the model or a result. Save the credential first with `POST /v1/browser/credentials` (`vetta browser credential save`), or let `signup` create the account — it generates a password, seals it into the vault, then registers.

### Saved logins are default-deny

A saved login ("context") is **identity-scoped** and **default-deny**. It is **bound when a browser opens**: the engine attaches the context at session start and writes cookies and storage into it when the session closes. So a login is filled in two steps — `save_context` with no browser open creates the empty saved login under a name owned by the session's identity; a browser opened with that `context_name` and `persist_context` (or `human_login`) fills it at close. `save_context` from a browser open on another or no context is refused with `validation_failed` — nothing that browser holds can reach the saved login.

An agent cannot reopen a saved login just by naming it in `context_name` — it needs an **explicit grant**, by **agent id** or by **role**, made from the dashboard. Without a grant, `context_name` is refused with `forbidden`. On a session that reopened a saved login, `extract`, `observe`, `links` and `screenshot` also need `allow_extract`, and what they return is redacted of email, phone and card patterns.

### Human-first login

Set `human_login` to run a **human-first** flow: `save_context` the name first, open with that `context_name` and `human_login`, a person completes the login once in the live view, then `save_context` again marks it saved and the close persists it for later reuse. During the flow a **15-minute lock** is held on the context until the second `save_context` completes; a second open of the same context while locked is `409 context_locked`.

## Session lifetime & what's withheld

* Each session runs under `timeout_minutes` — default **15 minutes**, maximum **360** (6 hours). See [Limits & billing](/docs/computer/limits-and-billing#the-browser).
* The agent's own browser opens on its first `browser` action and **closes when the agent session ends or pauses for input** — it never idles on the clock while a question waits for you. The next turn opens a fresh one on demand; a saved login (`context_name`) carries the signed-in state across.
* Minutes are billed **open to close** on the `browser` line item, settled when the session closes; `act`, `extract` and `observe` add their model spend on the same line.
* **Withheld from the agent and from API keys:** the raw connect URL and the live-view URL. The live view answers a signed-in dashboard session only; an API key gets `forbidden`.

## Configuration reference

<ParamField path="allowed_domains" type="string[]" default={'["*"]'}>
  The domains the browser may reach. Omit it for the open web — unrestricted browsing **is logged**. Private, loopback and metadata hosts stay blocked either way (see above), so this list narrows public reach rather than granting it. An explicit `[]` is `validation_failed`. Using a [proxy](#configuration-reference) requires a scoped list — `"*"`, including the default, is not allowed with a proxy.
</ParamField>

<ParamField path="timeout_minutes" type="integer" default="15">
  Per-session timeout. Range **1–360** minutes (up to **6 hours**).
</ParamField>

<ParamField path="region" type="string" default="us-west">
  Geographic region the browser runs in: `us-west`, `us-east`, `eu-central`, or `ap-southeast`. Pick the one closest to the target site.
</ParamField>

<ParamField path="keep_alive" type="boolean" default="false">
  Keep the session alive across brief disconnects instead of closing it. Useful for long, resumable flows.
</ParamField>

<ParamField path="persist_context" type="boolean" default="false">
  Write cookies and local storage back to the named `context_name` when the session closes, so a later session can resume the same logged-in state. Implied by `human_login`.
</ParamField>

<ParamField path="viewport" type="object">
  `{ width, height }` in pixels for the browser viewport.
</ParamField>

<ParamField path="os" type="string">
  Emulated platform fingerprint: `windows`, `mac`, `linux`, `mobile`, or `tablet`.
</ParamField>

<ParamField path="block_ads" type="boolean" default="false">
  Block ad and tracker requests.
</ParamField>

<ParamField path="solve_captchas" type="boolean" default="true">
  Attempt automated CAPTCHA solving on supported challenges.
</ParamField>

<ParamField path="stealth" type="boolean" default="false">
  Enable advanced anti-bot evasion (stealth / verified fingerprint).
</ParamField>

<ParamField path="allow_writes" type="boolean" default="false">
  When `false`, the [write guard](#write-guard) refuses write-looking `act`, `click` and `type`. Set `true` to permit them.
</ParamField>

<ParamField path="allow_extract" type="boolean" default="false">
  Gates `extract`, `observe`, `links` and `screenshot` **on saved-login sessions**. Off by default so a reused login can't silently exfiltrate page content. When on, credential-shaped values, e-mail addresses and phone numbers are redacted from what those actions return.
</ParamField>

<ParamField path="context_name" type="string">
  Reopen a **saved login** ("context") by name, 1–64 characters. Subject to the default-deny grant model above.
</ParamField>

<ParamField path="human_login" type="boolean" default="false">
  Start a **human-first login flow** on an existing `context_name`. Takes a 15-minute lock on the context until `save_context` completes, and persists the login at close.
</ParamField>

<ParamField path="proxy" type="boolean" default="false">
  Route egress through a **residential** path. Requires a scoped `allowed_domains` list — `true` with `"*"` is `validation_failed`.
</ParamField>

<ParamField path="ask_actions" type="string[]" default="[&#x22;signup&#x22;]">
  Read by the agent [`browser` tool](/docs/capabilities/tools#the-browser-tool) only: the actions that pause for the operator's approval while the tool is `allow`. Any of `goto`, `click`, `type`, `act`, `extract`, `observe`, `links`, `screenshot`, `login`, `signup`, `save_context`; an unknown name is `validation_failed`. Has no effect on a browser opened over REST.
</ParamField>

<Card title="Next: snapshots & volumes" icon="camera" href="/docs/computer/snapshots-and-volumes">
  Checkpoint a running VM, fork from it, and keep state on a persistent volume.
</Card>
