Skip to content

Keep the extension backend quiet: isolated framework state, no Runtime/Debugger, no title marker - #133

Merged
broven merged 4 commits into
mainfrom
feat/extension-quiet-attach
Oct 8, 2026
Merged

broven merged 4 commits into
mainfrom
feat/extension-quiet-attach

Conversation

@broven

@broven broven commented Oct 7, 2026

Copy link
Copy Markdown
Owner

Summary

Reduce what a website can observe when browserwright drives a tab on the extension backend (chrome.debugger). This is the low-cost subset of a red-team/blue-team exercise. The more invasive parts are deliberately left out (see below).

  1. Framework state lives in an isolated world. Browserwright's page-side helpers and state run in an isolated world (repl/isolated_world.py) instead of the page main world. Page↔tab identity matching reads the frame id over a separate CDP session instead of writing a marker into the page.
  2. The 👀 title marker is removed. The extension no longer rewrites document.title, which pages can observe. Attached tabs are marked with a per-tab action badge ("AI", Chrome UI only) and tab groups instead.
  3. Runtime stays quiet. The extension facade does not keep Runtime.enable on. Console messages arrive as plain text via the Console domain. Set BW_CAPTURE_CONSOLE=1 on the daemon to get full Runtime capture (object previews, exceptions). See docs/runtime-console.md.
  4. No Debugger domain. Execution contexts are discovered through DOM handles (daemon/server/extension_contexts.py). A page's debugger; statements never pause it.
  5. Screenshots keep the caret. Executor screenshots default to caret="initial", so Playwright no longer injects a caret-hiding style. An explicit caret= still wins.
  6. Fix (9fb1597). Identity matching also requires the Page's own session to answer a constant evaluation. Without this, an executor rebind after a service-worker replacement could accept a stale Page and hang on goto. This was caught by test_extension_sw_loss_orphans_resident_executor.

Behaviour changes for users

  • No 👀 prefix in tab titles any more. Look for the tab group or the "AI" toolbar badge instead.
  • Console object previews and exception events are off by default. Console text still arrives.

Deliberately not included

  • Simulated human input: pointer paths, key timing, wheel scrolling, pixel snapping.
  • Any tab or window activation (bringToFront / focusing the window). It steals macOS focus.
  • Cooperative or idle-scheduled inspection, snapshot visibility changes, and Node-side Readability.

Test plan

  • mise run lint
  • mise run test: skill 338 passed; daemon 982 passed, 97 skipped; evals 13/13; pi passed.
  • Real-Chrome extension E2E, 24 files: 55 passed. Includes the new test_runtime_console_cost.py, test_debugger_pause_extension.py, test_screenshot_caret.py, plus read_markdown, page binding, reload orphan, multisession and recovery suites.
  • git grep confirms none of bring_to_front, bringToFront, windows.update, human_input remain.

Follow-ups, not in this PR:

  • task_runner._bind_session_to_page still writes a temporary main-world marker.
  • Extension manifest version is not bumped.

🤖 Generated with Claude Code

broven and others added 4 commits October 7, 2026 21:08
Framework views (read_markdown normalization, smart-goto settle probes)
now evaluate in a dedicated CDP isolated world via
repl/isolated_world.py instead of page.evaluate, so page scripts can
neither observe nor intercept them. Agent-authored page.evaluate keeps
Playwright's main-world semantics.

Exact Page <-> target matching (bind_current_page, page_for_target,
switch_tab(page)) reads the top-frame CDP identity over an auxiliary
CDP session instead of writing a temporary global marker into the
page. The extension facade now serves an explicit attachToTarget on an
already-announced tab as an independent auxiliary session, so
context.new_cdp_session(page) and its detach no longer collide with
Playwright's primary session.

The extension no longer rewrites document.title with the attach marker
(and no longer injects title-stripping code into utility worlds). The
attachment indicator moves to extension UI (per-tab action badge);
tab groups remain the visible "agent is driving this tab" signal.
cleanupMarkersBeforeReload is renamed to
cleanupAttachmentIndicatorsBeforeReload and only clears badges.

Co-Authored-By: Claude <noreply@anthropic.com>
…ugger

The extension facade no longer keeps Runtime.enable on for Playwright's
page sessions. Runtime event subscription makes Chrome build console
object previews synchronously (large console.table calls slow the page
even when nobody reads them), and it is observable page-side.

Instead, Runtime.enable on a page session:
- discovers real default execution contexts through native Document
  handles (DOM.getDocument / getFrameOwner / describeNode / resolveNode,
  new daemon/server/extension_contexts.py) and publishes synthetic
  executionContextCreated/Destroyed events; Debugger stays disabled, so
  `debugger` statements never pause or slow the page;
- re-creates named utility worlds per frame on navigation;
- enables the legacy Console domain and translates console-api
  messages into text-only Runtime.consoleAPICalled events, buffering
  startup text until the new realm is published.

Full Runtime capture (object previews, exception events) is opt-in with
BW_CAPTURE_CONSOLE=1 in the daemon environment; an explicit CDP
session's Runtime.enable still subscribes natively. See
docs/runtime-console.md.

The e2e harness gains an extension_only_chrome fixture (no remote
debugging port) used by the new console-cost and debugger-pause
real-Chrome tests.

Co-Authored-By: Claude <noreply@anthropic.com>
Playwright's screenshot() defaults to caret="hide", which writes inline
caret-color styles on every editable element (shadow roots and child
frames included) and restores them afterwards; page code can observe
those shared-DOM mutations. The executor's live surface now installs
repl/screenshot_defaults.py, which makes Page/Locator/ElementHandle
screenshot() default to caret="initial" for executor contexts. All
other options keep native behavior, and an explicit caret (including
caret="hide") is passed through unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Identity matching reads the top-frame id over a fresh auxiliary CDP
session, which answers even when the Page's own primary session has
gone stale. After an extension service-worker replacement the tab is
re-attached without Playwright's domain subscriptions, so the executor's
issue #86 in-place rebind accepted the stale Page and the next
page.goto hung at commit (test_extension_sw_loss_orphans_resident_executor).

The former main-world marker probe rejected such Pages implicitly. Now
the single identity match must also answer a constant evaluation on its
own session; otherwise there is no usable Page and the existing
recycle-and-recover path runs, as on main. The probe reads and writes
no page state.

Co-Authored-By: Claude <noreply@anthropic.com>
@broven
broven merged commit 7b12ff3 into main Oct 8, 2026
1 check passed
@broven
broven deleted the feat/extension-quiet-attach branch October 8, 2026 08:04
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.

1 participant