Skip to content

feat: Support external CDP WebSocket URL for connecting to existing browsers#396

Open
warmshao wants to merge 2 commits intobrowser-use:mainfrom
warmshao:feat/external-cdp-wss
Open

feat: Support external CDP WebSocket URL for connecting to existing browsers#396
warmshao wants to merge 2 commits intobrowser-use:mainfrom
warmshao:feat/external-cdp-wss

Conversation

@warmshao
Copy link
Copy Markdown
Contributor

@warmshao warmshao commented May 5, 2026

This commit adds the ability to connect to an already-running local Chrome/Chromium instance via its CDP WebSocket endpoint, instead of spawning a new embedded WebContentsView for every session.

Motivation

When driving a browser that the user already has open (e.g. with extensions, cookies, or specific profiles loaded), it is more convenient to attach to that existing instance rather than launching a fresh one. This is especially useful for workflows that require a logged-in state or specific browser configuration.

How to use

  1. Open Chrome/Chromium and navigate to chrome://inspect/#remote-debugging.
  2. Enable "Discover network targets" or start Chrome with --remote-debugging-port=9222.
  3. Copy the browser-level WebSocket URL (e.g. ws://127.0.0.1:9222).
  4. Open Settings → Browser in the app.
  5. Paste the URL into the CDP WebSocket URL field.
  6. (Optional) Check Always allow to keep a single global daemon alive across all sessions, avoiding Chrome's "Allow remote debugging?" dialog on every follow-up.

How it works

  • A long-running daemon.js process holds a single CDP WebSocket connection.
  • Agent scripts talk to the daemon over a local named pipe / Unix socket (\.\pipe\browser-use-bh-global on Windows, /tmp/bh-global.sock on Unix).
  • The daemon reuses the same WebSocket for every agent turn, so Chrome only prompts once (or never if already allowed).
  • If the WebSocket drops, the daemon attempts to reconnect automatically.
  • If reconnection fails, the daemon exits and the main process starts a fresh one on the next session.

Files changed

  • Adapter layer (claude-code/adapter.ts, codex/adapter.ts, runEngine.ts, types.ts): Pass cdpUrl and daemonSocket through the engine spawn pipeline.
  • Harness (helpers.js, daemon.js, harness.ts): Add daemon IPC client, WebSocket reconnection logic, and stock-file bootstrap for daemon.js.
  • Main process (index.ts): Manage per-session and global daemon lifecycle, CDP health checks before reusing a daemon, and settings-aware cleanup.
  • Settings UI (SettingsPane.tsx, cdpUrlIpc.ts, cdpUrlStore.ts, SettingsWindow.ts): Add CDP URL input, test connection button, and "Always allow" toggle.
  • Renderer (AgentPane.tsx, useSessionsQuery.ts, types.ts): Surface externalBrowser flag in session lists.
  • Shell/Logs (shell.ts, logsPill.ts): Adjust logs overlay behavior when no embedded view is present.

Notes

  • Chrome 144+ may return 404 on HTTP /json/* endpoints when remote debugging is toggled via the UI. The daemon reads DevToolsActivePort from the Chrome profile as a fallback.
  • On Windows, npm-installed CLI shims (.cmd, .ps1) are handled correctly via cmd.exe /d /s /c or powershell.exe.

Summary

Test plan

  • npm run lint passes
  • npm run typecheck passes
  • npm run test passes (unit + integration)
  • New code has tests (per D1 directive — TDD, ≥80% coverage on new src/main/src/shared modules)
  • Python changes: pytest + ruff check + mypy pass
  • Manual QA in npm run dev for UI-facing changes

Screenshots / recordings

Risk + rollback

…rowsers

This commit adds the ability to connect to an already-running local Chrome/Chromium
instance via its CDP WebSocket endpoint, instead of spawning a new embedded
WebContentsView for every session.

## Motivation

When driving a browser that the user already has open (e.g. with extensions,
cookies, or specific profiles loaded), it is more convenient to attach to that
existing instance rather than launching a fresh one. This is especially useful
for workflows that require a logged-in state or specific browser configuration.

## How to use

1. Open Chrome/Chromium and navigate to `chrome://inspect/#remote-debugging`.
2. Enable "Discover network targets" or start Chrome with `--remote-debugging-port=9222`.
3. Copy the browser-level WebSocket URL (e.g.
   `ws://127.0.0.1:9222/devtools/browser/<id>`).
4. Open **Settings → Browser** in the app.
5. Paste the URL into the **CDP WebSocket URL** field.
6. (Optional) Check **Always allow** to keep a single global daemon alive across
   all sessions, avoiding Chrome's "Allow remote debugging?" dialog on every
   follow-up.

## How it works

- A long-running `daemon.js` process holds a single CDP WebSocket connection.
- Agent scripts talk to the daemon over a local named pipe / Unix socket
  (`\.\pipe\browser-use-bh-global` on Windows, `/tmp/bh-global.sock` on Unix).
- The daemon reuses the same WebSocket for every agent turn, so Chrome only
  prompts once (or never if already allowed).
- If the WebSocket drops, the daemon attempts to reconnect automatically.
- If reconnection fails, the daemon exits and the main process starts a fresh
  one on the next session.

## Files changed

- **Adapter layer** (`claude-code/adapter.ts`, `codex/adapter.ts`, `runEngine.ts`,
  `types.ts`): Pass `cdpUrl` and `daemonSocket` through the engine spawn pipeline.
- **Harness** (`helpers.js`, `daemon.js`, `harness.ts`): Add daemon IPC client,
  WebSocket reconnection logic, and stock-file bootstrap for `daemon.js`.
- **Main process** (`index.ts`): Manage per-session and global daemon lifecycle,
  CDP health checks before reusing a daemon, and settings-aware cleanup.
- **Settings UI** (`SettingsPane.tsx`, `cdpUrlIpc.ts`, `cdpUrlStore.ts`,
  `SettingsWindow.ts`): Add CDP URL input, test connection button, and
  "Always allow" toggle.
- **Renderer** (`AgentPane.tsx`, `useSessionsQuery.ts`, `types.ts`): Surface
  `externalBrowser` flag in session lists.
- **Shell/Logs** (`shell.ts`, `logsPill.ts`): Adjust logs overlay behavior when
  no embedded view is present.

## Notes

- Chrome 144+ may return 404 on HTTP `/json/*` endpoints when remote debugging
  is toggled via the UI. The daemon reads `DevToolsActivePort` from the Chrome
  profile as a fallback.
- On Windows, npm-installed CLI shims (`.cmd`, `.ps1`) are handled correctly
  via `cmd.exe /d /s /c` or `powershell.exe`.
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 17 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/src/main/hl/stock/helpers.js">

<violation number="1" location="app/src/main/hl/stock/helpers.js:214">
P2: `createContext` returns a daemon-backed context even when the daemon never becomes ready, instead of throwing after the retry loop.</violation>
</file>

<file name="app/src/main/settings/cdpUrlStore.ts">

<violation number="1" location="app/src/main/settings/cdpUrlStore.ts:75">
P2: Change notifications fire even when persistence fails, so runtime observers can react to an unsaved CDP setting.</violation>
</file>

<file name="app/src/main/hl/engines/claude-code/adapter.ts">

<violation number="1" location="app/src/main/hl/engines/claude-code/adapter.ts:139">
P2: Connection-mode env vars are added without clearing conflicting inherited vars, so stale BU_DAEMON_SOCKET / BU_CDP_WS / BU_TARGET_ID / BU_CDP_PORT values can force the helper into the wrong browser-connection mode.</violation>

<violation number="2" location="app/src/main/hl/engines/claude-code/adapter.ts:140">
P3: Prompt mentions the wrong env var for external CDP WebSocket mode; it should refer to `BU_CDP_WS`, not `BU_DAEMON_SOCKET`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

await client.send('Runtime.evaluate', { expression: '1' }).catch(() => {});
}
}
return {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: createContext returns a daemon-backed context even when the daemon never becomes ready, instead of throwing after the retry loop.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/hl/stock/helpers.js, line 214:

<comment>`createContext` returns a daemon-backed context even when the daemon never becomes ready, instead of throwing after the retry loop.</comment>

<file context>
@@ -102,19 +120,118 @@ class CdpSession {
+        await client.send('Runtime.evaluate', { expression: '1' }).catch(() => {});
+      }
+    }
+    return {
+      targetId: targetId || 'external',
+      port,
</file context>
Suggested change
return {
if (info.product == null) {
throw new Error(`createContext: daemon socket ${daemonSocket} is unreachable or not ready`);
}
return {
Fix with Cubic

@@ -0,0 +1,94 @@
/**
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Change notifications fire even when persistence fails, so runtime observers can react to an unsaved CDP setting.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/settings/cdpUrlStore.ts, line 75:

<comment>Change notifications fire even when persistence fails, so runtime observers can react to an unsaved CDP setting.</comment>

<file context>
@@ -0,0 +1,94 @@
+  } catch (err) {
+    mainLogger.error('cdpUrl.set-failed', { error: (err as Error).message });
+  }
+  onChange?.(next);
+  return next;
+}
</file context>
Fix with Cubic

'You are driving a specific Chromium browser view on this machine.',
`Your target is CDP target_id=${ctx.targetId} on port ${ctx.cdpPort} (env BU_TARGET_ID / BU_CDP_PORT).`,
];
if (ctx.cdpUrl) {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Connection-mode env vars are added without clearing conflicting inherited vars, so stale BU_DAEMON_SOCKET / BU_CDP_WS / BU_TARGET_ID / BU_CDP_PORT values can force the helper into the wrong browser-connection mode.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/hl/engines/claude-code/adapter.ts, line 139:

<comment>Connection-mode env vars are added without clearing conflicting inherited vars, so stale BU_DAEMON_SOCKET / BU_CDP_WS / BU_TARGET_ID / BU_CDP_PORT values can force the helper into the wrong browser-connection mode.</comment>

<file context>
@@ -135,10 +135,16 @@ const claudeCodeAdapter: EngineAdapter = {
       'You are driving a specific Chromium browser view on this machine.',
-      `Your target is CDP target_id=${ctx.targetId} on port ${ctx.cdpPort} (env BU_TARGET_ID / BU_CDP_PORT).`,
+    ];
+    if (ctx.cdpUrl) {
+      lines.push(`You are connected to an external browser via CDP (env BU_DAEMON_SOCKET).`);
+    } else {
</file context>
Fix with Cubic

`Your target is CDP target_id=${ctx.targetId} on port ${ctx.cdpPort} (env BU_TARGET_ID / BU_CDP_PORT).`,
];
if (ctx.cdpUrl) {
lines.push(`You are connected to an external browser via CDP (env BU_DAEMON_SOCKET).`);
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Prompt mentions the wrong env var for external CDP WebSocket mode; it should refer to BU_CDP_WS, not BU_DAEMON_SOCKET.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/hl/engines/claude-code/adapter.ts, line 140:

<comment>Prompt mentions the wrong env var for external CDP WebSocket mode; it should refer to `BU_CDP_WS`, not `BU_DAEMON_SOCKET`.</comment>

<file context>
@@ -135,10 +135,16 @@ const claudeCodeAdapter: EngineAdapter = {
-      `Your target is CDP target_id=${ctx.targetId} on port ${ctx.cdpPort} (env BU_TARGET_ID / BU_CDP_PORT).`,
+    ];
+    if (ctx.cdpUrl) {
+      lines.push(`You are connected to an external browser via CDP (env BU_DAEMON_SOCKET).`);
+    } else {
+      lines.push(`Your target is CDP target_id=${ctx.targetId} on port ${ctx.cdpPort} (env BU_TARGET_ID / BU_CDP_PORT).`);
</file context>
Suggested change
lines.push(`You are connected to an external browser via CDP (env BU_DAEMON_SOCKET).`);
+ lines.push(`You are connected to an external browser via CDP WebSocket (env BU_CDP_WS).`);
Fix with Cubic

The new CdpUrlSection component calls settings.cdpUrl.get() on mount.
The existing test mock did not include this API, causing a TypeError
(\"Cannot read properties of undefined (reading 'get')\") and failing CI.
@Cheggin
Copy link
Copy Markdown
Collaborator

Cheggin commented May 6, 2026

this is really cool, though I think the incentive behind the app is that this should be a complete replacement of that flow, because you can copy all auth cookies in this environment.

But I think we should support the capability regardless. I will review later!

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