feat(browser): restore driver "extension" via a loopback Chrome extension relay (no remote-debugging prompt)#100619
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5348cfe902
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| Browser: identity?.browserVersion ?? "Chrome/unknown", | ||
| "Protocol-Version": "1.3", | ||
| "User-Agent": identity?.userAgent ?? "unknown", | ||
| webSocketDebuggerUrl: `ws://127.0.0.1:${resolvedPort()}/cdp`, |
There was a problem hiding this comment.
Return an authenticated CDP WebSocket URL
For extension profiles with gateway auth, /json/version is fetched through profile.cdpUrl with Basic auth, but this returned webSocketDebuggerUrl drops the token. The same relay rejects /cdp upgrades unless isAuthorized passes, and the existing CDP helpers/Playwright path use the returned WS URL for the next connection, so a paired extension can make HTTP discovery succeed while every CDP handshake is rejected as unauthorized.
Useful? React with 👍 / 👎.
| const resolved = resolveBrowserConfig(cfg.browser, cfg); | ||
| for (const [name, profile] of Object.entries(resolved.profiles)) { | ||
| if (profile.driver === "extension") { | ||
| return { name, relayPort: profile.cdpPort ?? resolved.extensionRelayDefaultPort }; |
There was a problem hiding this comment.
Use per-profile relay ports when pairing
When a user configures another driver: "extension" profile alongside the built-in chrome profile, config resolution assigns distinct ports via resolved.extensionRelayPorts, but the pairing CLI ignores that map and falls back to extensionRelayDefaultPort for any unpinned profile. In a config like profiles.work.driver = "extension", work is the first extension profile but may actually be assigned defaultPort - 1, so openclaw browser extension pair prints the wrong relay endpoint and pairs the extension to a different profile or a non-matching port.
Useful? React with 👍 / 👎.
|
Codex review: needs real behavior proof before merge. Reviewed July 6, 2026, 3:51 AM ET / 07:51 UTC. Summary PR surface: Source +2439, Tests +517, Docs +139, Config +3. Total +3098 across 47 files. Reproducibility: yes. at source level for the review blockers: the relay returns an unauthenticated discovered WebSocket URL, pairing ignores resolved extension-profile ports, and synthetic page targets omit Playwright's required Review metrics: 2 noteworthy metrics.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Maintainer decision needed
Security Review findings
Review detailsBest possible solution: Keep the PR open, fix the relay correctness and hardening blockers, add redacted live Chrome extension proof, and have maintainers decide whether this bundled driver should land now while cross-machine relay work stays on the linked strategy track. Do we have a high-confidence way to reproduce the issue? Yes, at source level for the review blockers: the relay returns an unauthenticated discovered WebSocket URL, pairing ignores resolved extension-profile ports, and synthetic page targets omit Playwright's required Is this the best way to solve the issue? No, not yet. The direction is plausible, but this is not the best merge shape until correctness, hardening, live proof, and the first-party browser-control boundary are resolved. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against be48a430bfa6. Label changesLabel justifications:
Evidence reviewedPR surface: Source +2439, Tests +517, Docs +139, Config +3. Total +3098 across 47 files. View PR surface stats
Security concerns:
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
Review history (1 earlier review cycle)
|
5348cfe to
0a59513
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0a59513ce1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return { | ||
| targetId, | ||
| type: "page", | ||
| title: tab.info.title, | ||
| url: tab.info.url, |
There was a problem hiding this comment.
Include browserContextId in synthetic page targets
When a shared tab is auto-attached, this targetInfo is sent in Target.attachedToTarget. The repo pins [email protected], whose CRBrowser._onAttachedToTarget asserts targetInfo.browserContextId for every non-browser target before falling back to the default context, so the extension relay throws as soon as Playwright sees a shared or newly-created tab instead of listing/controlling it. Add a stable non-empty browserContextId to the synthetic page target info so the CDP contract matches Playwright's connectOverCDP expectations.
Useful? React with 👍 / 👎.
0a59513 to
2544feb
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2544febaa6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const existing = this.tabs.get(info.tabId); | ||
| if (existing) { | ||
| existing.info = info; | ||
| } else { |
There was a problem hiding this comment.
Reattach existing tabs after extension reconnect
When the extension socket reconnects while a CDP client is still connected, handleExtensionGone() clears each tab's attached state but intentionally keeps the tab entries. The next hello/tabs sync for the same shared tab takes this existing branch and only updates metadata, so auto-attach clients never get a new Target.attachedToTarget for the tab they just saw detached; the active Playwright connection can remain alive with no pages until the tab is removed/re-shared or the client reconnects. Re-run the auto-attach path when an existing tab is currently unattached and any client has autoAttach enabled.
Useful? React with 👍 / 👎.
| case "unpair": { | ||
| await chrome.storage.local.remove(["relayUrl", "token"]); | ||
| relayWs?.close(); | ||
| relayWs = null; | ||
| setBadge("off"); | ||
| sendResponse({ ok: true }); |
There was a problem hiding this comment.
Detach debugger sessions when unpairing
If the user clicks Unpair while any shared tab is attached, this only removes the relay credentials and closes the WebSocket; it never calls detachDebugger() for the attachedTabs set or removes the tabs from the OpenClaw group. Since the debugger attachment is independent of the relay socket, Chrome can continue showing the debugging infobar and the extension keeps those tabs attached after the user has explicitly revoked pairing. Detach all attached tabs (and ideally clear/revoke the shared group) before returning success.
Useful? React with 👍 / 👎.
2544feb to
99f70b2
Compare
Update: remote-node support + live end-to-end proof + dedupeRemote browser nodes now supported. The relay auth token is now a Live end-to-end (real Chrome). Loaded the built unpacked extension into a
This complements the unit tests, which already verify the relay bridge's CDP Dedupe. #53599 is the canonical tracking issue (referenced); #55840 (closed |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 99f70b22cd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| async function findOpenClawGroups() { | ||
| try { | ||
| return await chrome.tabGroups.query({ title: OPENCLAW_TAB_GROUP_TITLE }); |
There was a problem hiding this comment.
Track extension-created groups before sharing tabs
When a user already has, or manually renames, a Chrome tab group to OpenClaw, this title-only lookup treats every tab in that group as shared immediately on hello/tabs sync, before the user clicks the extension's share button or drags tabs into a group created by the extension. Because listSharedTabs() sends all groups returned here to the relay, pairing the extension can expose unrelated tabs to agents; persist and check group ids created by the extension, or use another non-colliding marker, instead of trusting the display title alone.
Useful? React with 👍 / 👎.
…on relay Reintroduces browser profile driver "extension" (removed in 2026.3.22) as a loopback relay that drives the user's signed-in Chrome through an MV3 extension instead of the remote-debugging port. This avoids Chrome's blocking "Allow remote debugging?" prompt, which cannot be clicked when the operator drives OpenClaw from a phone. Automated tabs live in an "OpenClaw" tab group (the consent boundary), mirroring the Codex/Claude-in-Chrome model. - relay bridge synthesizes the CDP browser target surface for Playwright connectOverCDP and forwards session-scoped commands to chrome.debugger - relay server binds loopback only; both sides authenticate with a token derived (HMAC-SHA256) from gateway auth, so the raw credential never reaches Chrome; extension origin + loopback Host checks guard the upgrade - built-in "chrome" profile; distinct relay ports per extension profile; relay reconciles on auth rotation / cdpPort change and prunes removed profiles - doctor + status surface the extension transport; doctor keeps repairing the retired relay endpoint URL on legacy "extension" profiles Refs #53599
Thin MV3 extension (chrome-extension/): a WebSocket client to the loopback relay plus chrome.debugger forwarding and OpenClaw tab-group management. All CDP target synthesis lives server-side in the relay bridge, so the extension stays a dumb transport (the removed 2026.3 extension put that logic in a 1000-line untestable service worker). Popup handles pairing and per-tab share toggle; `openclaw browser extension path|pair` load and pair it. A build copy hook stages it into dist so the load path is stable. Refs #53599
Adds docs/tools/chrome-extension for the restored extension driver (install, pair, tab-group consent model, security posture) and wires it into the browser docs profile section and nav. Refs #53599
Derives the relay auth token from a host-local secret in the credentials dir (created on first use) instead of gateway auth. Each machine that runs a browser — the gateway host and every browser node host — owns its own token, so the extension pairs with whichever machine hosts its Chrome and no gateway credential travels to a node. The node host already runs the shared browser control bootstrap, so this is all that was missing for cross-machine control. Also removes the "relay needs gateway auth before it can start" failure mode: startup and `openclaw browser extension pair` ensure the secret exists. Refs #53599
- Make the host-local relay secret creation atomic (O_CREAT|O_EXCL + adopt the winner on EEXIST) so the gateway service and `extension pair` CLI cannot mint divergent tokens on a fresh host (would 401 until restart); credentials dir created mode 0700. (adversarial review finding) - Resolve type-aware oxlint findings across the relay + extension: unknown catch vars, addEventListener over ws.on* in the MV3 worker, void async listeners, drop useless returns/spreads, Object.assign over map-spread, safe ws frame decode (Buffer[]/ArrayBuffer), toSorted. - Add extensionRelayDefaultPort/extensionRelayPorts to remaining test config literals; type the extension relay-core module (.d.ts, excluded from dist); regenerate docs_map.
- Hash both operands before timingSafeEqual so token comparison has no length short-circuit (GHSA-jj6q-rrrf-h66h). - Bound the relay WebSocketServer with maxPayload (64 MiB, headroom for CDP screenshots/bodies) against oversized frames (GHSA-vw3h-q6xq-jjm5). - Rewrite the config test env helper to avoid the skill-env-host-injection shape (GHSA-82g8-464f-2mv7); it is a test-only env swap.
99f70b2 to
a98e5b5
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a98e5b58a0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
| if (req.method === "GET" && (path === "/json" || path === "/json/list")) { | ||
| res.writeHead(200, { "Content-Type": "application/json" }); | ||
| res.end(JSON.stringify(bridge.sharedTabs())); |
There was a problem hiding this comment.
Return CDP-shaped targets from /json/list
When target-id probing falls back to /json/list (for example after Target.getTargetInfo fails on the extension CDP path), existing fallback code expects Chrome-style entries with id/url (extensions/browser/src/browser/pw-session.ts:1203-1210, and JSON tab consumers read t.id). This endpoint currently serializes RelayTabInfo (tabId, url, title, active), so multiple shared tabs cannot be resolved or listed through the relay in that fallback path. Return Chrome-compatible target objects with a stable id (and type/webSocketDebuggerUrl where applicable) instead of the extension wire shape.
Useful? React with 👍 / 👎.
| pending.reject(new Error("extension relay stopped")); | ||
| } | ||
| this.pendingExtension.clear(); | ||
| this.extension?.socket.close(1001, "relay stopped"); |
There was a problem hiding this comment.
Detach tabs before stopping the relay
If OpenClaw stops or rebinds the relay while a CDP client has attached tabs, stopExtensionRelays() reaches this dispose() path, which closes the extension socket and clears bridge state without sending any detach commands. The extension's WebSocket close handler only schedules reconnect and keeps its attachedTabs, so Chrome can continue showing the debugger infobar and the tabs remain debugger-attached after OpenClaw has stopped. Detach all attached tabs before closing the extension socket, or make the extension detach on relay shutdown/close.
Useful? React with 👍 / 👎.
|
Merged via squash.
|
…sion relay (no remote-debugging prompt) (openclaw#100619) * feat(browser): restore driver "extension" via loopback Chrome extension relay Reintroduces browser profile driver "extension" (removed in 2026.3.22) as a loopback relay that drives the user's signed-in Chrome through an MV3 extension instead of the remote-debugging port. This avoids Chrome's blocking "Allow remote debugging?" prompt, which cannot be clicked when the operator drives OpenClaw from a phone. Automated tabs live in an "OpenClaw" tab group (the consent boundary), mirroring the Codex/Claude-in-Chrome model. - relay bridge synthesizes the CDP browser target surface for Playwright connectOverCDP and forwards session-scoped commands to chrome.debugger - relay server binds loopback only; both sides authenticate with a token derived (HMAC-SHA256) from gateway auth, so the raw credential never reaches Chrome; extension origin + loopback Host checks guard the upgrade - built-in "chrome" profile; distinct relay ports per extension profile; relay reconciles on auth rotation / cdpPort change and prunes removed profiles - doctor + status surface the extension transport; doctor keeps repairing the retired relay endpoint URL on legacy "extension" profiles Refs openclaw#53599 * feat(browser): bundle the OpenClaw MV3 Chrome extension Thin MV3 extension (chrome-extension/): a WebSocket client to the loopback relay plus chrome.debugger forwarding and OpenClaw tab-group management. All CDP target synthesis lives server-side in the relay bridge, so the extension stays a dumb transport (the removed 2026.3 extension put that logic in a 1000-line untestable service worker). Popup handles pairing and per-tab share toggle; `openclaw browser extension path|pair` load and pair it. A build copy hook stages it into dist so the load path is stable. Refs openclaw#53599 * docs(browser): document the Chrome extension profile Adds docs/tools/chrome-extension for the restored extension driver (install, pair, tab-group consent model, security posture) and wires it into the browser docs profile section and nav. Refs openclaw#53599 * feat(browser): make the extension relay work on remote browser nodes Derives the relay auth token from a host-local secret in the credentials dir (created on first use) instead of gateway auth. Each machine that runs a browser — the gateway host and every browser node host — owns its own token, so the extension pairs with whichever machine hosts its Chrome and no gateway credential travels to a node. The node host already runs the shared browser control bootstrap, so this is all that was missing for cross-machine control. Also removes the "relay needs gateway auth before it can start" failure mode: startup and `openclaw browser extension pair` ensure the secret exists. Refs openclaw#53599 * fix(browser): harden relay secret creation and satisfy CI lint/typecheck - Make the host-local relay secret creation atomic (O_CREAT|O_EXCL + adopt the winner on EEXIST) so the gateway service and `extension pair` CLI cannot mint divergent tokens on a fresh host (would 401 until restart); credentials dir created mode 0700. (adversarial review finding) - Resolve type-aware oxlint findings across the relay + extension: unknown catch vars, addEventListener over ws.on* in the MV3 worker, void async listeners, drop useless returns/spreads, Object.assign over map-spread, safe ws frame decode (Buffer[]/ArrayBuffer), toSorted. - Add extensionRelayDefaultPort/extensionRelayPorts to remaining test config literals; type the extension relay-core module (.d.ts, excluded from dist); regenerate docs_map. * fix(browser): satisfy OpenGrep security policy on the relay - Hash both operands before timingSafeEqual so token comparison has no length short-circuit (GHSA-jj6q-rrrf-h66h). - Bound the relay WebSocketServer with maxPayload (64 MiB, headroom for CDP screenshots/bodies) against oversized frames (GHSA-vw3h-q6xq-jjm5). - Rewrite the config test env helper to avoid the skill-env-host-injection shape (GHSA-82g8-464f-2mv7); it is a test-only env swap.
…sion relay (no remote-debugging prompt) (openclaw#100619) * feat(browser): restore driver "extension" via loopback Chrome extension relay Reintroduces browser profile driver "extension" (removed in 2026.3.22) as a loopback relay that drives the user's signed-in Chrome through an MV3 extension instead of the remote-debugging port. This avoids Chrome's blocking "Allow remote debugging?" prompt, which cannot be clicked when the operator drives OpenClaw from a phone. Automated tabs live in an "OpenClaw" tab group (the consent boundary), mirroring the Codex/Claude-in-Chrome model. - relay bridge synthesizes the CDP browser target surface for Playwright connectOverCDP and forwards session-scoped commands to chrome.debugger - relay server binds loopback only; both sides authenticate with a token derived (HMAC-SHA256) from gateway auth, so the raw credential never reaches Chrome; extension origin + loopback Host checks guard the upgrade - built-in "chrome" profile; distinct relay ports per extension profile; relay reconciles on auth rotation / cdpPort change and prunes removed profiles - doctor + status surface the extension transport; doctor keeps repairing the retired relay endpoint URL on legacy "extension" profiles Refs openclaw#53599 * feat(browser): bundle the OpenClaw MV3 Chrome extension Thin MV3 extension (chrome-extension/): a WebSocket client to the loopback relay plus chrome.debugger forwarding and OpenClaw tab-group management. All CDP target synthesis lives server-side in the relay bridge, so the extension stays a dumb transport (the removed 2026.3 extension put that logic in a 1000-line untestable service worker). Popup handles pairing and per-tab share toggle; `openclaw browser extension path|pair` load and pair it. A build copy hook stages it into dist so the load path is stable. Refs openclaw#53599 * docs(browser): document the Chrome extension profile Adds docs/tools/chrome-extension for the restored extension driver (install, pair, tab-group consent model, security posture) and wires it into the browser docs profile section and nav. Refs openclaw#53599 * feat(browser): make the extension relay work on remote browser nodes Derives the relay auth token from a host-local secret in the credentials dir (created on first use) instead of gateway auth. Each machine that runs a browser — the gateway host and every browser node host — owns its own token, so the extension pairs with whichever machine hosts its Chrome and no gateway credential travels to a node. The node host already runs the shared browser control bootstrap, so this is all that was missing for cross-machine control. Also removes the "relay needs gateway auth before it can start" failure mode: startup and `openclaw browser extension pair` ensure the secret exists. Refs openclaw#53599 * fix(browser): harden relay secret creation and satisfy CI lint/typecheck - Make the host-local relay secret creation atomic (O_CREAT|O_EXCL + adopt the winner on EEXIST) so the gateway service and `extension pair` CLI cannot mint divergent tokens on a fresh host (would 401 until restart); credentials dir created mode 0700. (adversarial review finding) - Resolve type-aware oxlint findings across the relay + extension: unknown catch vars, addEventListener over ws.on* in the MV3 worker, void async listeners, drop useless returns/spreads, Object.assign over map-spread, safe ws frame decode (Buffer[]/ArrayBuffer), toSorted. - Add extensionRelayDefaultPort/extensionRelayPorts to remaining test config literals; type the extension relay-core module (.d.ts, excluded from dist); regenerate docs_map. * fix(browser): satisfy OpenGrep security policy on the relay - Hash both operands before timingSafeEqual so token comparison has no length short-circuit (GHSA-jj6q-rrrf-h66h). - Bound the relay WebSocketServer with maxPayload (64 MiB, headroom for CDP screenshots/bodies) against oversized frames (GHSA-vw3h-q6xq-jjm5). - Rewrite the config test env helper to avoid the skill-env-host-injection shape (GHSA-82g8-464f-2mv7); it is a test-only env swap.
What Problem This Solves
Driving your signed-in Chrome with OpenClaw today means the
userprofile(
driver: "existing-session"), which attaches over Chrome's remote-debuggingport. Chrome then shows a blocking modal — "Allow remote debugging?" — that
must be clicked on the desktop. When you drive OpenClaw from a phone
(Telegram/WhatsApp) with nobody at the computer, automation stalls on a dialog
nobody can dismiss.
The old
driver: "extension"relay that handled this was removed in 2026.3.22(#47893), with no replacement for signed-in browsers away from the keyboard
(regression tracked in #53599).
Why This Change Was Made
I compared our integration with how Codex and Claude solve the same thing
(Codex source inspected directly in the sibling
openai/codexcheckout; Claudevia public docs + teardowns):
chrome.debuggervia a Web Store extensionBoth Codex and Claude use the same shape: an MV3 extension that drives tabs with
chrome.debugger(no remote-debugging port → no modal) and scopes automatedtabs into a colored tab group. This PR brings that shape to OpenClaw and
restores
driver: "extension".User Impact
New built-in
chromebrowser profile (driver: "extension"):chrome.debugger,so it works when you are away from the computer.
"OpenClaw" tab group. Drag a tab in to share, out to revoke; the agent's own
tabs land there automatically. Same model as Codex/Claude.
openclaw browser extension path(load unpacked) →openclaw browser extension pair(paste the pairing string into the popup).with a token derived (HMAC-SHA256) from gateway auth, so the raw gateway
credential never reaches Chrome; the extension upgrade is origin- and
loopback-Host-checked. Compared with the
userprofile (whole signed-inbrowser exposed once you approve the prompt), the shared surface stays scoped
to a tab group you control at a glance.
Existing
openclaw,user, and remote-CDP profiles are unchanged. Doctor keepsmigrating legacy config; the old relay-era
cdpUrlonextensionprofiles isstripped (the relay owns its endpoint now).
Scope note: this restores the same-host case (gateway + Chrome on one
machine, driven remotely via a channel) with the modern tab-group UX. The relay
is loopback-only; exposing it for the fully cross-machine topology in #53599
(gateway on a VPS, browser on a laptop) is a follow-up — it should ride the
existing gateway node-host/tunnel path rather than opening a new network port.
Architecture
extensions/browser/src/browser/extension-relay/— the relay is TypeScript andtestable.
relay-bridge.tsowns all CDP target synthesis (emulates thebrowser target for Playwright's
connectOverCDP, maps session-scoped commandsand child/iframe sessions to
chrome.debugger). The removed extension put thislogic in a 1000-line untestable MV3 service worker — that is why it rotted.
extensions/browser/chrome-extension/— a thin MV3 extension: WebSocket clientchrome.debuggerforwarding + tab-group management, nothing more. Pure logic(pairing parse, backoff, color mapping) is split into a unit-tested module.
chromeprofile; distinct relay ports per extension profile; relayreconciles on auth rotation /
cdpPortchange and prunes removed profiles.Evidence
Validated on Blacksmith Testbox (Linux, Node 24):
pnpm test extensions/browser— 1432 passed (14 new relay/extension tests:bridge CDP synthesis, session routing, child-session reap, auth derivation,
pairing round-trip, port assignment).
pnpm tsgo:core,pnpm tsgo:extensions,pnpm tsgo:core:test— all green.oxfmt --check+oxlinton changed files — clean.confirmed findings fixed (child-session map leak, attach-in-flight race on tab
regroup, extension attach idempotency, multi-profile port collision, relay
reconciliation on auth rotation) with regression tests.
Not yet exercised: a live end-to-end load of the unpacked extension against a
real Chrome (no headed Chrome in CI). The relay/bridge contract is covered by
the Playwright-handshake unit tests; a manual smoke on a real desktop is the
recommended pre-merge check.
Refs #53599