Skip to content

feat(python): extension_cdp_url for local_browser.connect - #3252

Merged
seanmcguire12 merged 4 commits into
mainfrom
evals/sydney-runkle-extension-cdp-url
Oct 19, 2026
Merged

seanmcguire12 merged 4 commits into
mainfrom
evals/sydney-runkle-extension-cdp-url

Conversation

@seanmcguire12

@seanmcguire12 seanmcguire12 commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Why

Stagehand's extension opens its own CDP connection to the browser, and it is handed the URL the Python client connected with. That only works when the client and the browser can use the same address.

Sometimes they can't. Our case: the client runs in a server process and reaches a sandboxed Chrome through an authenticated gateway, wss://<gateway>/.../cdp?api_key=.... The extension, running inside that Chrome, should dial the browser's local endpoint (ws://127.0.0.1:<port>/devtools/browser/<id>). Using the gateway URL would hand the gateway's credentials to the browser, which loads arbitrary pages, and send every extension command out and back in through the gateway. Today the only workaround is overriding the private Stagehand._worker_init_params.

What

browser = await local_browser.connect(
    cdp_url="wss://gateway/cdp?api_key=...",
    extension_cdp_url="ws://127.0.0.1:9300/devtools/browser/<id>",
)
stagehand = await Stagehand.create(browser=browser)
  • local_browser.connect takes an optional extension_cdp_url, which must be a ws:// or wss:// URL with a host.
  • It is carried on the claimed browser's worker metadata, and Stagehand.create sends it as the init param browser_cdp_url in place of the client's URL.
  • When it is unset, behaviour is unchanged.
  • Python SDK only, with a minor changeset. The TypeScript SDK has the same assumption and could take the same option; happy to follow up.

Tests

  • New tests cover the init param, connect carrying the value through, the default, and rejection of non-WebSocket URLs.
  • ruff format --check, ruff check, ty check and generate.py --check pass.
  • pytest: 793 passed. test_official_api_routes_through_and_never_closes_a_caller_http_client fails with and without this change in my checkout, because it needs the extension built in packages/extension/dist.

🤖 Generated with Claude Code


Summary by cubic

Adds an optional extension_cdp_url to local_browser.connect in the Python SDK so Stagehand's extension can dial a different CDP WebSocket than the client's, when the client reaches Chrome through a proxy or gateway the extension cannot use.

  • When set, Stagehand.create sends it as the init param browser_cdp_url in place of the client's URL; when unset, behavior is unchanged.
  • The value must be a ws:// or wss:// URL with a host; invalid schemes, ports (including 0), or missing hostnames raise a validation error.
  • Python SDK only, with a minor changeset; the TypeScript SDK could take the same option in a follow-up.
  • New tests cover the init param, metadata carrying the value through, the default, and rejection of invalid URLs. pytest reports 793 passed; test_official_api_routes_through_and_never_closes_a_caller_http_client fails with and without this change because it needs the extension built in packages/extension/dist.

Written for commit 48a3789. Summary will update on new commits.

View guided diff Turn on auto-fix

## Why

Stagehand's extension opens its own CDP connection to the browser, and
it is handed the URL the Python client connected with. That only works
when the client and the browser can use the same address.

Sometimes they can't. Our case: the client runs in a server process and
reaches a sandboxed Chrome through an authenticated gateway,
`wss://<gateway>/.../cdp?api_key=...`. The extension, running inside
that Chrome, should dial the browser's local endpoint
(`ws://127.0.0.1:<port>/devtools/browser/<id>`). Using the gateway URL
would hand the gateway's credentials to the browser, which loads
arbitrary pages, and send every extension command out and back in
through the gateway. Today the only workaround is overriding the private
`Stagehand._worker_init_params`.

## What

```python
browser = await local_browser.connect(
    cdp_url="wss://gateway/cdp?api_key=...",
    extension_cdp_url="ws://127.0.0.1:9300/devtools/browser/<id>",
)
stagehand = await Stagehand.create(browser=browser)
```

- `local_browser.connect` takes an optional `extension_cdp_url`, which
must be a `ws://` or `wss://` URL with a host.
- It is carried on the claimed browser's worker metadata, and
`Stagehand.create` sends it as the init param `browser_cdp_url` in place
of the client's URL.
- When it is unset, behaviour is unchanged.
- Python SDK only, with a minor changeset. The TypeScript SDK has the
same assumption and could take the same option; happy to follow up.

## Tests

- New tests cover the init param, connect carrying the value through,
the default, and rejection of non-WebSocket URLs.
- `ruff format --check`, `ruff check`, `ty check` and `generate.py
--check` pass.
- `pytest`: 793 passed.
`test_official_api_routes_through_and_never_closes_a_caller_http_client`
fails with and without this change in my checkout, because it needs the
extension built in `packages/extension/dist`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds an optional `extension_cdp_url` to `local_browser.connect` so
Stagehand's extension can dial a different CDP WebSocket than the
client's, for cases where the client reaches Chrome through a proxy the
extension cannot use.

- When set, Stagehand sends it as the init param `browser_cdp_url` in
place of the client's URL; when unset, behavior is unchanged.
- The URL must be `ws://` or `wss://` with a host; anything else raises
a validation error.
- Python SDK only; the TypeScript SDK has the same assumption and could
take the same option in a follow-up.
- New tests cover the init param, carrying the value through, the
default, and rejection of invalid URLs. `pytest` reports 793 passed;
`test_official_api_routes_through_and_never_closes_a_caller_http_client`
fails with and without this change because it needs the extension built
in `packages/extension/dist`.

<sup>Written for commit c119e53.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/3223?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="View guided diff"
src="https://www.cubic.dev/buttons/review-in-cubic-light.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

Co-authored-by: Sydney Runkle <sydney@langchain.dev>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Sean McGuire <75873287+seanmcguire12@users.noreply.github.com>
@changeset-bot

changeset-bot Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 48a3789

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@browserbasehq/stagehand-python Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/sdk-python/src/stagehand/_validation.py

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 8 files

Architecture diagram
sequenceDiagram
    participant Client as Python Client
    participant Connect as local_browser.connect()
    participant Validate as URL Validator
    participant Browser as StagehandBrowser
    participant Stagehand as Stagehand.create()
    participant Ext as Browser Extension

    Note over Client,Ext: Browser Connection with Optional Extension CDP URL

    Client->>Connect: connect(cdp_url, extension_cdp_url?)
    
    alt extension_cdp_url provided
        Client->>Validate: Validate extension_cdp_url
        alt Valid ws:// or wss:// URL
            Validate-->>Connect: Valid URL
        else Invalid URL format
            Validate-->>Connect: ValidationError
            Connect-->>Client: Raise error
        end
        Connect->>Browser: Attach metadata (browser_cdp_url = extension_cdp_url)
    else No extension_cdp_url
        Connect->>Browser: Attach metadata (browser_cdp_url = None)
    end

    Client->>Stagehand: Stagehand.create(browser)
    
    alt browser_cdp_url is set
        Stagehand->>Ext: Send browser_cdp_url as init param
        Note over Stagehand,Ext: Extension dials local CDP endpoint
    else browser_cdp_url is None
        Stagehand->>Ext: Use client's web_socket_debugger_url
        Note over Stagehand,Ext: Extension uses same URL as client
    end

    Ext->>Ext: Connect to CDP WebSocket
    Ext-->>Stagehand: Extension ready
    Stagehand-->>Client: Stagehand instance
Loading

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.

View guided diff | Turn on auto-fix | Re-trigger cubic

@seanmcguire12 seanmcguire12 changed the title feat(python): extension_cdp_url for local_browser.connect (#3223) feat(python): extension_cdp_url for local_browser.connect Oct 19, 2026
@seanmcguire12
seanmcguire12 added this pull request to stack #3258 October 19, 2026 18:58
@seanmcguire12
seanmcguire12 merged commit 21e0363 into main Oct 19, 2026
22 of 26 checks passed
@seanmcguire12
seanmcguire12 deleted the evals/sydney-runkle-extension-cdp-url branch October 19, 2026 19:47
miguelg719 pushed a commit that referenced this pull request Oct 20, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @browserbasehq/stagehand@4.3.0

### Minor Changes

- [#3267](#3267)
[`cb9a1d1`](cb9a1d1)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add
extension archive getter

- [#3253](#3253)
[`6bf4146`](6bf4146)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add
optional `extensionCdpUrl` to `localBrowser.connect` to allow
configuring the in-browser CDP url

- [#3263](#3263)
[`ecb2a6b`](ecb2a6b)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add
`headers` to `localBrowser.connect` so CDP gateways can be authenticated
without credentials in the URL

- [#3183](#3183)
[`901eb27`](901eb27)
Thanks [@supremeboxlogos](https://github.com/supremeboxlogos)! - Add
`page.waitForFunction()` to wait until a predicate in the page returns a
truthy value.

### Patch Changes

- [#3229](#3229)
[`c6b6f27`](c6b6f27)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - skip
caching for request bodies larger than 25 MB.

- [#3198](#3198)
[`10418f9`](10418f9)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - enable
WebMCP by default for local chromium versions newer than 157

- [#3210](#3210)
[`1c40ba7`](1c40ba7)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - Require
client transport for Browserbase Model Gateway and cache requests.

- [#3209](#3209)
[`17de6ea`](17de6ea)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - Route
Browserbase Model Gateway and cache requests through the connected SDK.
## @browserbasehq/eve@0.2.2

### Patch Changes

- Updated dependencies
[[`c6b6f27`](c6b6f27),
[`cb9a1d1`](cb9a1d1),
[`10418f9`](10418f9),
[`1c40ba7`](1c40ba7),
[`17de6ea`](17de6ea),
[`6bf4146`](6bf4146),
[`ecb2a6b`](ecb2a6b),
[`901eb27`](901eb27)]:
  - @browserbasehq/stagehand@4.3.0
## @browserbasehq/stagehand-protocol@3.0.0

### Major Changes

- [#3210](#3210)
[`1c40ba7`](1c40ba7)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - Require
client transport for Browserbase Model Gateway and cache requests.

### Minor Changes

- [#3208](#3208)
[`4a7c637`](4a7c637)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - Support
routing Browserbase Model Gateway and cache requests through the
connected SDK.

- [#3183](#3183)
[`901eb27`](901eb27)
Thanks [@supremeboxlogos](https://github.com/supremeboxlogos)! - Add the
`page.wait_for_function` method.
## @browserbasehq/stagehand-extension@1.2.0

### Minor Changes

- [#3208](#3208)
[`4a7c637`](4a7c637)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - Support
routing Browserbase Model Gateway and cache requests through the
connected SDK.

- [#3183](#3183)
[`901eb27`](901eb27)
Thanks [@supremeboxlogos](https://github.com/supremeboxlogos)! - Add the
`page.wait_for_function` method.

### Patch Changes

- [#3229](#3229)
[`c6b6f27`](c6b6f27)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - skip
caching for request bodies larger than 25 MB.

- [#3257](#3257)
[`113ad81`](113ad81)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - honour a
changed extension CDP WS URL when creating a new Stagehand instance
after closing the previous one

- [#3210](#3210)
[`1c40ba7`](1c40ba7)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - Require
client transport for Browserbase Model Gateway and cache requests.
## @browserbasehq/stagehand-go@4.3.0

### Minor Changes

- [#3267](#3267)
[`cb9a1d1`](cb9a1d1)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add
extension archive getter

- [#3265](#3265)
[`a31e2da`](a31e2da)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add
`Headers` to `LocalBrowserConnectOptions` so CDP gateways can be
authenticated without credentials in the URL

- [#3254](#3254)
[`9eac212`](9eac212)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add
optional `ExtensionCDPURL` to `LocalBrowserConnectOptions` to allow
configuring the in-browser CDP url.

- [#3183](#3183)
[`901eb27`](901eb27)
Thanks [@supremeboxlogos](https://github.com/supremeboxlogos)! - Add
`Page.WaitForFunction` to wait until a predicate in the page returns a
truthy value.

### Patch Changes

- [#3229](#3229)
[`c6b6f27`](c6b6f27)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - skip
caching for request bodies larger than 25 MB.

- [#3198](#3198)
[`10418f9`](10418f9)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - enable
WebMCP by default for local chromium versions newer than 157

- [#3210](#3210)
[`1c40ba7`](1c40ba7)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - Require
client transport for Browserbase Model Gateway and cache requests.

- [#3209](#3209)
[`17de6ea`](17de6ea)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - Route
Browserbase Model Gateway and cache requests through the connected SDK.
## @browserbasehq/stagehand-python@4.3.0

### Minor Changes

- [#3260](#3260)
[`2859fd5`](2859fd5)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add
`headers` to `local_browser.connect` so CDP gateways can be
authenticated without credentials in the URL

- [#3267](#3267)
[`cb9a1d1`](cb9a1d1)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add
extension archive getter

- [#3252](#3252)
[`21e0363`](21e0363)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add
`extension_cdp_url` to `local_browser.connect` for clients that reach
Chrome through a proxy the extension cannot use

- [#3183](#3183)
[`901eb27`](901eb27)
Thanks [@supremeboxlogos](https://github.com/supremeboxlogos)! - Add
`page.wait_for_function()` to wait until a predicate in the page returns
a truthy value.

### Patch Changes

- [#3229](#3229)
[`c6b6f27`](c6b6f27)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - skip
caching for request bodies larger than 25 MB.

- [#3198](#3198)
[`10418f9`](10418f9)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - enable
WebMCP by default for local chromium versions newer than 157

- [#3210](#3210)
[`1c40ba7`](1c40ba7)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - Require
client transport for Browserbase Model Gateway and cache requests.

- [#3209](#3209)
[`17de6ea`](17de6ea)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - Route
Browserbase Model Gateway and cache requests through the connected SDK.
## @browserbasehq/stagehand-examples@0.0.3

### Patch Changes

- Updated dependencies
[[`c6b6f27`](c6b6f27),
[`cb9a1d1`](cb9a1d1),
[`10418f9`](10418f9),
[`1c40ba7`](1c40ba7),
[`17de6ea`](17de6ea),
[`6bf4146`](6bf4146),
[`ecb2a6b`](ecb2a6b),
[`901eb27`](901eb27)]:
  - @browserbasehq/stagehand@4.3.0
  - @browserbasehq/stagehand-integrations@4.0.6
## @browserbasehq/stagehand-integrations-claude-agent-sdk@4.0.5

### Patch Changes

- Updated dependencies []:
  - @browserbasehq/stagehand-integrations@4.0.6
##
@browserbasehq/stagehand-integrations-example-claude-code-facade@4.0.6

### Patch Changes

- Updated dependencies []:
  - @browserbasehq/stagehand-integrations@4.0.6
  - @browserbasehq/stagehand-integrations-claude-agent-sdk@4.0.5
## @browserbasehq/stagehand-integrations-claude-cua-sdk@4.0.4

### Patch Changes

- Updated dependencies
[[`c6b6f27`](c6b6f27),
[`cb9a1d1`](cb9a1d1),
[`10418f9`](10418f9),
[`1c40ba7`](1c40ba7),
[`17de6ea`](17de6ea),
[`6bf4146`](6bf4146),
[`ecb2a6b`](ecb2a6b),
[`901eb27`](901eb27)]:
  - @browserbasehq/stagehand@4.3.0
  - @browserbasehq/stagehand-integrations@4.0.6
## @browserbasehq/stagehand-integrations-example-codex-facade@4.0.6

### Patch Changes

- Updated dependencies []:
  - @browserbasehq/stagehand-integrations@4.0.6
  - @browserbasehq/stagehand-integrations-codex-sdk@4.0.5
## @browserbasehq/stagehand-integrations-codex-sdk@4.0.5

### Patch Changes

- Updated dependencies []:
  - @browserbasehq/stagehand-integrations@4.0.6
## @browserbasehq/stagehand-integrations@4.0.6

### Patch Changes

- Updated dependencies
[[`c6b6f27`](c6b6f27),
[`cb9a1d1`](cb9a1d1),
[`10418f9`](10418f9),
[`1c40ba7`](1c40ba7),
[`17de6ea`](17de6ea),
[`6bf4146`](6bf4146),
[`ecb2a6b`](ecb2a6b),
[`901eb27`](901eb27)]:
  - @browserbasehq/stagehand@4.3.0
## @browserbasehq/stagehand-integrations-example-cursor-facade@4.0.4

### Patch Changes

- Updated dependencies []:
  - @browserbasehq/stagehand-integrations@4.0.6
  - @browserbasehq/stagehand-integrations-cursor-sdk@4.0.5
## @browserbasehq/stagehand-integrations-cursor-sdk@4.0.5

### Patch Changes

- Updated dependencies []:
  - @browserbasehq/stagehand-integrations@4.0.6
## @browserbasehq/stagehand-integrations-deepagents-sdk@4.0.5

### Patch Changes

- Updated dependencies []:
  - @browserbasehq/stagehand-integrations@4.0.6
## @browserbasehq/stagehand-integrations-eve-sdk@4.0.5

### Patch Changes

- Updated dependencies []:
  - @browserbasehq/stagehand-integrations@4.0.6
## @browserbasehq/stagehand-integrations-fx-sdk@4.0.5

### Patch Changes

- Updated dependencies []:
  - @browserbasehq/stagehand-integrations@4.0.6
## @browserbasehq/stagehand-integrations-gemini-cua-sdk@4.0.4

### Patch Changes

- Updated dependencies
[[`c6b6f27`](c6b6f27),
[`cb9a1d1`](cb9a1d1),
[`10418f9`](10418f9),
[`1c40ba7`](1c40ba7),
[`17de6ea`](17de6ea),
[`6bf4146`](6bf4146),
[`ecb2a6b`](ecb2a6b),
[`901eb27`](901eb27)]:
  - @browserbasehq/stagehand@4.3.0
  - @browserbasehq/stagehand-integrations@4.0.6
## @browserbasehq/stagehand-integrations-example-mastra-facade@4.0.6

### Patch Changes

- Updated dependencies []:
  - @browserbasehq/stagehand-integrations@4.0.6
## @browserbasehq/stagehand-integrations-mastra-sdk@4.0.5

### Patch Changes

- Updated dependencies []:
  - @browserbasehq/stagehand-integrations@4.0.6
## @browserbasehq/stagehand-integrations-example-pi-facade@4.0.6

### Patch Changes

- Updated dependencies
[[`c6b6f27`](c6b6f27),
[`cb9a1d1`](cb9a1d1),
[`10418f9`](10418f9),
[`1c40ba7`](1c40ba7),
[`17de6ea`](17de6ea),
[`6bf4146`](6bf4146),
[`ecb2a6b`](ecb2a6b),
[`901eb27`](901eb27)]:
  - @browserbasehq/stagehand@4.3.0
  - @browserbasehq/stagehand-integrations@4.0.6
## @browserbasehq/stagehand-integrations-pi-sdk@4.0.5

### Patch Changes

- Updated dependencies []:
  - @browserbasehq/stagehand-integrations@4.0.6
## @browserbasehq/stagehand-integrations-example-vercel-ai-facade@4.0.6

### Patch Changes

- Updated dependencies []:
  - @browserbasehq/stagehand-integrations@4.0.6

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
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.

3 participants