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

# Web search, reading, and browsing

> Configure web providers and understand what each web capability can do.

Formation exposes three independent capabilities: search, page reading, and browser
interaction. Providers implement vendor-neutral contracts; the agent sees stable
tool names. Search is implemented with Cloudflare Web Search, Brave and
Perplexity Search, page reading with Cloudflare and Firecrawl, and interaction
with Cloudflare and Browserbase. The integration catalog preserves provider
choice, instance defaults with agent overrides, agent-owned browser
authentication, and per-operation effect semantics.

## Configuration and identity

Credentials are held by **vendor accounts**: one credential per vendor per
owner (the install or an agent), shared by every capability that vendor
implements. Vendor-level settings such as a Cloudflare account ID or a
Browserbase project ID live on the account. A capability **connection** then
chooses which vendor's implementation it uses and carries only capability-level
settings (browser hostname restrictions, a search gateway or engine) and
limits. Connecting Cloudflare once therefore covers search, reading and the
browser; switching a capability between vendors never moves or erases a
credential, and removing an account leaves the connections that pointed at it
unavailable rather than borrowing another vendor's key.

An install connection with no stored row is **automatic**: the capability uses
the first vendor in catalog order (Cloudflare first) that the install has an
account for. Storing `mode: "disabled"` is the opt-out; storing `mode: "own"`
pins a provider. Agents automatically inherit the install's effective
connection unless explicitly turned off or customized with their own provider
and their own vendor account. A missing agent row means inheritance, including
the instance request limit; a stored disabled row remains an opt-out. Disabled
capabilities receive no tool. An explicit agent override never falls back to an
install account when its own account is missing or invalid. Configuration is
read again before execution; rotation, removal and disabling invalidate
previously offered tools, and browser sessions are swept when the account they
were opened under changes.

A hosting operator can supply a **managed vendor account** to the process with
`MESH_CLOUDFLARE_API_TOKEN` and `MESH_CLOUDFLARE_ACCOUNT_ID`. It behaves as the
install's Cloudflare account whenever no stored one exists, so a freshly booted
install has every web capability available to every agent with no dashboard
setup. An install owner can connect their own Cloudflare account, which takes
precedence while stored, and remove it to return to the managed one; the
dashboard shows which is in use. Agents never use a managed account for a
provider of their own. The managed token is read from the environment on each
use and is never written to the database or returned by any route.

Web tools use dynamic `tool.Provider` grants rather than static secret-kind
`Needs`: an offer captures the agent and effective connection versions, and
execution rechecks both under the same locks used by configuration writes.
Keys are passed only to the selected adapter for that request, never injected
into a workspace, prompt, or browser page. Queued stale offers are refused.
In-flight HTTP calls poll the binding every second and cancel on change; a
second check withholds results that race revocation. Disabling returns after
the durable gate changes, not after a remote service confirms cancellation.
Already-transmitted requests may still consume provider quota. This is an
explicit query-provider exception to the generic drain-before-return lifecycle;
it does not claim a remote action was undone or a token revoked at its issuer.

Infrastructure credentials pay for search or browser execution. Website accounts,
cookies, and authenticated profiles belong to an agent. Sharing a provider key
does not share a website identity. Browser sessions are run resources; persistent
profiles are separate, agent-owned resources with serialized writers.

## Runtime contracts

`web_search` returns ranked source URLs, titles, excerpts, and available dates.
`web_fetch` returns extracted page content and the requested source URL. Both call
declared hosted APIs; the control plane never fetches arbitrary website URLs.
`browser_navigate`, `browser_read`, `browser_act`, and `browser_screenshot` operate
on an isolated remote session. Browser actions cross the durable effect boundary
and uncertain effects are never automatically replayed.

The generic web capability is a Formation-owned tool surface. Vendor integrations are
replaceable adapters with declarative configuration and a single catalog entry,
following `layers.md`. This refines the older prohibition on first-party vendor
wrappers: providers do not introduce vendor-specific tools or harness branches.
Remote MCP remains the extension surface for additional third-party tools; web
connections do not reuse MCP credentials or its server identity table.

| Tool | Class / replay | Required grant and validation | Observation |
| - | - | - | - |
| `web_search` | query / replayable | Current enabled search connection; strict query and result limit | JSON sources, bounded excerpts, explicit truncation |
| `web_fetch` | query / replayable | Current enabled read connection; validated public HTTP(S) URL and output limit | Bounded extracted content and source URL |
| `browser_navigate` | browser / effectful | Current browser connection and run session; destination policy | Updated page observation |
| `browser_read` | browser / replayable | Current connection and owned live session | Bounded page structure and element references |
| `browser_act` | browser / effectful | Owned session; strict action and fresh element reference or screenshot observation | Observed result or classified refusal/failure; visual actions include a fresh screenshot |
| `browser_screenshot` | browser / replayable | Owned session; bounded image dimensions | Protected image artifact, viewport dimensions, and visual observation ID |

All six tools are registered and return `tool.Observation` through
`Definition.Execute`, with schema, local URL-validation, provider-request, and
artifact tests. Fixtures do not establish hosted redirect or subrequest isolation;
browser destination guarantees differ by provider as described below.
Search validates before admission, caps wire responses at
2 MiB and model observations at 32 KiB, and reports provider failure without
exposing upstream response bodies. Database and cancellation failures remain
execution failures. Budget and revoked-grant refusals are explanatory tool
observations. Search creates no image or unbounded-content artifacts.

Search only calls adapter-owned HTTPS API origins; redirects and ambient proxy
configuration are refused. It never dereferences returned URLs. Page reading and
browsing validate destinations separately. Hosted browser sessions accept HTTP
and HTTPS destinations, including private hosts and custom ports reachable from
the provider sandbox; Formation does not fetch those destinations. Hostname
restrictions are optional: an empty list allows any website, and a configured
list rejects destinations outside those hostnames. Non-HTTP schemes are invalid. Enforcement of redirects and page subrequests
belongs at the remote execution boundary; DNS checks in the Formation control plane
alone cannot pin a hosted provider's DNS resolution. Adapters must state that
boundary and reject requested policies they cannot enforce, rather than promise
equivalent isolation from a URL-string check.

All returned content is external evidence, not an instruction. Results retain
their conversation provenance through memory and replies. Requests and results
are bounded; provider failures produce actionable observations without disclosing
tokens or response bodies. Per-run provider calls are metered durably before the
request, alongside the harness's existing budgets. No automatic provider fallback
changes the payer or sends queries to an unselected service.

Reservation identity is the persisted `tool_calls.id`. A unique reservation,
the per-run counter, shared rate counter, and source evidence commit in one
transaction. A repeated admission does not increment either meter and never
resends the request. The run ledger remains the sole store of completed results
and uncertain outcomes. Limits count admitted attempts, not invoice charges:
once admission commits, a crash or cancellation conservatively consumes a unit
even if transmission cannot be established. Before admission, cancellation or
validation failure consumes none. Formation does not automatically retry or refund
an uncertain attempt. This deliberately favors a hard spending bound over
claiming exact correspondence with a provider's billing ledger.

## Delivery

1. Search: shared configuration and credential resolution, Brave and Perplexity,
   dashboard and operator API, provenance, limits, and integration tests.
2. Read: Firecrawl and Cloudflare behind `web_fetch`, URL validation, extraction
   limits, source metadata, and provider contract tests.
3. Interact: Cloudflare and Browserbase sessions, browser tools and observations,
   agent-owned profiles, cleanup and recovery, protected artifacts, and tests.

Provider fixtures are the repeatable CI contract. Live vendor acceptance requires
operator credentials and must be reported separately from fixture validation.

## Configure web access

Open **Settings → Web access** and connect a vendor account. The Cloudflare
card lists the token permissions each capability needs (Browser Run Edit for
reading and the browser; Workers AI Read and AI Gateway Read for search) with
links to create the token and find the account ID. Saving the account runs a
verification of every capability the vendor implements and reports, per
capability, whether it worked and, when it did not, the HTTP status, the
vendor's numeric error codes and request ID (never its message text), and the
permissions that capability needs. A Cloudflare 403 is not always a missing
grant: an account the feature is not enabled for answers the same way, which
the error codes distinguish. Every capability left on automatic selection is then available to all
agents. Open an agent's **Tools → Web access** and choose **Customize** to
configure that agent's own provider and vendor account, **Turn off** to opt
out, or **Use instance settings** to restore inheritance. New and existing
untouched agents inherit live instance settings. No restart or environment
variable is required.

Cloudflare Web Search is served through AI Gateway. The connection can name a
gateway (default `default`) and a search engine (`ceramic`, the default, `exa`
or `linkup`); engine pricing and the AI Gateway credit requirement are
Cloudflare's. Brave and Perplexity remain available as search-only vendors.

The connection meters requests per minute across its agents and requests per
run. An inheriting agent can lower the instance's per-run allowance. A rate or
run limit produces an observation explaining exhaustion; requests are never
retried automatically. Provider changes and key rotation invalidate old offers.
Selecting a different provider does not send it the previous provider's key:
keys are stored separately by vendor and owner. Removing a vendor account
deletes its key and settings atomically; capabilities that pointed at it stop
being offered until another account is connected or the capability is moved.

Search currently accepts `query` (up to 2,000 bytes) and `limit` (1–10, default
5\). Provider-specific filters and generated research answers are outside this
initial common contract. Excerpts are bounded and carry a `truncated` indicator.

## Operator API

| Method | Route | Authority |
| - | - | - |
| GET | `/api/settings/web` | Instance owner |
| PUT | `/api/settings/web/{capability}` | Instance owner |
| POST | `/api/settings/web/{capability}/test` | Instance owner |
| PUT | `/api/settings/web/vendors/{vendor}` | Instance owner |
| DELETE | `/api/settings/web/vendors/{vendor}` | Instance owner |
| POST | `/api/settings/web/vendors/{vendor}/test` | Instance owner |
| GET | `/api/agents/{slug}/web` | Operator with access to the agent |
| PUT | `/api/agents/{slug}/web/{capability}` | Operator with access to the agent |
| POST | `/api/agents/{slug}/web/{capability}/test` | Operator with access to the agent |
| PUT | `/api/agents/{slug}/web/vendors/{vendor}` | Operator with access to the agent |
| DELETE | `/api/agents/{slug}/web/vendors/{vendor}` | Operator with access to the agent |
| POST | `/api/agents/{slug}/web/vendors/{vendor}/test` | Operator with access to the agent |

GET returns `capabilities` and `vendors`. Each capability carries its provider
descriptors (`name`, `label`, `vendor`, `permissions`, capability-level
`fields`), the stored `settings`, `effective_provider`, `credential_source`
(`install` or `agent`), `account_source` (`owner`, `environment` or empty),
`account_version`, `credential_present` and `available`. Each vendor carries
its descriptor (`label`, `credential_label`, `setup_instructions`,
`setup_links`, vendor-level `fields`), the owner's stored `config` and
`version`, `credential_source`, `configured`, `environment_available` (a
managed account exists for the install whether or not it is in use) and the
`capabilities` the vendor implements with the permissions each needs. Secrets
and secret fragments are never returned. Every response is
`Cache-Control: no-store`. Every write answers with the same listing.

`PUT …/vendors/{vendor}` stores or rotates an account:

```json theme={null}
{
  "config": {"account_id": "<32-character Cloudflare account ID>"},
  "version": 0,
  "api_key": "<vendor credential>"
}
```

`version` is the account's version from GET (zero for a new account); a stale
version returns 409. Omit `api_key` to keep the stored credential; creating an
account without one returns 412. Blank keys are rejected. `DELETE` takes
`{"version": n}` and removes the account and its credential; for the install
this is also how an operator returns to a managed account.

`PUT …/{capability}` takes a full settings replacement and the connection
version from GET (zero for a new configuration):

```json theme={null}
{
  "mode": "own",
  "provider": "cloudflare",
  "config": {"search_provider": "ceramic"},
  "max_calls": 10,
  "requests_per_minute": 60,
  "version": 0
}
```

An install uses `mode: "auto"` (an empty provider and config) to return to
automatic selection; an agent uses `mode: "inherit"` the same way. `mode:
"disabled"` removes the capability from future offers. `mode: "own"` requires
that the owner has an account with the provider's vendor and otherwise returns
412; it never stores a half-configured connection that would silently fall
back.

`POST …/vendors/{vendor}/test` verifies the owner's saved account against every
capability the vendor implements and returns, per capability, the provider,
the permissions it needs and a probe result. `POST …/{capability}/test`
verifies the saved effective connection of one capability. Both accept only an
empty body or `{}`; the credential, configuration and target are always the
saved ones.

The API settings write is audited by the existing operator middleware. Runtime
observations identify the capability, provider, and credential owner type;
queries and result excerpts inherit the originating conversation's evidence
scope. Encrypted credentials continue to use Formation's existing master-key rotation
and backup procedures. Migration `0066` adds the connections and meters and
extends external evidence without changing MCP connection ownership.

## Public page reading

`web_fetch` is registered by the `read` capability with `firecrawl` and
`cloudflare` providers. Configure it in **Settings → Web access** and agents inherit it automatically. Agent overrides are available in Tools settings. Cloudflare requires
an account ID and a token with Browser Rendering Edit permission; Firecrawl
requires a Firecrawl API key. The same operator API, version checks, durable
attempt limits, and conversation evidence used by search apply independently
to read connections.

Arguments are a public `url` and optional `max_bytes` (1–20,000; default 12,000).
The tool returns `requested_url`, optional `title`, Markdown `content`,
`retrieved_at`, and an explicit `truncated` flag. Neither adapter claims a
verified final redirect URL. Provider responses are limited to 2 MiB; encoded
observations to 30 KiB, including JSON escaping. Empty results, unsuccessful
responses, and reported HTTP error pages fail without exposing vendor bodies.
Cloudflare can still return a site's block page as ordinary extracted content;
callers must assess the content, not infer success from a nonempty result alone.

The only network connections made by Formation are to the two declared HTTPS API
origins. Model arguments cannot supply headers, cookies, HTML, actions, custom
API endpoints, or proxy settings. Firecrawl cache reuse and storage are disabled
for these requests; the provider's separate retention policy still applies.
TLS verification is explicitly enabled.

Input validation rejects credentials, custom ports, non-HTTP schemes, local
hostnames, ambiguous numeric hosts, and private/reserved IP literals. These
checks do **not** pin the hosted browser's DNS resolution or constrain its
redirects/subrequests. The hosted provider is the network isolation boundary;
this capability is for public content and supplies no Formation network access or
website credentials. It does not offer an operator domain-egress restriction
because these quick-action APIs cannot provide equivalent enforcement across
both adapters. Deployments requiring such a restriction must leave this
capability disabled until a provider with that enforceable contract is added.

Provider contracts:
[Firecrawl scrape](https://docs.firecrawl.dev/api-reference/endpoint/scrape) and
[Cloudflare Markdown](https://developers.cloudflare.com/browser-run/quick-actions/markdown-endpoint/).

## Browser interaction

Browser interaction combines element references with native visual and keyboard
controls. Automated tests cover provider protocols, input handling, and
database-backed ownership/lifecycle; live website validation is separate.
Application compatibility, including Google Docs editing, must be established
with the intended provider and workflow. Operator login/MFA handoff is not part
of these controls.

The `browser` connection offers `browser_navigate`, `browser_read`,
`browser_act`, and `browser_screenshot` through Cloudflare Browser Run and
Browserbase. Both adapters use hosted browsers controlled through CDP. Configure
an account ID (Cloudflare) or project ID (Browserbase) and the provider credential.
For Cloudflare, [create a custom API token](https://dash.cloudflare.com/profile/api-tokens)
with **Account → Browser Run → Edit** (called Browser Rendering in some Cloudflare
documentation), including the configured account. **Specific account** and
**All accounts** both work when they include that account. In the Cloudflare dashboard,
select that account, open Search (Cmd/Ctrl + K), and choose **Copy account ID**.
See the [Cloudflare setup guide](https://developers.cloudflare.com/browser-run/get-started/)
and [account ID instructions](https://developers.cloudflare.com/fundamentals/account/find-account-and-zone-ids/).
Browserbase credentials and the project ID come from its project settings.

Cloudflare acquires a browser directly over the documented
[`/browser-run/devtools/browser` WebSocket endpoint](https://developers.cloudflare.com/browser-run/cdp/#acquire-and-connect-to-a-browser),
with Bearer authentication and a five-minute idle allowance. Optional domain
guardrails use the documented base64url `cf-brapi-guardrails` acquisition header.
Formation validates the returned session identity and constructs its fixed Cloudflare
reconnect URL, rather than following an arbitrary provider-supplied URL. It reads
the session ID from `cf-browser-session-id`, or `Cloudflare.getSessionId` when
that header is absent, following Cloudflare's
[Agents SDK](https://github.com/cloudflare/agents/blob/7dd51c518c517b0f9f92ec7e2c3ad1004222955e/packages/agents/src/browser/browser-run.ts#L384)
and [CDP implementation](https://github.com/cloudflare/puppeteer/blob/5f0ffea563f7cf8659772462f1a404769a67bee1/packages/puppeteer-cloudflare/src/cloudflare/protocol.ts#L159-L170).
Subsequent commands reconnect to the same retained session. Saved
`/browser-rendering/devtools/browser` WebSocket endpoints remain supported.
Cleanup uses the documented HTTP DELETE endpoint without a CDP reconnect that
would extend idle lifetime; a closing response does not yet prove termination.
If a browser call fails, its error identifies the stage (for example, session
creation, browser connection, or navigation). HTTP 401 and 403 remain distinct:
401 means browser access was rejected; it does not establish that the API token
is invalid or expired. HTTP 403 suggests checking token permissions and account
access. Neither status alone proves the credential's exact defect.
HTTP 429 indicates provider rate or capacity limits. Failed HTTP and WebSocket handshakes retain up
to eight numeric Cloudflare error codes (1–999999) from a bounded 16 KiB JSON
envelope and a format-validated CF-Ray request ID. Free-form provider messages,
response bodies, credentials, and raw connection errors are never exposed.
Failure observations carry `is_error: true` and structured `browser_failure`
(stage, phase, reason, outcome, recovery, retry guidance, and HTTP status or
provider codes/request ID when known). Web capability provenance preserves
this metadata; commitment evidence rejects error observations. A completed tool
handler still has execution status `succeeded`, but the message trace labels the
observation **operation failed**. This separates execution from operation outcome
without changing replay or mutation-completion semantics.
The classifications distinguish three cases:

* A known pre-dispatch refusal, such as a stale reference, changed viewport,
  obscured target, or read-only field, reports `not_dispatched`. Observe again
  before choosing a fresh action; the refused input was not sent.
* When every input command was acknowledged but reading or saving its resulting
  observation fails, the outcome is `input_dispatched`. This proves browser
  input delivery, not that the website saved an edit. Inspect with `browser_read`
  or `browser_screenshot` before deliberately continuing. The session remains
  available for that inspection and subsequent actions.
* A failure during input delivery remains `uncertain`. Read-only inspection
  remains available, but cannot itself prove whether the original effect
  happened. The original operation must be reconciled before further mutations;
  Formation does not silently release that reservation or replay the input.

No classification automatically retries an edit or treats stable page content
as proof that a website committed it.
Read-only observation failures dispatch no input and may be retried. They do not
turn an active session uncertain or clear uncertainty from an earlier edit.
Editors that keep changing after the bounded wait return the latest complete
observation with `page_changing: true`; controls are revalidated before input.

Browsing is open by default: a blank hostname list omits provider domain controls.
An operator may optionally supply comma-separated hostnames, including the login,
redirect, API, and asset hosts a workflow needs. Saved lists remain in effect
until explicitly cleared. This is not the control-plane URL reader: destinations
are opened by the remote cloud browser, so Formation does not impose the reader's
public-network or default-port policy on them. Provider reachability still applies.
When supplied, the list is sent to the provider's domain controls; arbitrary provider endpoints, JavaScript, HTTP headers,
filesystem uploads, and downloads are not exposed as agent arguments.

These controls have different scopes. [Cloudflare guardrails](https://developers.cloudflare.com/browser-run/features/guardrails/)
restrict HTTP(S) requests, including redirects and page dependencies, to the
configured hostnames. [Browserbase allowed domains](https://docs.browserbase.com/platform/browser/security/allowed-domains)
restrict top-frame HTTP(S) navigation and also permit subdomains; they do not
restrict iframe loads, scripts, images, or XHR. Formation separately validates HTTP(S) addresses and, when a list is configured,
refuses observations outside its exact hostname list, but those checks
cannot prevent requests already made by the hosted page. Browserbase therefore
does not provide request-wide domain isolation. Deployments requiring that policy
must use an appropriate provider rather than treating these adapters as equivalent
egress boundaries. Neither local fixtures nor URL validation prove remote DNS
pinning or private-address filtering.

Instance setup offers **Save and enable**, **Disable connection** (retains the
credential), and a confirmed **Disconnect** action (deletes the saved credential
and disables the connection). Agents must still choose to use the connection.

Browser connections initially show **Configured**. **Test saved browser connection**
acquires a fresh blank browser, reconnects, verifies `Browser.getVersion`, and
closes the session. It uses the saved effective provider and credential, including
instance infrastructure inheritance; unsaved form values are never sent. It does
not load website profiles, create a run, or navigate to a website. Success shows
**Verified** for the current view; it is not proof of a website login or completed
Google Docs edit. Saving or changing the effective connection clears that result.
On failure, the result includes only a fixed stage, HTTP status, numeric provider
codes and validated request ID. For Cloudflare HTTP 401, Formation also checks both
user-token and account-token verification APIs; it reports an active token only
after a successful verification and never infers an expired token from a failed
verification request.

`POST /api/settings/web/browser/test` (instance owner) and
`POST /api/agents/{slug}/web/browser/test` (authorized agent access) accept an empty
body or `{}`. Other input is rejected. Search and reading tests send one small
fixed request the same way. The provider call has a 45-second timeout and
independent bounded cleanup, and configuration and account versions are checked
again before a result is returned. Website identities remain agent-owned
throughout.

A browser session belongs to one run and uses that run's deadline, not a separate
five-minute lifetime. Browser operations use Formation's normal run allowance; the
legacy web-connection request/rate fields do not cap browser work and are not
shown in browser setup. Search and reader limits are unchanged. Durable browser
usage and source evidence are still recorded.
Cloudflare's five-minute `keep_alive` setting is an **idle** timeout between CDP
connections, not a total-session limit; Cloudflare also imposes its own quotas.
Browserbase's session timeout follows the remaining run duration, within its
provider-supported range of 60 seconds to six hours. Formation cleans up at run end.
One live session may write an agent's profile at a time. Durable operation leases
serialize access across replicas; a crashed operation becomes uncertain. Read
and screenshot calls are replayable observations, while navigation and actions
cross the run ledger's existing external-effect boundary before execution.
The runtime never repeats a dispatched action automatically. After an uncertain
action, inspect the page with `browser_read` or `browser_screenshot`; the recovery
classification above determines whether further actions are available.
A lost session requires a new run, rather than an invisible session replacement.

### Element and visual actions

`browser_read` returns bounded text, visible controls, and fresh `ref` values.
References live in isolated JavaScript worlds and reject changed, detached,
or obscured targets; typing also refuses read-only regions. The reader traverses open shadow roots and
available child-frame contexts. Frame observations identify unavailable or
restricted contexts instead of inventing controls. Frame and element counts are
bounded, and truncation is explicit. Closed shadow-root internals are not exposed
as DOM references.

Use a `ref` with `click`, `type`, `press`, or `fill_secret`. `type` selects and
replaces the entire value of an input, textarea, or editable text region; it is
not append-only typing. `press` focuses the referenced control and sends a native
key or chord. Password fields support ordinary clicking and typing. References
are replaced by a new read and expire after an action or navigation.

For a canvas editor or another surface without a useful DOM reference, call
`browser_screenshot`. Its protected image accompanies `observation_id`,
`viewport_width`, `viewport_height`, and `expires_in_seconds`. Supply that ID
instead of `ref` to use native input:

| Action | Additional arguments | Behavior |
| - | - | - |
| `click`, `double_click` | `x`, `y`; optional `button` (`left` or `right`) | Click the observed viewport position |
| `drag` | `x`, `y`, `to_x`, `to_y`; optional `button` | Move the pointer while holding the button |
| `scroll` | `x`, `y`, `delta_x` and/or `delta_y` | Send a wheel event over that position |
| `insert_text` | `text` | Insert at the current focus/selection without clearing or moving focus |
| `press` | `key` | Send a native key or modifier chord to the current focus |

Coordinates and wheel distances use CSS pixels in the returned viewport, with
coordinates measured from its top-left corner. Pointer origins and drag
destinations must be inside that viewport. Visual `insert_text` is limited to 32
characters per action; DOM `type` retains its 2,000-byte limit. Key
chords support `Control`/`Ctrl`, `Alt`/`Option`, `Shift`, and `Meta`/`Command`/`Cmd`,
plus letters, digits, function keys F1–F24, and editing/navigation keys such as
`Backspace`, `Delete`, `Home`, `End`, `PageUp`, `PageDown`, `Escape`, `Tab`, `Enter`,
and the arrow keys. Modifiers are released within the action. No agent-provided
JavaScript is evaluated.

For example, after observing an editor, click its visible editing surface:

```json theme={null}
{"action":"click","observation_id":"<screenshot observation ID>","x":320,"y":280}
```

Each successful visual action returns the resulting page, a fresh screenshot, and a new
`observation_id`. Use the new ID for the next step:

```json theme={null}
{"action":"press","observation_id":"<new observation ID>","key":"Control+End"}
```

Then use that action's new ID with
`{"action":"insert_text","observation_id":"<new observation ID>","text":"banana"}`.
`insert_text` uses the selection/caret already present; use a selection chord
such as `Control+a` deliberately when replacing content is intended.
It sends native key events for supported characters, with Chromium text insertion
for other Unicode characters. Send chunks of at most 32 characters and inspect
the returned screenshot before continuing. Newlines and tabs act as their keys.

A visual observation expires after 60 seconds, an action, navigation, or a
viewport/document/frame change; taking another screenshot replaces its ID.
Visual input is fenced to the owning agent, run, target, document, and observed
viewport. When hostname restrictions are configured, they also apply to the
receiving frame. Native pointer and focused-keyboard input can reach rendered
frame or shadow content without exposing it as a DOM reference; unavailable
frame validation is refused rather than bypassing the restriction.

`fill_secret` requires a fresh DOM reference to an input or textarea and an
agent-vault entry name ([Agent vault](/runtime/agent-vault.md)). Formation opens the value
for the session's agent, inserts it over CDP, and discards it, so the model names
the secret and never sees it. The action refuses `text`, `key`, and visual
coordinates/observation IDs. Contenteditable regions and canvas surfaces do not
accept secret-reference filling.

Post-action observations wait at least 750 ms and require a complete document
with 500 ms of stable visible content, within an eight-second bound. Stability
is not confirmation that a submission or document edit was saved; verify the
website result and, when relevant, persistence after a reload.

### Website identities and screenshots

Website profiles contain cookies and local storage in Playwright storage-state
shape. They are encrypted in Formation's existing secret store, so the existing
master-key rotation process covers them. Profiles are agent-owned and remain
separate from provider credentials. IndexedDB, session storage, browser
extensions, downloads, and complete browser-user-data directories are not
persisted. Cookies can expire or be invalidated by the website. Credentials typed through
`browser_act` are ordinary tool arguments recorded in the run ledger; use
`fill_secret` or authenticated profile import when avoiding password text in tool
history is required. Visual controls do not add operator MFA handoff.

In an agent's Tools page, **Website logins** imports authenticated storage state,
clears saved login state, and stops an active browser. Profiles cannot be
replaced while a browser is using them. Profile APIs never return stored cookie
values. The endpoints are `GET/PUT /api/agents/{slug}/browser-profile` and
`POST /api/agents/{slug}/browser-profile/stop`, behind agent-access checks and the
existing mutation authentication/audit middleware. PUT accepts `version` and
`state`; changing stale state fails rather than overwriting it.

Screenshots are JPEGs bounded to 1280×720 and 2 MiB, encrypted as agent-owned
artifacts, with a 24-hour read lifetime. The tool observation retains an artifact
reference. The background sweep deletes expired encrypted image bytes while
retaining artifact metadata for audit. Vision-capable models receive the newest screenshot as image bytes,
never a signed remote browser URL. Operators can retrieve an artifact through
`GET /api/agents/{slug}/browser-artifacts/{artifact}` with agent authorization.

A background sweep closes sessions after a run ends, credentials are disabled
or changed, or the session expires. A busy operation retains its lease until it
finishes or the lease expires. A remote close failure retains the profile writer
and retries cleanup, preventing another run from concurrently using its login.
Provider timeouts bound orphaned sessions after a process crash. No API token,
CDP connection URL, or cookie value is returned in browser observations.

### Opt-in Google Docs acceptance

`TestLiveGoogleDocVisualEditing` exercises the real Formation tool pipeline against
Cloudflare, with an isolated local scratch database and a disposable Google Doc
that permits anonymous editing. It appends a unique `banana mesh-live-<UUID>`
marker, verifies rendered document content, reloads through `browser_navigate`,
and requires that marker to persist. It closes the remote browser and drops the
scratch database afterwards. This tests tool execution, not a Daedalus model
conversation or every Google Docs workflow.
Canvas-only builds are verified from the actual screenshot using local
`tesseract` OCR; install it on the test machine. No document API, export endpoint,
pending input value, or remote OCR service is used for persistence verification.

Compile the test binary from the repository root:

```sh theme={null}
go test -c -o /tmp/mesh-browser-visual.test ./internal/webbrowser
```

Run only this test, supplying provider credentials directly on standard input
from your secret manager. Its stdin JSON shape is
`{"provider":"cloudflare","account_id":"<account ID>","token":"<token>"}`;
credentials do not belong in shell arguments, environment variables, or a checked-in
fixture. Configure the database URL for a local test role allowed to create
databases:

```sh theme={null}
MESH_LIVE_BROWSER_TEST=1 \
MESH_LIVE_GOOGLE_DOC_URL='https://docs.google.com/document/d/<disposable-id>/edit' \
MESH_TEST_DATABASE_URL='postgres:///postgres?host=/tmp&sslmode=disable' \
MESH_LIVE_ARTIFACT_DIR='/tmp/mesh-browser-visual-artifacts' \
/tmp/mesh-browser-visual.test -test.run '^TestLiveGoogleDocVisualEditing$' -test.timeout 10m
```

`MESH_LIVE_ARTIFACT_DIR` is optional. When set, the test exports restricted-permission
JPEG evidence before editing, after editing, and after reload; failures can export
a verification screenshot. The credential is encrypted only in the scratch
database, and the test does not update a production connection or import a website
identity. Report live results separately from ordinary CI, where the test skips.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.