* fix(browser): harden existing-session driver validation, session lifecycle, and code quality
Fix config validation rejecting existing-session profiles that lack
cdpPort/cdpUrl (they use Chrome MCP auto-connect instead). Fix callTool
tearing down the MCP session on tool-level errors (element not found,
script error), which caused expensive npx re-spawns. Skip unnecessary
CDP port allocation for existing-session profiles. Remove redundant
ensureChromeMcpAvailable call in isReachable.
Extract shared ARIA role sets (INTERACTIVE_ROLES, CONTENT_ROLES,
STRUCTURAL_ROLES) into snapshot-roles.ts so both the Playwright and
Chrome MCP snapshot paths stay in sync. Add usesChromeMcp capability
flag and replace ~20 scattered driver === "existing-session" string
checks with the centralized flag.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(browser): harden existing-session driver validation and session lifecycle (#45682) (thanks @odysseus0)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(browser): add batch actions, CSS selector support, and click delayMs
Adds three improvements to the browser act tool:
1. CSS selector support: All element-targeting actions (click, type,
hover, drag, scrollIntoView, select) now accept an optional
'selector' parameter alongside 'ref'. When selector is provided,
Playwright's page.locator() is used directly, skipping the need
for a snapshot to obtain refs. This reduces roundtrips for agents
that already know the DOM structure.
2. Click delay (delayMs): The click action now accepts an optional
'delayMs' parameter. When set, the element is hovered first, then
after the specified delay, clicked. This enables human-like
hover-before-click in a single tool call instead of three
(hover + wait + click).
3. Batch actions: New 'batch' action kind that accepts an array of
actions to execute sequentially in a single tool call. Supports
'stopOnError' (default true) to control whether execution halts
on first failure. Results are returned as an array. This eliminates
the AI inference roundtrip between each action, dramatically
reducing latency and token cost for multi-step flows.
Addresses: #44431, #38844
* fix(browser): address security review — batch evaluateEnabled guard, input validation, recursion limit
Fixes all 4 issues raised by Greptile review:
1. Security: batch actions now respect evaluateEnabled flag.
executeSingleAction and batchViaPlaywright accept evaluateEnabled
param. evaluate and wait-with-fn inside batches are rejected
when evaluateEnabled=false, matching the direct route guards.
2. Security: batch input validation. Each action in body.actions
is validated as a plain object with a known kind string before
dispatch. Applies same normalization as direct action handlers.
3. Perf: SELECTOR_ALLOWED_KINDS moved to module scope as a
ReadonlySet<string> constant (was re-created on every request).
4. Security: max batch nesting depth of 5. Nested batch actions
track depth and throw if MAX_BATCH_DEPTH exceeded, preventing
call stack exhaustion from crafted payloads.
* fix(browser): normalize batch act dispatch
* fix(browser): tighten existing-session act typing
* fix(browser): preserve batch type text
* fix(browser): complete batch action execution
* test(browser): cover batch route normalization
* test(browser): cover batch interaction dispatch
* fix(browser): bound batch route action inputs
* fix(browser): harden batch interaction limits
* test(browser): cover batch security guardrails
---------
Co-authored-by: Diwakar <diwakarrankawat@gmail.com>
* fix(browser): resolve correct targetId in navigate response after renderer swap
When `navigateViaPlaywright` triggers a Chrome renderer-process swap
(e.g. navigating from chrome-extension:// to https://), the old
`tab.targetId` captured before navigation becomes stale. The `/navigate`
route previously returned this stale targetId in its response.
After navigation, re-resolve the current tab by matching against the
final URL via `profileCtx.listTabs()`. If the old target is already gone
but the new one is not yet visible (extension re-attach in progress),
retry after 800ms.
Follow-up to #19744 (67bac62c2) which fixed the extension-side stale
session cleanup.
* fix(browser): prefer non-stale targetId when multiple tabs share the same URL
When multiple tabs have the same URL after navigation, find() could pick
a pre-existing tab instead of the newly created one. Now only re-resolve
when the old target is gone (renderer swap detected), and prefer the tab
whose targetId differs from the old one.
* fix(browser): encapsulate targetId resolution logic after navigation
Introduced a new function `resolveTargetIdAfterNavigate` to handle the resolution of the correct targetId after a navigation event that may trigger a renderer swap. This refactor improves code clarity and reuses the logic for determining the current targetId, ensuring that the correct tab is identified even when multiple tabs share the same URL.
* refactor(tests): simplify listTabs initialization in agent snapshot tests
Updated the initialization of listTabs in the agent snapshot tests for better readability by removing unnecessary line breaks. This change enhances code clarity without altering the test logic.
* fix(ui): widen Set type to accept string tokens in external-link helper
* chore: retrigger CI (unrelated Windows flaky test)
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Default missing fill field type to 'text' in /act route to avoid spurious 'fields are required' failures from relay/tool callers. Add regression test for fill payloads with ref+value only.
* Browser/Security: constrain trace and download output paths to temp roots
* Changelog: remove advisory ID from pre-public security note
* Browser/Security: constrain trace and download output paths to temp roots
* Changelog: remove advisory ID from pre-public security note
* test(bluebubbles): align timeout status expectation to 408
* test(discord): remove unused race-condition counter in threading test
* test(bluebubbles): align timeout status expectation to 408
* fix(browser): prevent permanent timeout after stuck evaluate
Thread AbortSignal from client-fetch through dispatcher to Playwright
operations. When a timeout fires, force-disconnect the Playwright CDP
connection to unblock the serialized command queue, allowing the next
call to reconnect transparently.
Key changes:
- client-fetch.ts: proper AbortController with signal propagation
- pw-session.ts: new forceDisconnectPlaywrightForTarget()
- pw-tools-core.interactions.ts: accept signal, align inner timeout
to outer-500ms, inject in-browser Promise.race for async evaluates
- routes/dispatcher.ts + types.ts: propagate signal through dispatch
- server.ts + bridge-server.ts: Express middleware creates AbortSignal
from request lifecycle
- client-actions-core.ts: add timeoutMs to evaluate type
Fixes#10994
* fix(browser): v2 - force-disconnect via Connection.close() instead of browser.close()
When page.evaluate() is stuck on a hung CDP transport, browser.close() also
hangs because it tries to send a close command through the same stuck pipe.
v2 fix: forceDisconnectPlaywrightForTarget now directly calls Playwright's
internal Connection.close() which locally rejects all pending callbacks and
emits 'disconnected' without touching the network. This instantly unblocks
all stuck Playwright operations.
closePlaywrightBrowserConnection (clean shutdown) now also has a 3s timeout
fallback that drops to forceDropConnection if browser.close() hangs.
Fixes permanent browser timeout after stuck evaluate.
* fix(browser): v3 - fire-and-forget browser.close() instead of Connection.close()
v2's forceDropConnection called browser._connection.close() which corrupts
the entire Playwright instance because Connection is shared across all
objects (BrowserType, Browser, Page, etc.). This prevented reconnection
with cascading 'connectOverCDP: Force-disconnected' errors.
v3 fix: forceDisconnectPlaywrightForTarget now:
1. Nulls cached connection immediately
2. Fire-and-forgets browser.close() (doesn't await — it may hang)
3. Next connectBrowser() creates a fresh connectOverCDP WebSocket
Each connectOverCDP creates an independent WebSocket to the CDP endpoint,
so the new connection is unaffected by the old one's pending close.
The old browser.close() eventually resolves when the in-browser evaluate
timeout fires, or the old connection gets GC'd.
* fix(browser): v4 - clear connecting state and remove stale disconnect listeners
The reconnect was failing because:
1. forceDisconnectPlaywrightForTarget nulled cached but not connecting,
so subsequent calls could await a stale promise
2. The old browser's 'disconnected' event handler raced with new
connections, nulling the fresh cached reference
Fix: null both cached and connecting, and removeAllListeners on the
old browser before fire-and-forget close.
* fix(browser): v5 - use raw CDP Runtime.terminateExecution to kill stuck evaluate
When forceDisconnectPlaywrightForTarget fires, open a raw WebSocket
to the stuck page's CDP endpoint and send Runtime.terminateExecution.
This kills running JS without navigating away or crashing the page.
Also clear connecting state and remove stale disconnect listeners.
* fix(browser): abort cancels stuck evaluate
* Browser: always cleanup evaluate abort listener
* Chore: remove Playwright debug scripts
* Docs: add CDP evaluate refactor plan
* Browser: refactor Playwright force-disconnect
* Browser: abort stops evaluate promptly
* Node host: extract withTimeout helper
* Browser: remove disconnected listener safely
* Changelog: note act:evaluate hang fix
---------
Co-authored-by: Bob <bob@dutifulbob.com>
* refactor: consolidate duplicate utility functions
- Add escapeRegExp to src/utils.ts and remove 10 local duplicates
- Rename bash-tools clampNumber to clampWithDefault (different signature)
- Centralize formatError calls to use formatErrorMessage from infra/errors.ts
- Re-export formatErrorMessage from cli/cli-utils.ts to preserve API
* refactor: consolidate remaining escapeRegExp duplicates
* refactor: consolidate sleep, stripAnsi, and clamp duplicates