Skip to content

feat: browser_intercept, managed daemon lifecycle, loopback-only hardening, MAIN-world console capture - #19

Merged
compnew2006 merged 30 commits into
compnew2006:mainfrom
Bzcasper:feat/daemon-lifecycle-and-intercept
Sep 30, 2026
Merged

compnew2006 merged 30 commits into
compnew2006:mainfrom
Bzcasper:feat/daemon-lifecycle-and-intercept

Conversation

@Bzcasper

Copy link
Copy Markdown

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 --noEmit is clean.

What's in here

  • Request interception — new browser_intercept tool (MCP tool, extension handler handlers/intercept.js, lib/intercept.js) with unit tests on both sides.
  • Managed daemon lifecycle — scripts/daemon.mjs + npm run daemon:{start,stop,restart,status}; BROWSER_CONTROLLER_DAEMON_MODE=connect makes the stdio client a thin adapter that waits for a managed daemon instead of spawning one; example systemd user unit under deploy/systemd/.
  • Ingress hardening — daemon control plane hard-bound to 127.0.0.1 (no WS_HOST override), plus SECURITY.md / .env.example / README updates documenting that remote callers need a separately authenticated transport.
  • Page console capture from the MAIN world — extension/page-console.js, so console output is captured for page-side console calls, with a test.
  • Real-Chrome smoke test — npm run smoke:real-chrome exercises extension reconnect across daemon restarts (systemd-aware).
  • Dialog/network tool tweaks and registry/router test updates to keep the drift guards honest with the new tool set.

Notes for review

  • This is several commits touching different areas; happy to split into separate PRs (intercept / daemon lifecycle / console capture) if you prefer.
  • npm run lint currently fails on main in my environment (eslint 10 + @babel/eslint-parser 7 vs Babel 8 peer conflict; npm ci needs --legacy-peer-deps). Unrelated to this PR, so I left it alone.

Test plan

  • npm run build && npm run typecheck
  • npm test — 32 files / 373 tests
  • Daemon restart with the extension attached reconnects (real Chrome, Linux)

Bzcasper and others added 22 commits September 24, 2026 17:27
…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-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 926c2d22-4f2f-4df7-aae2-42e54679e687


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

noiemany and others added 4 commits September 30, 2026 11:26
…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>
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>
noiemany and others added 3 commits September 30, 2026 11:54
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>
@compnew2006
compnew2006 merged commit b382c95 into compnew2006:main Sep 30, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants