feat: browser_intercept, managed daemon lifecycle, loopback-only hardening, MAIN-world console capture - #19
Merged
compnew2006 merged 30 commits intoSep 30, 2026
Conversation
…ement undefined" A selector that finds nothing (usually the page navigated or posted back) was reported as "Element undefined is gone from the DOM (feed virtualized)" plus a useless re-snapshot. It now names the selector and the page URL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ewrite, shadow DOM
Refs were looked up by a data-mcp-ref attribute nothing writes any more, so
every ref action fell through to the smart-selector fallback, whose first
step returned the FIRST querySelector match — clicks "succeeded" on the
wrong element, and hover/select/drag/fill_form/upload/press_key could not
use snapshot refs at all.
- lib/page-dom.js: page runtime (install-once, execDom) with ref registry →
first VISIBLE selector match across open+closed shadow roots and
same-origin iframes → verified fallback (unique, or nth among exact
role/tag/name twins; ambiguous = REF_GONE instead of a guess). Composed
hit-testing for occlusion, flat-tree text, CSS-independent names.
- All tools use it: click, type, press_key, hover, select, drag, fill_form
(select also matches option labels), upload_file (CDP objectId, works in
shadow roots/frames), scroll.
- browser_find: tokenized scoring over names/labels/attributes, role words
("search input", "... button"), shadow DOM, wrapper/echo suppression,
sort before limit, optional role filter, hint when empty.
- browser_click_text: case-insensitive name/composed-text match (CSS
uppercase no longer breaks exact), shadow DOM, clicks the owning control
with a trusted CDP click.
- browser_wait: any visible match (not just the first), plus text and
urlIncludes conditions.
- browser_text: shadow-DOM content, mode:"article", offset paging, 100k cap.
- browser_snapshot: flat tree (closed shadow roots, slots, display:contents),
short refs, no isNew on the first snapshot, path-only same-origin hrefs,
landmarks named by labels only, filter/depth/ref/maxChars (default 20k).
- tests: page-dom resolver/find/click_text/wait/snapshot/text suites on a
small fake DOM.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Benchmark T1: nodejs.org/dist/index.json froze its tab; text, evaluate and navigate each timed out at 125 s because an in-flight executeScript can't be aborted and every later call queued behind it on the tab mutex. - safeExec races every page function against an 8 s budget; a timeout marks the tab wedged (TAB_WEDGED, with recovery advice). While wedged, calls pay one 1.5 s probe instead of queueing; a responding page clears the mark, so does a new navigation or closing the tab. - Overlay show/hide is best-effort (1.5 s cap, skipped on wedged tabs); CDP attach/viewport/focus-emulation probes are bounded (5 s); evaluate checks responsiveness before attaching. - navigate (and the new browser_tabs action "reload") bypass the queue for a wedged tab and replace it: a new tab at the same window/index/active state, the frozen one closed. Measured 0.6 s vs 47-57 s waiting on the page (Page.crash and tabs.discard don't help; discard even changes the tab id). Results carry replacedTabId + the new tabId; browser_batch follows it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…screenshots
Claude-in-Chrome style "computer" actions, all additive:
- browser_click x/y (a real click at a viewport point, reports what it hit),
clickCount 1-3 (triple click), modifiers (ctrl+click...).
- browser_hover x/y; browser_scroll x/y sends a real mouse-wheel event at
the point (inner panels, maps, virtual lists).
- browser_press_key: space-separated sequences ("ArrowDown ArrowDown Enter",
"ctrl+a Backspace") and repeat.
- browser_type without ref/selector types into the focused field (and says
so when nothing but the page body has focus).
- browser_screenshot region {x,y,width,height} zooms into a rectangle (scale
up to 4, default 2), and every CDP screenshot reports how image pixels map
to the x/y these tools take — no coordinate-frame guesswork.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ull URLs - New tool browser_resize_window: width/height and/or state (normal, maximized, minimized, fullscreen) for the window holding a tab; reports the resulting window and viewport size (Claude-in-Chrome resize_window). - browser_tabs create active:false opens a tab in the background (the user keeps their tab in front); focus window:true also raises the window; list fullUrls:true keeps long URLs. - Tool guidance updated for the new capabilities. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… network content.js patched console in the isolated world, so the page's console.log/warn/info/debug never reached browser_console (only uncaught errors did). console-main.js (a MAIN-world content script) patches the page's console and hands entries to content.js as JSON on a private DOM event. - browser_console: pattern (regex), level (one or several), limit (latest N); reports the unfiltered total. - browser_network: failed requests are recorded (webRequest.onErrorOccurred: DNS, blocked, aborted…), urlPattern (substring, Claude-in-Chrome style) next to the regex filter, failed:true for errors and HTTP 4xx/5xx. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ones browser_upload_file gains imageBase64 (+fileName/mimeType) and fromScreenshot (any tab, optional region) — the Claude-in-Chrome upload_image case. The File is built in the page from the bytes: into an <input type="file"> (files + input/change) or, for any other target, as a dragenter/dragover/drop with a DataTransfer, which is what upload drop zones listen for. Target by ref, selector, x/y, or the first file input. Local file paths keep going through CDP DOM.setFileInputFiles as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New tool browser_gif (Claude-in-Chrome gif_creator): start / frame / stop / status / clear / export. While recording, the router captures a downscaled JPEG frame after every page-changing action (after the reply is sent, inside the tab mutex); export decodes the frames with OffscreenCanvas, marks click points with a red ring, and encodes a looping GIF89a with real pacing (0.4-2.5 s per frame). The MCP server writes the file (default ~/Downloads/browser-recording-<time>.gif) and returns its path, never the image bytes. lib/gif-encoder.js is dependency-free: fixed 256-colour palette (6x6x6 cube + 40 greys — UI screenshots are mostly greys and flat colours) and LZW; tests round-trip the LZW through a reference decoder, and Pillow reads the live output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bridge kept a single extension socket and closed it whenever another connected. Now every socket is its own connection (bridge-connections.ts): - Extensions announce a persistent browserId + label in helloAck (stored in chrome.storage.local); legacy ones count as "default". - A reconnect of the same browser replaces its old socket once the new one finished its handshake; other browsers stay connected. - Routing: a session's selected browser, else the most recently connected one — with one browser everything behaves exactly as before. - New tools, answered by the bridge itself: browser_list_browsers and browser_select_browser (browserId or label, "auto" = default). A session whose browser went away gets a clear error instead of the wrong browser; a released session forgets its choice. - A browser disconnecting fails only the calls sent to it; pings, pongs and handshake timers are per connection; control messages go to all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…les}}
New tool browser_shortcuts — Browser Controller's counterpart of
Claude-in-Chrome's shortcuts: save a named list of steps (browser_batch
actions with {{variable}} placeholders), then run it in ONE call with vars
filled in; list / show / delete. A run is a browser_batch (stops at the
first failing step, default tab, output last by default); a whole-string
placeholder keeps the value's type. Missing vars are reported up front.
Stored locally in ~/.browser-controller/shortcuts.json (atomic write;
BC_SHORTCUTS_FILE overrides). Runs in the MCP process like browser_batch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ilities - browser_gif export travels in 600 KB parts (the daemon's WebSocket frames are capped at 1 MB); the MCP tool reassembles and writes the file. - Version 2.4.0 (package, manifest); README: key capabilities and the tool tables updated for shared ref resolution, shadow DOM, frozen-tab recovery, coordinate actions, GIF, shortcuts, multi-browser and the new params. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…returned Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…0 lines); lint Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Local intercept work: extension handlers/lib, MCP intercept tool, router/state/cdp/tabs updates, registry/meta/dialog/network wiring, and coverage in extension-router, forward-handler, registry suites.
# Conflicts: # README.md # extension/events.js # extension/handlers/cdp.js # extension/handlers/tabs.js # extension/lib/router.js # extension/lib/state.js # mcp-server/src/tools/index.ts # mcp-server/src/tools/meta.ts # package.json # tests/extension-router.test.ts # tests/tools/registry.test.ts # vitest.config.ts
…fic bridge unit The bridge unit bound mcp-proxy to 0.0.0.0, which contradicts the loopback-only stance documented in SECURITY.md. Remote/n8n ingress is deployment-specific and belongs outside this repo.
|
ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing |
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…pping, iframe visibility Review findings on compnew2006#20: - [P1] A wedged tab's navigate skipped the queue and with it the lock check, so session B could replace and close a tab locked by session A. The router now checks ownership on that path, replaceFrozenTab refuses another session's tab too, and the owner's lock moves to the replacement tab instead of being dropped. - [P1] Screenshot coordinate mapping ignored the device pixel ratio (DPR 2: 1600x1200 image of an 800x600 viewport still said x = imageX). The mapping now comes from the real image size (PNG/JPEG header), and maxWidth caps the image by accounting for the ratio. - [P2] isVisible() did not look at enclosing iframes, so wait(visible) passed for a button inside an opacity:0 iframe. Visibility now requires every enclosing frame element to be visible. Each fix has a regression test that fails on the previous code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat: v2.4.0 — Claude-in-Chrome parity: reliable targeting, shadow DOM, frozen-tab recovery, coordinates, GIF, multi-browser, shortcuts
Review findings on compnew2006#19: - [P1] Tab-scoped rules became browser-wide: they were installed as dynamic rules without tabIds or resource types. They are now session rules (the only kind Chrome lets you scope to tabs) with the rule/scope tabIds and the requested resourceTypes. - [P1] Clearing left Chrome enforcing old rules: sync removed only the ids of the NEW set. It now removes every session rule in our id range (including ones from a previous service worker) before adding the current set. - [P1] scripts/daemon.mjs trusted any live pid in daemon.json: status said running, start skipped, stop killed an unrelated process. A pid now counts as the daemon only if the daemon answers — /status (now reports pid) must name the same pid; an older daemon must answer on the recorded IPC socket. Stale metadata is cleaned without signalling anything. - [P2] scopeKey deduplicates tab ids ("tabs:15", not "tabs:15,15"), so replacing a tab's rules replaces them; an empty set removes the scope. - [P2] Enforcement is reported truthfully: header rules are enforced as request-header modifications; mock rules (Chrome can't fake bodies) are ledger-only and listed under unsupported with enforcement "partial"; captures say "enforced" only for rules Chrome really has installed; list-rules marks each rule's enforced state. Tests: a stateful declarativeNetRequest mock (6 cases) and an impostor-pid lifecycle test (2 cases); all 8 fail on the previous code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 tasks done
Resolves the conflicts with 2.4.0 so compnew2006#19 merges cleanly once 2.4.0 is on main: - router/tabs/network/meta/README/tests: keep the 2.4.0 code, re-add browser_intercept (router dispatch, registry test, README row). - tabs tool timeout: 35s (2.4.0 reload waits up to 30s) instead of 20s. - Console capture: one MAIN-world script. Keep console-main.js (private DOM event) and drop page-console.js — window.postMessage reached the page's own "message" listeners. Bridge test updated to the kept contract. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI failed at `npm ci` (ERESOLVE): @babel/eslint-parser 7 needs
@babel/core 7, but core/preset-typescript are on 8. Bump the parser to
^8.0.6 and drop the removed `allowDeclareFields` preset option (the same
mismatch made ESLint fail to parse every .ts file). With TS linted again,
fix the 8 errors it surfaced: `{ cause }` on rethrown errors and one
useless initial assignment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Changes from running Browser Controller as a long-lived, shared daemon on a Linux workstation, rebased onto current
main(v2.3.0). Merges cleanly with the observe/act, batch, trusted-input and CDP-screenshot work; full suite passes (373 tests),tsc --noEmitis clean.What's in here
browser_intercepttool (MCP tool, extension handlerhandlers/intercept.js,lib/intercept.js) with unit tests on both sides.scripts/daemon.mjs+npm run daemon:{start,stop,restart,status};BROWSER_CONTROLLER_DAEMON_MODE=connectmakes the stdio client a thin adapter that waits for a managed daemon instead of spawning one; example systemd user unit underdeploy/systemd/.127.0.0.1(noWS_HOSToverride), plusSECURITY.md/.env.example/ README updates documenting that remote callers need a separately authenticated transport.extension/page-console.js, so console output is captured for page-sideconsolecalls, with a test.npm run smoke:real-chromeexercises extension reconnect across daemon restarts (systemd-aware).Notes for review
npm run lintcurrently fails onmainin my environment (eslint 10 +@babel/eslint-parser7 vs Babel 8 peer conflict;npm cineeds--legacy-peer-deps). Unrelated to this PR, so I left it alone.Test plan
npm run build && npm run typechecknpm test— 32 files / 373 tests