v0.1.3 to v0.2.0

276 added, 364 removed. Audit A to A.

---
name: chrome_browser
- description: Act in your own Chrome through a paired extension - one explicitly shared tab or the agent's own background tab, trusted input without taking focus, and a clean handoff of the live page to you and back.
- version: 0.1.3
+ title: Chrome (shared tab)
+ description: Work in the owner's own Chrome through Microsoft's official Playwright Extension - the tab they share plus background tabs for the same work, trusted input in background tabs, and handback by plain conversation.
+ version: 0.2.0
type: script
runtime: node
- license: MIT
+ os: [darwin, linux, win32]
+ license: MIT AND Apache-2.0
permissions: [net, fs, subprocess]
- when_to_use: The owner asks to read or act on a page in their real, signed-in Chrome, wants to keep working in another tab meanwhile, or needs to take over the same page for a login, CAPTCHA or decision and hand it back.
+ when_to_use: The owner wants work done in their own signed-in Chrome - in a tab they share and in background tabs for the same work - while they keep using the browser, and takes the page over by saying so when a login, a code or a decision is theirs.
model_experience:
- what_model_sees: Seven browser_* tools appear when the host has bound this skill's MCP server to the task. browser_state is one JSON object; actions return a receipt with one outcome per step. Installing the skill alone adds no tool.
- token_effect: Seven small schemas per round while bound. Snapshots, page text and screenshots cost what they contain; a read with cache_key can be searched later without reading the page again.
- timeout_sec: 300
+ what_model_sees: Once the owner has imported this skill's server under Settings → Advanced → MCP Servers and enabled it, 25 tools named mcp_chrome_browser__*. Two are this skill's (browser_connect, browser_disconnect) and 23 are the official Playwright MCP 0.0.84 tools (browser_snapshot, browser_click, browser_type, browser_fill_form, browser_tabs, browser_take_screenshot and others). Action results are short Playwright markdown with the Playwright code that ran, the page address and title when they changed, and any dialog. browser_snapshot returns the page's accessibility snapshot inline, and screenshots arrive as image content. Installing the skill alone adds no tool.
+ token_effect: About 25 tool schemas wherever the host loads this server's tools. Each snapshot costs what the page's accessibility tree contains; screenshots cost an image.
+ timeout_sec: 60
scripts:
- - name: mcp_server.mjs
- description: The task's browser executor, an MCP stdio server. The host starts it for one task attempt; it is not run by hand.
- name: setup.mjs
- description: Setup Ouroboros runs for the owner. "init" creates pairing, "install-native --browser" registers that browser's local helper, "pair" confirms the extension, "rotate" replaces pairing, and "recover --confirm" moves malformed state aside. It never prints the credential.
+ description: "status - checks the pinned dependency and patch, prints the document for Settings → Import mcp.json and the owner's steps. Writes nothing, starts nothing."
+ - name: server.mjs
+ description: The MCP stdio server. The host's task MCP holder starts it from the imported Settings entry; it is not run with skill_exec.
install_specs:
- kind: npm
- package: ws@8.22.0
+ package: "@playwright/mcp@0.0.84"
---
- # Chrome browser
-
- This skill lets one Ouroboros task work in the owner's real Chrome without
- taking the browser away from them. It has three parts:
-
- - `extension/` — a Chrome extension (Manifest V3) the owner loads once;
- - `scripts/mcp_server.mjs` — the executor the host starts for a task attempt;
- - `scripts/setup.mjs` — the owner's pairing helper.
-
- It needs an Ouroboros host that binds a skill's MCP server to a task with
- `browser_protocol: ouroboros_chrome` and answers the guard described in
- [`PROTOCOL.md`](PROTOCOL.md). On a host without that, the skill installs and
- does nothing. POSIX hosts only in this version. Node 20 or newer (the scripts
- use the global Web Crypto).
-
- **What the permissions mean.** `net`: one loopback WebSocket port and the
- host's guard socket. `fs` and `subprocess` are declared because
- `browser_execute` runs the script it is given inside the executor's own Node
- process: the `vm` realm is not a sandbox, and such a script can reach the file
- system and start processes with the rights of that process. The host admits
- `browser_execute` only where it allows unrestricted execution; the typed
- `browser_action` operations use neither. The executor reads and writes only
- its state directory; `install-native` also writes one native-host registration
- for the browser it names, in that browser's user data directory.
- `OUROBOROS_CHROME_STATE_DIR` names the state directory for
- an installation (or a disposable test root); whoever sets the executor's
- environment chooses it, and the skill does not confine it further.
-
- ## What the owner does once
-
- Loading and pairing in personal Chrome are owner gestures, not agent consent.
- Ouroboros prepares the package, the private credential and a local helper; it
- does not silently load the extension or enter that credential in the owner's
- browser. The owner runs no script and copies no credential.
-
- 1. Ouroboros runs `setup.mjs init` and then `setup.mjs install-native --browser
- chrome` (or `chrome-for-testing`, `chromium`) as the OS user who runs that
- browser. The first creates private `pairing.json` (mode 600); the second
- registers a local helper for that browser on macOS or Linux. For a browser
- started with its own `--user-data-dir`, add `--user-data-dir` with the same
- directory. Neither prints the credential; repeating `install-native` updates
- paths after a move.
- 2. The owner opens `chrome://extensions`, turns on **Developer mode**, chooses
- **Load unpacked**, and picks the printed `extension` folder.
- 3. Ouroboros starts `setup.mjs pair`, which waits up to 120 s, and asks the
- owner to click the extension icon and choose **Connect**. The popup asks the
- local helper for the existing pairing, stores it in this Chrome profile and
- shares no tab; `pair` then records which extension proved it, and only that
- one is admitted afterwards. The popup shows **Pairing saved** until a task
- connects, then **Connected**. If the helper is unavailable, **Manual
- pairing** accepts the credential and port from `pairing.json`; never paste
- it into a chat.
- 4. Add the MCP server that `setup.mjs status` prints under `mcp_server` in
- Settings → MCP.
-
- `setup.mjs rotate` replaces the credential and clears the pinned extension.
- The popup keeps its old copy and disables **Connect** until the owner chooses
- **Forget pairing**. Start `pair`, then ask the owner to choose **Forget pairing**
- and **Connect**. The same steps apply after changing the port with `init --port`.
- If setup reports malformed pairing, nothing is replaced.
- `setup.mjs recover --confirm` moves that file aside unchanged (same bytes and
- mode) and creates a new credential;
- then the owner chooses **Forget pairing** and **Connect** while `pair` waits.
-
- Use the pairing in one Chrome profile at a time. To move it to another
- profile, choose **Forget pairing** in the old one or turn the extension off
- there; **Stop sharing** ends tab access but keeps the connection. The helper
- sees the extension ID, not the profile, and the listener admits one extension
- connection, so a second profile cannot be detected: when the popup cannot
- connect it says that no task may be running or that another profile may be
- connected. Automatic multi-profile provisioning is not supported; separate state directories and ports alone do not arrange separate native-host registrations.
-
- An unpacked extension still requires Developer mode and Load unpacked. Native
- bootstrap registration supports Google Chrome, Chrome for Testing and Chromium
- on macOS and Linux; elsewhere only Manual pairing remains. This package is not
- published in the Chrome Web Store and does not provide one-click installation.
- A managed Chrome policy may also block Developer mode, unpacked extensions, or
- Native Messaging.
+ # Chrome (shared tab)
- After a package update, use **Reload** for this unpacked extension in
- `chrome://extensions`. Do not forget the pairing or enter the credential again.
- The build digest checks package files on disk, not the old worker's loaded code;
- verify the new input-preparation receipt and the requested page effect after Reload.
+ One Ouroboros task can work in the tab the owner shares and in background tabs it
+ opens for the same work. Connecting requires approval and activates the selected
+ tab; later handback happens by conversation, without a technical handoff lock.
+ The skill uses Microsoft's unchanged **Playwright Extension** and the official
+ **Playwright MCP server** (`@playwright/mcp` 0.0.84 with `playwright-core`
+ 1.65.0-alpha-2026-10-30). An exact, pinned patch adapts the server:
- ## What happens in a task
+ 1. **Background work.** Tabs the agent opens are created without being activated,
+ selecting a tab does not bring it to front, and clicks and checkboxes work in a
+ tab that is not visible (see "Input in a background tab").
+ 2. **Explicit connection.** The server never starts, finds or reads a browser or a
+ profile. `browser_connect` returns a one-time link; the owner opens it in their
+ own Chrome and picks the tab to share.
+ 3. **Handback by conversation.** There is no handoff lock and no turn-taking. When the
+ owner wants to act in the page, the agent stops sending actions until they say
+ they are done, then reads the page afresh.
- - The executor listens on one loopback port (default 47831). The extension
- tries that port when its popup opens, every 30 seconds, and within seconds
- after its connection to an executor closes (retries at 1, 2, 4, 8 and 16 s),
- so a task that just started may wait up to 30 s for it.
- - If the first `browser_state` runs before setup or pairing is complete, the
- same executor checks the pairing file again on a later call. A listener
- already listening is kept. A busy port (`BROWSER_PORT_BUSY`: another task's
- executor holds it) is tried again on the next call, not remembered.
- - **One port, one executor at a time.** When a lease ends with nothing to
- resume — `browser_disconnect`, a refused document, a refused connection — the
- executor closes its listener so another task's executor (for example a
- delegated browser executor) can bind the port. A handoff keeps it. The next
- `browser_connect` binds the port again and waits for the extension.
- - **Your tab.** Open the tab, click the extension icon, **Share this tab**.
- Its id appears in `browser_state.approved_tabs`; `browser_connect` with that
- `tab_id` attaches. The extension has no permission to list tabs or to read
- their titles and addresses, and no other id can be connected.
- - **The agent's own tab.** `browser_connect` with a `url` opens a new tab in
- the background. While the agent holds it, it is closed again on
- `browser_disconnect`, executor exit, a lost connection or a refused
- navigation. Once it has been handed to the owner (`browser_handoff`, Stop
- sharing, Cancel) it is theirs and is never closed by the skill — also not
- after `browser_resume`, on executor exit, a worker restart or Forget pairing.
- - While attached, Chrome shows its own "started debugging this browser" bar
- and the extension icon shows **ON** on that tab. **Cancel** on that bar, or
- **Stop sharing** in the popup, ends the session at once and for good.
- - Nothing in this skill activates a tab, focuses a window or moves the system
- pointer. Clicks and typing are Chrome's own trusted input events sent to the
- attached tab, which may stay in the background.
+ ## Setup (the owner, once)
- ## Tools
+ 1. In the Chrome profile to share, install **Playwright Extension** by Microsoft from
+ the Chrome Web Store:
+ <https://chromewebstore.google.com/detail/playwright-extension/mmlmfjhmonkocbjadbfplnigmagldckm>.
+ This skill was tested with version 0.4.0 (relay protocol 2).
+ 2. Enable the skill. Ouroboros installs its one declared npm dependency,
+ `@playwright/mcp@0.0.84`, which brings exactly `playwright-core` and `playwright`
+ 1.65.0-alpha-2026-10-30. npm must be on PATH. No browser is downloaded.
+ 3. Run `setup.mjs status` (with `skill_exec`). It checks every pinned file and the
+ patch, and prints `mcp_import_document`, ready to paste into Ouroboros:
- | Tool | Use |
- | --- | --- |
- | `browser_state` | Who owns the executor, what is leased, whether it was revoked or detached. |
- | `browser_connect` | Exactly `{tab_id}` (a shared tab) or exactly `{url}` (own background tab). |
- | `browser_action` | One typed operation, or `sequence` with up to 32 of them: a single step or a short fixed series without a loop or a condition. |
- | `browser_execute` | The main tool for a series of steps: a script in the task's persistent realm runs one whole mechanical stretch (a loop over known pages or elements, waits, results kept in `vars`) in one call. Only where the host allows unrestricted execution. |
- | `browser_handoff` | Detach now and leave the page, with everything typed into it, to the owner. |
- | `browser_resume` | Attach to the handed-off tab again. |
- | `browser_disconnect` | End the lease and report whether Chrome confirmed the detach. |
+ ```json
+ {"mcpServers": {"chrome_browser": {"type": "stdio",
+ "command": "<the node it ran with>", "args": ["<skill>/scripts/server.mjs"],
+ "task_session": true, "browser_policy": true}}}
+ ```
- Operations: `observe`, `read`, `get_text`, `get_attribute`, `snapshot`,
- `dom_snapshot`, `ax_snapshot`, `screenshot`, `wait`, `navigate`, `click`,
- `double_click`, `fill`, `type`, `press`, `check`, `uncheck`, `scroll`,
- `cached_text`. A target is `{selector}` (CSS, must match one element, or add
- `nth`) or `{handle}` from `snapshot`.
- For `read` and `get_text`, `start` is a zero-based text offset and `max_chars`
- is the returned window size (20,000 by default, at most 200,000). Their
- `coverage` gives the captured `[start,end)` and total text length. To keep a
- larger window without returning it all, pass `cache_key` and `capture_chars`
- (up to 200,000); the response still returns only `max_chars`. For example,
- `{"start": 0, "max_chars": 2000, "capture_chars": 60000, "cache_key": "raw"}`
- captures up to 60,000 characters and returns the first 2,000.
- `cached_text` is answered only while a lease is connected; the cache itself
- survives handoff and navigation, so resume first. It reports its coverage; a
- search with no matches and
- `complete:false` says only that the captured window has no match. To inspect
- an uncaptured offset, issue another `read` or `get_text` with that `start`.
- Coverage describes rendered DOM text, not proof that a virtualized site exposed
- the entire underlying file.
- Before first input in each document/attachment, the executor prepares one
- renderer frame without activating the tab; pixels are discarded, not returned.
- A missing frame refuses before input, never triggers a sacrificial/repeated click.
- Still verify the requested effect: dispatch acknowledgement alone is not success.
+ Both flags are host features: `task_session` keeps one server process for a task's
+ tool loop, so a connection survives between calls; `browser_policy` gives that
+ process the host's request guard. On a host without them, or with the guard missing,
+ `browser_connect` refuses (`BROWSER_GUARD_REQUIRED`) and nothing is contacted.
+ `"ready": true` from `setup.mjs status` covers only the pinned dependency and the
+ patch; whether the host runs the server with both features shows when
+ `browser_connect` is called.
+ 4. **Import.** In **Settings → Advanced → MCP Servers** choose **Import mcp.json**, paste
+ the whole document and choose **Preview**. It reads `Add (disabled)` and `Server ID
+ chrome_browser`, and says what both flags mean. Choose **Apply 1 to draft**: nothing is
+ saved or started yet. If it says `Not imported: browser_policy, task_session`, this
+ Ouroboros cannot run the server yet: stop and report that sentence. `mcp_server_entry`
+ in the same report is one saved settings row, for a tool that writes settings itself;
+ Import refuses that shape.
+ 5. **Enable.** The new server is off. Turn on its **Enabled** box and **Enable MCP client**.
+ 6. **Save.** Choose **Save Settings**. Ouroboros lists the tools through a short-lived
+ server process that connects to no browser. The card may still say **Not listed yet**
+ after that background listing finishes: choose **Refresh saved catalog** or reload
+ Settings to see the 25 tools. Keep the Server ID: it names every tool. The display
+ name can be changed.
- The operation enum excludes `select`, `drag`, `upload`, and `submit`: validating
- hosts refuse them at the schema boundary. Direct executor calls additionally
- return `BROWSER_OPERATION_UNSUPPORTED` without dispatch. A normal `click` or `press` can
- still trigger a page's own form submission. `type` and `fill` verify that the
- intended field holds focus after their trusted pointer click, before sending
- keyboard input. If the page redirects focus, the click stays dispatched and
- the step fails without typing into the other field.
+ ## Using it (for the agent)
- ```json
- {"operation": "sequence", "args": {"steps": [
- {"operation": "click", "args": {"selector": "#start"}},
- {"operation": "wait", "args": {"selector": "#step2", "timeout_ms": 8000}},
- {"operation": "fill", "args": {"selector": "#step2", "text": "value"}},
- {"operation": "read", "args": {"cache_key": "after"}}
- ]}}
- ```
+ 1. **Connect.** Call `browser_connect`. It answers at once with a one-time link
+ (`chrome-extension://mmlmfjhmonkocbjadbfplnigmagldckm/connect.html?...`). Give it
+ to the owner only, in this conversation: it is a local capability for their
+ browser, not a diagnostic to paste elsewhere.
+ 2. **Approval.** Ask the owner to open their own Chrome, paste the link into a **new
+ tab**, pick the tab to share and choose **Allow & select**. Chrome switches to that
+ tab once - that is the agreed activation. `browser_tabs` never activates a tab; a popup
+ a page opens can (see "Limits you must not hide"). Until the owner says they approved,
+ other tools answer `BROWSER_NEEDS_APPROVAL`; nothing is queued and nothing happens later
+ by itself.
+ Calling `browser_connect` again while waiting returns the same link; it stops working
+ when the server session ends or after 15 minutes without approval. If Chrome refuses the
+ chosen tab, tools answer `BROWSER_TAB_NOT_ATTACHED` with Chrome's reason; the next
+ `browser_connect` says why the last attempt ended and gives a new link.
+ 3. **Work.** `browser_snapshot` returns the page's accessibility tree with element refs;
+ act with `browser_click`, `browser_type`, `browser_fill_form` (also for checkboxes and
+ radio buttons), `browser_select_option`, `browser_press_key`, `browser_navigate`.
+ Action results are short: the Playwright code that ran (for example
+ `await page.getByRole('button', { name: 'Save' }).click();`), the page address and title
+ when they changed, and any dialog. A failed action returns its error. Call
+ `browser_snapshot` to see the page afterwards. Results point at no file: a screenshot, or a
+ response body from `browser_network_request` that is an image, is attached as an image;
+ another binary body is described and not returned; and console output is read with
+ `browser_console_messages`. For other
+ pages of the same work, open background tabs with `browser_tabs` action `new` (optionally
+ with a `url`); `select` changes which tab the agent acts in, not which tab the owner sees.
+ 4. **Handback.** When the owner says they will do something themselves - a login, a
+ code, a payment, a decision - stop sending actions. When they say they are done,
+ call `browser_snapshot` before anything else: their typing, login and unsaved text
+ are in the page. Never ask for a password or a code in chat.
+ 5. **Dialogs.** An `alert`/`confirm` the page shows blocks the agent's page tools
+ until someone answers it. The owner may answer it in Chrome; answer it with
+ `browser_handle_dialog` only when that is clearly the agent's to decide.
+ 6. **End.** `browser_disconnect` ends the connection when the work is done or the owner
+ asks. Every tab stays open with what was typed into it. A connection that was lost
+ is never re-established by itself: ask for a new approval with `browser_connect`.
- ```js
- // browser_execute: values in `vars` stay for the next call
- vars.snap = await browser.snapshot({});
- const links = vars.snap.elements.filter((e) => e.tag === 'a');
- await browser.click({ handle: links[0].handle });
- return (await browser.read({ cache_key: 'page' })).length;
- ```
+ Only tabs opened with `browser_tabs` action `new` can be closed by the agent. The
+ owner's shared tab and tabs a page opened stay open.
- ## One call per mechanical stretch
+ ## What the owner should know
- Step-by-step control — one tool call, one model round, the next call — is for
- the moments that need judgment, not the norm. The norm is one `browser_execute`
- (or one typed `sequence`) for the whole mechanical stretch up to the next
- semantic uncertainty: a loop over a known list of pages or elements, reading
- many pages, waiting with `browser.wait`, collecting results in `vars`, and
- returning one compact summary. A model round costs seconds; a step inside a
- script costs milliseconds, and the receipt lists each step's outcome, not every
- page's text — only what the script returns comes back.
+ - The connect page warns that the connection reaches the whole browser profile:
+ signed-in sessions and cookies of every site, in any tab the agent opens. That is
+ accurate. Other existing tabs are not attached, listed or changed.
+ - The connect page also shows a "bypass" token (`PLAYWRIGHT_MCP_EXTENSION_TOKEN=...`)
+ that lets a Playwright client connect without this dialog. This skill never uses it:
+ every connection needs the owner's approval. The owner should not share that token.
+ - The shared tab and the agent's tabs sit in a tab group named "Playwright ·
+ Ouroboros". Dragging a tab into the group hands it to the agent; dragging it out, or
+ closing the group's last tab, takes it back (the connection ends with the last tab).
+ - Chrome may show its own debugging notification while the extension is attached.
+ Page captures do not show browser chrome. The headed check observed no viewport-height
+ change on attachment; the notification's appearance and wording were not qualified.
+ - If the extension's background worker is stopped while a tab is shared, the connection is
+ lost and Chrome keeps the extension attached to that tab, so sharing that same tab again
+ fails. Another tab can be shared. No way to share that same tab again was tested. This
+ was seen in headless tests after a forced stop; the worker was not stopped by 150
+ seconds of idle.
- Observe and start a new round only when something needs judgment: a login or
- CAPTCHA, a page whose structure is not what the script expected, an action
- whose effect is unknown, or a decision the owner would want to make. A new
- page by itself is not such a moment: navigate, wait, read and go on inside the
- script. `sequence` is for a short fixed series (at most 32 steps, stopping at
- the first failure); anything with a loop or a condition is a script.
+ ## Errors and what they mean
- This is not "one script for everything". Write the script to stop at the first
- thing it cannot decide and return what it collected together with what it saw
- (the URL and the first lines of the unexpected page, for example); keep its
- position in `vars` so the next call continues instead of starting over. The
- rules of this skill hold inside a script exactly as between calls: a step with
- an `unknown` outcome is not sent again — read the page; a login or CAPTCHA is
- a `browser_handoff`, never a retry; one script is bounded by
- `OUROBOROS_CHROME_EXECUTE_TIMEOUT_MS` (50 s by default) and by the host's
- Stop. Keep each `browser_execute` under about 50 s: the host's per-call limit
- is 60 s by default (`MCP_TOOL_TIMEOUT_SEC`) and a call that exceeds it ends
- the whole task's browser session, not just the script. Split a longer loop
- into resumable chunks that keep their position in `vars`, and prefer
- `browser.wait` to `sleep` (30 s at most per call). What a cut script already
- put in `vars` stays there.
+ | Code | Meaning | What to do |
+ |---|---|---|
+ | `BROWSER_NOT_CONNECTED` | No connection yet. | `browser_connect`. |
+ | `BROWSER_NEEDS_APPROVAL` | Link given, owner has not approved. Nothing was done or queued. | Wait for the owner. |
+ | `BROWSER_ATTACHING` | Approved; setting up. | Call again shortly. |
+ | `BROWSER_TAB_NOT_ATTACHED` | The owner approved, but Chrome refused to attach the chosen tab (its answer is quoted), for example because the extension still holds an attachment from before its worker restarted. Nothing was opened instead. | Tell the owner. With a new `browser_connect` link they can share another tab. Approving the same tab again fails the same way; no way to share it again was tested. |
+ | `BROWSER_CONNECTION_LOST` | Extension disconnected, tabs closed, extension stopped or Chrome quit. | New `browser_connect`. |
+ | `BROWSER_APPROVAL_EXPIRED` | Not approved within 15 minutes. | New `browser_connect`. |
+ | `BROWSER_DISCONNECTED` | Ended with `browser_disconnect`. | New `browser_connect` if needed. |
+ | `BROWSER_GUARD_REQUIRED` / `_INVALID` / `_NOT_ARMED` | The host's request guard is not configured (`_REQUIRED`); the host set it up without a usable address or token (`_INVALID`, reported by `browser_connect` before any link is made); or it did not confirm the route after approval (`_NOT_ARMED`). | Report it; it is a setup or host problem. |
+ | `BROWSER_INPUT_NOT_READY` | Chrome produced no frame for the tab; no input was sent. | Retry once later or report. |
+ | `BROWSER_INPUT_UNCONFIRMED` | Chrome accepted a click but the page saw no trusted event at the target. It was not sent again. | `browser_snapshot`, then decide. |
+ | `BROWSER_INPUT_NOT_VERIFIED` | A click went out once, but its delivery could not be checked (the page refused the check, or the document changed, for example because the click navigated). It was not sent again. | `browser_snapshot` to see its effect. |
+ | `BROWSER_TAB_CHANGED` | The tab this call was checked for closed, or stopped being current, while the host was being asked. Nothing was done, in it or in the tab current now. | `browser_snapshot` to see which tab is current, then decide. |
+ | `BROWSER_RECORD_ORIGIN_UNKNOWN` | A page made more redirects than are kept for checking (512 addresses per document), so its console entries cannot be judged and are withheld. | Read the page itself; a new document starts fresh. |
+ | `BROWSER_ARGUMENT_REFUSED` | `filename` (results come inline), `isolatedContext` (owner's profile only) or `_meta` (upstream's output options `cwd`, `raw` and `json`; results always come in the advertised form). | Drop the argument. |
+ | `CHROME_BROWSER_NOT_INSTALLED` / `_PIN_MISMATCH` / `_PATCH_FAILED` | The dependency is missing or not exactly the pinned files. | Re-enable the skill; check `setup.mjs status`. |
- ```js
- // browser_execute: every page of a known list in one call; stop where judgment is needed
- const snap = await browser.snapshot({ max_nodes: 300 });
- const urls = snap.elements
- .filter((e) => e.tag === 'a' && e.href && e.href.includes('/models/'))
- .map((e) => new URL(e.href, snap.url).href);
- vars.sizes = vars.sizes || {};
- for (const url of urls) {
- if (url in vars.sizes) continue; // a later call continues, it does not restart
- await browser.navigate(url);
- await browser.wait({ selector: 'h1', timeout_ms: 10000 });
- const page = await browser.read({ max_chars: 20000 });
- const size = page.text.match(/Model size\s+([\d.]+[KMB]) params/);
- if (!size) return { stopped_at: url, seen: page.text.slice(0, 300), done: vars.sizes };
- vars.sizes[url] = size[1];
- }
- return { pages: urls.length, sizes: vars.sizes };
- ```
+ A refusal from the host's guard is shown verbatim (for example
+ `BROWSER_LOCAL_READONLY_BLOCKED: ...` for `browser_evaluate`). A request the guard
+ refuses fails in the page with `net::ERR_BLOCKED_BY_CLIENT`, and the tool result lists
+ `method origin+path: reason` for refusals during that call. The guard also decides
+ every address `browser_navigate` or `browser_tabs` `new` would open, before it is
+ opened (a new tab without a `url` opens `about:blank`): the same decision the built-in
+ browser makes, including for `javascript:`, `data:` and `about:` addresses, which send
+ no request a route could judge. Upstream Playwright itself refuses `file:` addresses.
- ## Delegating to an executor
+ ## Input in a background tab
- Delegate browser work to a task-focused child of the same model. Keep the main
- agent's full context unchanged: it briefs, supervises and decides semantic forks;
- the child drives the page. Do not mirror each click or reread every page in main. The standing policy (escalation
- classes E0–E7, how the child waits for an answer, the return schema) and the
- brief template live once in
- [`references/executor-brief.md`](references/executor-brief.md); the child reads
- that file first, the parent does not paste it.
+ Upstream Playwright waits for the target to stay put across two animation frames,
+ and a hidden tab runs none, so its clicks time out there. This server keeps every other
+ upstream check - visible, enabled, not inert, scrolled into view, a clickable point,
+ and the hit-target test that the element itself would receive the input - and replaces
+ only that wait: the element's box is read twice, 60 ms apart on the server's clock. The
+ other states are read after that wait, as upstream reads them after its own, so a control
+ that stays put but turns disabled during it is not clicked.
+ A covered, moving, disabled or hidden target is not clicked; Playwright retries until
+ its 5 s action timeout and then reports why. There is no force option.
- 1. Release the browser: `browser_disconnect` frees the port (a handoff keeps it).
- 2. Fill the brief template; `owner_words` — the owner's exact sentences — is
- mandatory, and the parent withholds what would bias a measurement.
- 3. Schedule, then wait with `wait_task` (not a cold sleep: the child may escalate):
+ Before the first input into each document the server captures one frame of the tab and
+ discards it: a newly loaded hidden renderer in headed Chrome can otherwise accept input
+ without delivering it. After a click it asks the page whether a trusted pointer event
+ reached the target; if none did, the click is reported as `BROWSER_INPUT_UNCONFIRMED`
+ and never repeated. If that check could not be installed or read back (for example the
+ click navigated), the result says `BROWSER_INPUT_NOT_VERIFIED`: the click went out once
+ and its delivery is unknown. Checking and unchecking also verify the new state
+ (upstream). A click goes out at most once per call: if Playwright would send it again in
+ the same call (it does for a checkbox replaced right after the click that still shows the
+ old state), the call ends with Playwright's timeout error instead, and the click is not
+ repeated.
- ```
- schedule_subagent(
- subagent_id="<the same API-model row as the main agent>",
- objective="<the goal in one sentence>",
- expected_output="the return JSON of references/executor-brief.md",
- input_sources="declared",
- write_surface="external_workspace", write_root="<project folder>",
- initial_tool_names=["mcp_chrome_browser__browser_state", "mcp_chrome_browser__browser_connect",
- "mcp_chrome_browser__browser_action", "mcp_chrome_browser__browser_execute",
- "mcp_chrome_browser__browser_handoff", "mcp_chrome_browser__browser_resume",
- "mcp_chrome_browser__browser_disconnect",
- "escalate", "await_messages", "read_file", "finish_task"],
- external_tool_grants=[<the same seven mcp_chrome_browser__* names>],
- context="<the filled brief, starting with: read references/executor-brief.md of the chrome_browser skill>")
- ```
+ ## Request guard
- The tool names carry the MCP server id from Settings → MCP (`chrome_browser`
- with the entry `setup.mjs status` prints). `external_tool_grants` is what lets
- the child use the browser tools at all; `initial_tool_names` only makes them
- resident from its first round.
+ Every request that Playwright routes for the attached tabs - the agent's and the
+ owner's own browsing in them alike - is decided by the host's guard before it is sent.
+ This is a floor, not network containment.
- 4. A child's escalation arrives as mail. Answer with
- `forward_to_worker(task_id=<child>, message=...)`; when it is above your
- authority, `escalate` it to the owner yourself, quoting the child's
- question verbatim. The child sleeps warm meanwhile, keeping its tab.
+ What the agent reads follows the built-in browser's rule. Before a page tool acts in a
+ tab, and before any result is returned, the page's address and every redirect hop
+ recorded for the documents it now shows, subframes included - whoever navigated it, the
+ agent or the owner - go to the guard as addresses this task may read. If one is refused,
+ nothing is done in that page, or the result is replaced by the guard's reason; a tab title
+ in a tab list is judged with its tab. A hop counts until a new document really replaces the
+ one it led to: a 204, a download or a redirect that fails leaves the old document in the tab,
+ and the hop with it, and a new document clears it. This withholds what reaches the agent: a
+ hop's request has already been sent by then. Moving to another page or tab
+ (`browser_navigate`, going back, `browser_tabs`) is never refused because of the page being
+ left.
- ## Reading a receipt
+ Closing a tab hands "current" to a neighbour, and the owner or a page can do that at any moment.
+ An operation on the current tab, including navigation or going back, keeps the tab it was
+ checked for: if that tab closes during the checks, the call ends with `BROWSER_TAB_CHANGED`
+ and nothing is done. Navigation can still leave a refused page; it cannot silently switch to
+ its neighbour. Explicit tab selection, creation and closure remain available. A result is
+ also judged by the tab the operation worked in and by a listed tab, even if they closed before
+ it returned. A close before a call makes that call's check and action apply to the neighbour.
+ The check cannot hold a page still: a document that commits between the check and the action
+ gets the action, and the result is then judged by that document.
- Every step has an outcome. `observed`: a value was read. `not_dispatched`:
- nothing reached Chrome. `dispatched`: Chrome acknowledged the input, the effect
- was not checked. `confirmed`: the effect was read back. `unknown`: something
- may have been sent and the answer is missing — it is never sent again; read
- the page before deciding.
+ Playwright keeps a tab's console entries, page errors and network requests across its
+ documents and returns them on request. They are judged by where they came from, not by the
+ page shown now: a console entry or page error by the document that made it (its address and
+ hops, and any subframe hop seen under it; a message's script location says nothing about
+ that), and a request by its own redirect chain and the document and frame that made it. A
+ tab's own document request belongs to the document it opens. `browser_console_messages` and
+ `browser_network_requests` leave out what this task may not read and say how many records they
+ withheld, and `browser_network_request` refuses that one record; the rest of the history stays
+ readable. Each address is asked once per call and asked again by the next call, so a changed
+ answer counts at once. A record made before this connection was watching (the moment of
+ approval), or a request a new tab made before Playwright reported that tab, is taken as the first
+ document's, however long ago that document was replaced. A record belongs to the document that was current
+ when Playwright delivered it, in the order of the commits. A page being left can still log or
+ send something while it unloads (a `pagehide` or `unload` handler, a beacon); if Chrome reports
+ that after the next document's commit, it is judged as the next document's. Whether Chrome does
+ so is not shown by this skill's tests, whose stand-in Chrome reports only what a test states: it
+ is a stated limit, not a known leak.
- Before a pointer action the extension scrolls the target into view, reads its
- box twice a short interval apart, and asks the page which element would
- receive a pointer event at that point. A hidden, disabled, moving or covered
- target is refused with nothing sent — hidden includes fully transparent
- (`opacity: 0`) on the element or an ancestor. There is no force option and no
- `element.click()` fallback.
+ Not judged as requests:
- ## Handoff
+ - redirect hops (Playwright continues them without a route; their addresses are judged
+ before output instead, as above, after the request went out);
+ - WebSockets;
+ - requests Playwright cannot associate with a frame or worker, and requests Chrome
+ sends before the extension attached a tab (a popup's or new tab's first load; for
+ such a document, for a document Chrome restores from its back/forward cache, and for the
+ owner's tab as it was when shared, only the page's own address is known to the check above,
+ not the hops before it);
+ - service-worker requests: Playwright routes them only for workers it attaches; whether
+ the extension relay attaches any was not verified;
+ - CORS preflights go to the real server unrouted (the actual request is routed). Stock
+ Playwright answers them itself with a permissive 204 while any route exists; this
+ server's patch keeps the server's own CORS answer;
+ - what a page sends as it is being left. In one headless check (Chrome for Testing 153, the
+ agent navigating from a page the host refuses to an allowed page on another site), three
+ beacon POSTs from its `pagehide` and `unload` handlers reached their servers with the old
+ page's `Origin`, after the new page's first requests. The guard was not asked, Chrome's
+ debugger reported none of them and the record tools returned none. That is one observed
+ case, not a proven leak or a rule for other navigations.
- `browser_handoff` detaches Chrome's debugger. The page is not reloaded and
- nothing typed into it is lost; the owner logs in, solves the CAPTCHA or edits
- the form by hand. `browser_resume` attaches again to the same tab and reads the
- document afresh; element handles from before are stale, cached text is kept.
- A resume the host refuses leaves the tab with the owner, still handed off and
- open: resume again later, or end the handoff with `browser_disconnect`.
- If `browser_disconnect` races a pending resume, it ends that handoff; a later
- refusal cannot make the old tab resumable again.
- Never ask the owner for a password or a code in chat.
+ Bodies: an upload's file parts are not exposed by Chrome, so such a body is reported as
+ present but unreadable. Requests in the moment between attaching a tab and installing
+ the route are not judged; the agent cannot act in that moment.
## Limits you must not hide
- - **Requests.** While attached, a request Chrome pauses for the tab waits for
- the host's verdict. Verified on a real Chromium: main-frame documents, form
- posts and fetch/XHR, each with its complete body, and every redirect hop of
- them as a request of its own. Not verified, so not claimed: iframe documents
- and requests from inside iframes, uploads with file parts (reported as an
- incomplete body, which the host refuses), beacons, dedicated and shared
- workers, service workers, WebSockets, and tabs or pop-ups the page opens. Do
- not describe the guard as network containment.
- - **The first load of the agent's own tab** happens before the debugger is
- attached: the host admits the URL and then sees the landing URL, but that
- load's individual requests are not judged.
- - **Between calls**, a current compatible host may admit complete bodyless
- GET/HEAD subresource reads on the same valid lease, under its current task
- and destination policy. Documents, bodies and unknown resource types are
- refused; older hosts refuse all requests without an invocation. GET/HEAD
- describe request shape, not a guarantee that the server changes nothing.
- - **Navigation after the call.** A click (or key, or check) that asks the page
- to navigate — a link, a form post, a script — is followed inside its step,
- also when Chrome starts that navigation late, for at most 10 seconds. The
- step's `navigation` says what became of it: `committed`, `refused`,
- `same_document`, `abandoned`, `unknown`, or `null` when the input asked for
- none. A navigation the page starts later by itself, when no call is running,
- is refused at its request and does not happen. But one still `unknown` at
- the bound can commit after the call: the host then refuses the new document,
- the lease is released (`navigation_refused`) and the page has already
- changed. Nothing is ever sent again; read the page.
- - **A refused document fails its call.** When the host refuses a document the
- page has committed, the call reports an error with the host's code, the step
- keeps its true outcome, and the page is left on that document.
- - **Same-document changes** (`#fragment`, `history.pushState`) change only
- `lease.url`.
- - **A script that never yields** blocks the executor, including
- `browser_handoff`; the host's Stop still ends the process. The script realm
- is not a sandbox.
- - **A script is cut where it waits.** A handoff, a disconnect, a revocation or
- a lost connection ends the running `browser_execute`: the steps it started
- answer (cut), the script gets one turn to catch that and return, and a
- script still waiting then — also on a promise of its own — ends with
- `BROWSER_INVOCATION_CLOSED` (or the host's code for a refused document). The
- script itself cannot be stopped; its `browser` object and `sleep` only
- refuse from then on. `browser.state()` returns a promise, like every other
- member: `await` it.
- - **Revoked while attaching.** If you stop sharing, forget the pairing, cancel
- on Chrome's bar or close the tab while it is still being attached, the
- connection is refused and, for the first three, the session is revoked for
- good. A tab the agent was opening then stays open: it is yours.
- - **JavaScript dialogs** (`alert`, `confirm`, `beforeunload`) are not
- answered by this skill. Steps are written to report `BROWSER_DIALOG_OPEN`
- while one is open; that path has not been exercised against a real dialog.
- - **Not implemented:** `select`, `drag`, `upload`, `submit`; elements inside
- iframes; more than one lease.
- - **If the extension is removed or crashes** while attached, Chrome detaches
- the debugger itself and lets paused requests continue. The executor reports
- a lost transport and never claims a confirmed detach.
- - **Executor exit is not proof of detach.** Only `debugger_detach_ack: true`
- in `last_closure` is.
-
- ## Testing
-
- `tests/regressions.test.mjs` in this package runs the executor, the extension
- worker and the setup script against a mocked extension socket, a mocked
- `chrome.*` and a disposable guard socket — no Chrome, no pairing:
- `node --test --test-force-exit tests/regressions.test.mjs`.
- `node --test tests/observation_startup.test.mjs` also checks late pairing,
- source-bound text windows and cached-read coverage without a browser.
-
- `tests/chrome_browser/headless.test.mjs` and
- `tests/chrome_browser/input_frame.test.mjs` in the Hub repository drive the
- real extension in a disposable `--headless=new` Chromium. Its pairing step types the credential
- into the popup for the owner; it says nothing about a personal Chrome.
+ - **Bounded browser checks.** Headless checks and one headed session on macOS 26.4 used
+ Chrome for Testing 153, disposable profiles, local fixtures and extension 0.4.0 built
+ from Microsoft's source. Hidden trusted input, inactive agent-created tabs, image delivery
+ and same-page handback were observed. Headless checks also exercised redirect commitment
+ and historical-record refusals. Tab-closure races were checked through Playwright's pinned
+ consumers with a stand-in for Chrome. Stable Chrome, the pinned 157 browser, the Web Store
+ extension binary, Windows/Linux desktops, hardware input and real signed-in sites were
+ not tested. The headed evidence includes corrected interpretation of saved focus records,
+ not a clean pass of every original test assertion or proof of all possible focus changes.
+ - **Popups can bring Chrome forward.** In both headless and headed CfT 153, trusted clicks
+ in a hidden tab on `target=_blank` and `window.open` opened active tabs and switched the
+ simulated owner's tab. In the headed session they also brought the Chrome application
+ forward. `browser_tabs new` stayed inactive. This skill does not block page-opened popups,
+ change the extension or switch anything back; it cannot promise uninterrupted foreground work.
+ - **One connection per server process.** A host timeout, restart or cold resume starts
+ a new process, which needs a new approval.
+ - **Navigation is bounded for the host's default timeout.** A navigation gives up at 45 s, plus
+ at most 5 s waiting for "load", instead of upstream's 60 s and 5 s, so under the host's default
+ 60 s `MCP_TOOL_TIMEOUT_SEC` the agent gets Playwright's error on a live connection. If that
+ setting is lowered below about 50 s, the host's timeout comes first and the approval is lost.
+ Other calls are not covered by this bound.
+ - **Not offered:** `browser_run_code_unsafe` (it runs arbitrary code inside this server,
+ outside the guard) and `browser_close` (replaced by `browser_disconnect`). Local files
+ can be uploaded only from this server's private session folder, so in practice not.
+ - **Child process:** the only one is Playwright's own `sw_vers -productVersion` on macOS
+ when it connects (for its user-agent string). The skill starts no browser or helper.
+ - **Files:** a private session folder in the system temp directory, removed when the
+ server exits normally (its input ends, or SIGTERM, SIGINT or SIGHUP). A killed or crashed
+ server leaves the folder behind, and nothing removes it later. Upstream keeps a copy of each
+ screenshot there, a log of the pages' console messages, and each binary response body
+ read with `browser_network_request`. Results do not point at these files: they say the
+ screenshot or image is attached, that a binary body is not returned, or that new console
+ entries are read with `browser_console_messages`. Also Playwright's temporary artifacts
+ folder (removed on disconnect) and, on disconnect, Playwright's attempt to delete a browser
+ descriptor it never created under `~/Library/Caches/ms-playwright/b`.
+ - **Downloads** are not taken over: the connection sets no download folder
+ (`connectOverCDP` with `noDefaults` keeps the browser's own download handling), so
+ Chrome saves a page's download where it saves the owner's downloads, and the session
+ folder never holds it. In the headless QA, a download the agent's click started and
+ one the owner started both landed in Chrome's default Downloads folder and stayed after
+ the session ended. The result of the click does not mention the download.
- The Core registry consumer uses the actual `ToolRegistry`/`MCPManager` with a
- disposable installation. Run it from the Hub root with explicit paths to the
- Core checkout, its Python environment, full Chromium, and the skill's Node
- dependency directory (the directory containing `node_modules`, not
- `node_modules` itself):
+ ## Provenance
- ```sh
- OUROBOROS_CHROME_TEST_CORE=/absolute/path/to/core \
- OUROBOROS_CHROME_TEST_PYTHON=/absolute/path/to/core/.venv/bin/python \
- OUROBOROS_CHROME_TEST_CHROMIUM=/absolute/path/to/Google-Chrome-for-Testing \
- OUROBOROS_CHROME_TEST_NODE_ENV=/absolute/path/to/chrome_browser/.ouroboros_env/node \
- node tests/chrome_browser/core_integration.mjs
- ```
+ See [`ATTRIBUTION.md`](ATTRIBUTION.md) and [`pins.json`](pins.json). The patch and its
+ licence notices are in [`patches/`](patches/).