Skip to content

[fix]: fail fast on .connect() when no ext found - #2790

Open
seanmcguire12 wants to merge 5 commits into
mainfrom
fail-fast-when-no-ext-found
Open

[fix]: fail fast on .connect() when no ext found#2790
seanmcguire12 wants to merge 5 commits into
mainfrom
fail-fast-when-no-ext-found

Conversation

@seanmcguire12

@seanmcguire12 seanmcguire12 commented Aug 20, 2026

Copy link
Copy Markdown
Member

why

  • connecting Stagehand to an existing Browserbase session created without the Stagehand extension currently fails after a 60s timeout. this is a poor developer experience, & a generic timeout error is not informative at all

what changed

  • changed client side extension discovery to use CDP Extensions.getExtensions
    • this searches for the Stagehand Runtime
  • extension discovery fails immediately when Stagehand is missing, disabled, or installed more than once
  • updated Browserbase .connect() docs so externally managed sessions include a previously uploaded Stagehand extension when they are created
  • also added clarification in docs mentioning that extensions cannot be added after session startup, direct CDP attachment expects the browser to share the SDK filesystem, and Browserbase keep-alive sessions reconnect throughbrowserbase.connect() by session ID

test plan

  • TS: tests extension discovery, exact worker selection, invalid inventories, and command failures
  • python: tests the connection flow, invalid inventories, and command failures
  • go: tests extension discovery, exact worker selection, invalid inventories, and command failures

Summary by cubic

Fail fast when connecting to a Browserbase session that lacks the Stagehand extension. Previously .connect() hung for ~60s and timed out; now it errors immediately with a clear message when the extension is missing, disabled, or duplicated.

  • Replaces service-worker probing with CDP inventory via Extensions.getExtensions to find the installed and enabled “Stagehand Runtime,” then attaches to its service worker.
  • Errors include: “Stagehand extension is not installed…”, “installed but is disabled,” or “Multiple enabled Stagehand extensions are installed: …”.
  • Keeps browserbase.launch() behavior unchanged (it uploads and configures the extension automatically).
  • Updates docs to require including the uploaded extension ID when externally creating Browserbase sessions, clarifies that extensions cannot be added after startup, and that keep‑alive sessions should reconnect via browserbase.connect().

Review notes

  • TypeScript/Go/Python: removed preloaded worker scanning; added installed‑extension discovery and unified service‑worker attach logic.
  • New tests cover extension discovery, exact worker selection, invalid inventories, and command failures; removed tests for the old preloaded probing path.
  • Docs examples now pass extensionId when creating sessions and use browserbase.connect() for reconnection; localBrowser.connect() examples target local CDP only.

Rollout / migration

  • Externally managed Browserbase sessions must include the uploaded Stagehand extension when created. Required action: upload the extension, then pass its uploaded‑extension resource ID as extensionId to the Browserbase Sessions API.
  • For keep‑alive sessions, reconnect with browserbase.connect({ sessionId }). Do not rely on localBrowser.connect() for Browserbase sessions.
  • Expect earlier, explicit errors on .connect() if the extension is missing, disabled, or installed more than once.

Written for commit 6739a50. Summary will update on new commits.

Review in cubic

@seanmcguire12
seanmcguire12 requested a review from a team as a code owner August 20, 2026 22:40
@mintlify

mintlify Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
stagehand 🟢 Ready View Preview Aug 20, 2026, 10:43 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@changeset-bot

changeset-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6739a50

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

This PR includes changesets to release 10 packages
Name Type
@browserbasehq/stagehand-python Patch
@browserbasehq/stagehand-go Patch
@browserbasehq/stagehand Patch
@browserbasehq/stagehand-integrations Patch
@browserbasehq/stagehand-integrations-example-eve-facade Patch
@browserbasehq/stagehand-integrations-example-pi-facade Patch
@browserbasehq/stagehand-integrations-example-claude-code-facade Patch
@browserbasehq/stagehand-integrations-example-codex-facade Patch
@browserbasehq/stagehand-integrations-example-mastra-facade Patch
@browserbasehq/stagehand-integrations-example-vercel-ai-facade Patch

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.

4 issues found across 11 files

Confidence score: 3/5

  • In packages/sdk-python/src/stagehand/cdp_client.py, extension discovery can accept an enabled runtime when another matching installation is disabled, allowing ambiguous multiple installations and potentially connecting to the wrong extension; count all matching installed extensions before returning an ID.
  • The discovery error paths in packages/sdk-ts/src/cdpClient.ts (discoverInstalledStagehandExtensionId and CDPClient.connect) and packages/sdk-python/src/stagehand/cdp_client.py raise generic Error/RuntimeError values, making failures harder to classify and potentially exposing unsanitized messages; use the dedicated typed configuration/discovery errors instead.
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="packages/sdk-python/src/stagehand/cdp_client.py">

<violation number="1" location="packages/sdk-python/src/stagehand/cdp_client.py:414">
P2: When the browser contains one enabled and another disabled `Stagehand Runtime`, this branch accepts the enabled ID instead of rejecting multiple installations. Check the total matching `installed` count before returning an ID so `.connect()` honors the fail-on-duplicate contract.</violation>

<violation number="2" location="packages/sdk-python/src/stagehand/cdp_client.py:418">
P2: Custom agent: **Exception and error message sanitization**

The new extension-discovery failure path raises generic `RuntimeError` instead of a dedicated typed error, contrary to the exception sanitization rule. Define and use a specific Stagehand CDP/extension error class for the discovery and inventory-validation failures.</violation>
</file>

<file name="packages/sdk-ts/src/cdpClient.ts">

<violation number="1" location="packages/sdk-ts/src/cdpClient.ts:238">
P2: Custom agent: **Exception and error message sanitization**

When `CDPClient.connect` receives no extension source, it propagates a generic `Error`. Throw a dedicated typed configuration error instead.</violation>

<violation number="2" location="packages/sdk-ts/src/cdpClient.ts:528">
P1: Custom agent: **Exception and error message sanitization**

When `.connect()` discovers an invalid Stagehand extension inventory, `discoverInstalledStagehandExtensionId` rethrows generic `Error` instances to the caller. Replace these with a typed discovery error using fixed sanitized messages, and do not include extension IDs.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client as Stagehand Client
    participant CDP as CDP Client
    participant Chrome as Chrome Browser
    participant Ext as Extensions API
    participant SW as Service Worker
    
    Note over Client,SW: Connect to Browserbase Session
    
    Client->>CDP: connect({ preloadedExtension: true })
    CDP->>Chrome: open CDP WebSocket
    
    Note over CDP,Ext: NEW: Discover Stagehand Extension
    
    CDP->>Ext: Extensions.getExtensions()
    Ext-->>CDP: extension list (id, name, enabled)
    
    alt Extension not found
        CDP-->>Client: Error: "Stagehand extension is not installed"
    else Extension disabled
        CDP-->>Client: Error: "installed but disabled"
    else Multiple enabled extensions
        CDP-->>Client: Error: "Multiple enabled Stagehand extensions installed"
    else Single enabled extension found
        CDP->>CDP: Extract extensionId from inventory
    end
    
    Note over CDP,SW: Attach to Service Worker
    
    CDP->>Chrome: Target.getTargets()
    Chrome-->>CDP: target list
    CDP->>CDP: Filter for chrome-extension://{extensionId}/
    
    alt Worker not found
        CDP->>Chrome: Target.createTarget (activate worker)
        Chrome-->>CDP: activation targetId
    end
    
    CDP->>SW: Target.attachToTarget({ targetId, flatten: true })
    SW-->>CDP: sessionId
    
    CDP->>SW: Runtime.enable
    CDP->>SW: Runtime.addBinding
    CDP->>SW: Runtime.evaluate (readiness check)
    SW-->>CDP: runtime marker + hasReceiver
    
    CDP-->>Client: CDPClient (with sessionId, extensionId)
    Client->>Client: Store serviceWorker info
Loading

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

Re-trigger cubic

.map((extension) => extension.id)
.sort()
.join(", ");
throw new Error(`Multiple enabled Stagehand extensions are installed: ${ids}`);

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.

P1: Custom agent: Exception and error message sanitization

When .connect() discovers an invalid Stagehand extension inventory, discoverInstalledStagehandExtensionId rethrows generic Error instances to the caller. Replace these with a typed discovery error using fixed sanitized messages, and do not include extension IDs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk-ts/src/cdpClient.ts, line 528:

<comment>When `.connect()` discovers an invalid Stagehand extension inventory, `discoverInstalledStagehandExtensionId` rethrows generic `Error` instances to the caller. Replace these with a typed discovery error using fixed sanitized messages, and do not include extension IDs.</comment>

<file context>
@@ -496,82 +503,37 @@ export async function waitForRuntimeReady(
+      .map((extension) => extension.id)
+      .sort()
+      .join(", ");
+    throw new Error(`Multiple enabled Stagehand extensions are installed: ${ids}`);
+  }
+  if (installed.length > 0) {
</file context>

Comment on lines +414 to +422
if len(enabled) == 1:
return enabled[0].id
if len(enabled) > 1:
ids = ", ".join(sorted(extension.id for extension in enabled))
raise RuntimeError(f"Multiple enabled Stagehand extensions are installed: {ids}")
if installed:
raise RuntimeError(
"Stagehand extension is installed in the connected browser but is disabled."
)

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.

P2: When the browser contains one enabled and another disabled Stagehand Runtime, this branch accepts the enabled ID instead of rejecting multiple installations. Check the total matching installed count before returning an ID so .connect() honors the fail-on-duplicate contract.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk-python/src/stagehand/cdp_client.py, line 414:

<comment>When the browser contains one enabled and another disabled `Stagehand Runtime`, this branch accepts the enabled ID instead of rejecting multiple installations. Check the total matching `installed` count before returning an ID so `.connect()` honors the fail-on-duplicate contract.</comment>

<file context>
@@ -394,9 +402,32 @@ async def _load_unpacked_extension(self, extension_dir: str) -> str:
+        ]
+        enabled = [extension for extension in installed if extension.enabled]
+
+        if len(enabled) == 1:
+            return enabled[0].id
+        if len(enabled) > 1:
</file context>
Suggested change
if len(enabled) == 1:
return enabled[0].id
if len(enabled) > 1:
ids = ", ".join(sorted(extension.id for extension in enabled))
raise RuntimeError(f"Multiple enabled Stagehand extensions are installed: {ids}")
if installed:
raise RuntimeError(
"Stagehand extension is installed in the connected browser but is disabled."
)
if len(installed) > 1:
ids = ", ".join(sorted(extension.id for extension in installed))
raise RuntimeError(f"Multiple Stagehand extensions are installed: {ids}")
if len(enabled) == 1:
return enabled[0].id
if installed:
raise RuntimeError(
"Stagehand extension is installed in the connected browser but is disabled."
)

return enabled[0].id
if len(enabled) > 1:
ids = ", ".join(sorted(extension.id for extension in enabled))
raise RuntimeError(f"Multiple enabled Stagehand extensions are installed: {ids}")

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.

P2: Custom agent: Exception and error message sanitization

The new extension-discovery failure path raises generic RuntimeError instead of a dedicated typed error, contrary to the exception sanitization rule. Define and use a specific Stagehand CDP/extension error class for the discovery and inventory-validation failures.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk-python/src/stagehand/cdp_client.py, line 418:

<comment>The new extension-discovery failure path raises generic `RuntimeError` instead of a dedicated typed error, contrary to the exception sanitization rule. Define and use a specific Stagehand CDP/extension error class for the discovery and inventory-validation failures.</comment>

<file context>
@@ -394,9 +402,32 @@ async def _load_unpacked_extension(self, extension_dir: str) -> str:
+            return enabled[0].id
+        if len(enabled) > 1:
+            ids = ", ".join(sorted(extension.id for extension in enabled))
+            raise RuntimeError(f"Multiple enabled Stagehand extensions are installed: {ids}")
+        if installed:
+            raise RuntimeError(
</file context>

},
undefined,
signal,
throw new Error(

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.

P2: Custom agent: Exception and error message sanitization

When CDPClient.connect receives no extension source, it propagates a generic Error. Throw a dedicated typed configuration error instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk-ts/src/cdpClient.ts, line 238:

<comment>When `CDPClient.connect` receives no extension source, it propagates a generic `Error`. Throw a dedicated typed configuration error instead.</comment>

<file context>
@@ -213,39 +227,32 @@ export class CDPClient {
-          },
-          undefined,
-          signal,
+        throw new Error(
+          "Exactly one of extensionDir, extensionId, or preloadedExtension is required",
         );
</file context>

return {
id,
name,
version: "1.0.0",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I believe we published a patch so might be 1.0.1 for the extension, might need to update this test to pull from STAGEHAND_RUNTIME_VERSION

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