Implement Browserbase sessions in the Python browser factories - #2552
Conversation
Review fixes on the Browserbase session integration:
- Map fingerprint keys to the Browserbase API's camelCase wire names
(httpVersion, operatingSystems, screen.maxHeight/maxWidth/minHeight/
minWidth). browserbase-py 1.15 has no fingerprint TypedDict key, so
the Stainless transform passed the dumped snake_case keys through
unchanged and the API silently dropped them. The kwargs construction
now lives in _session_create_kwargs with direct test coverage.
- Delete the owned extension when session creation is cancelled:
the cleanup handler now catches BaseException, re-raising
non-Exception errors (CancelledError) unchanged.
- Suppress the Content-Type header on extensions.delete via the omit
sentinel, porting the TS SDK's explicit workaround (TS sends
headers {"Content-Type": null}).
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
7 issues found across 12 files
Confidence score: 3/5
- In
packages/sdk-python/src/stagehand/browser.py, a failed Browserbase release/delete can makehandle.close()reuse the failed task and skipsession.close()on later attempts, which can orphan sessions/extensions and leak resources—reset close-task state after failure so cleanup remains retryable. - In
packages/sdk-python/src/stagehand/cdp_client.py(_wait_for_preloaded_service_worker), timeout/error paths can include raw CDP target URLs and broadexcept Exception: passhides attach/evaluate failures, creating both data-exposure risk and poor diagnosability—sanitize timeout content and narrow/log caught exceptions. - In
packages/sdk-python/src/stagehand/browserbase_session.py, session create/retrieve paths chain upstream SDK exceptions intoBrowserbaseSessionError, so serialized tracebacks can leak details the public error is meant to hide;packages/sdk-python/tests/test_browserbase_session.pycurrently reinforces that behavior—raise sanitized errors withfrom Noneand update tests to assert typed, redacted failures. - In
packages/sdk-python/src/stagehand/extension_assets.py, non-deterministic ZIP metadata (mtime viaZipFile.write()) can make identical extension content hash differently and cause unnecessary reprovisioning/churn—write entries with fixedZipInfotimestamps/attributes.
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/extension_assets.py">
<violation number="1" location="packages/sdk-python/src/stagehand/extension_assets.py:29">
P3: Identical extension contents do not produce a deterministic archive because `ZipFile.write()` embeds filesystem mtimes. Write entries with a fixed `ZipInfo.date_time` (and fixed attributes) so repeated provisioning produces stable bytes.</violation>
</file>
<file name="packages/sdk-python/src/stagehand/cdp_client.py">
<violation number="1" location="packages/sdk-python/src/stagehand/cdp_client.py:490">
P3: In the preloaded-extension discovery loop, `except Exception: pass` swallows every attach/evaluate failure, and the eventual TimeoutError message only lists observed target types/URLs. If the Stagehand worker is present but the readiness evaluation keeps failing for a real reason (e.g. a session error, permission issue, or the worker dying between getTargets and attach), the operator sees only a generic 'Timed out discovering the preloaded Stagehand service worker' with no hint of the underlying failure. Consider capturing the last exception/detail (like _wait_for_runtime_receiver already does with its last_error) and including it in the timeout message, so a failed discovery is debuggable instead of opaque.</violation>
<violation number="2" location="packages/sdk-python/src/stagehand/cdp_client.py:506">
P1: Custom agent: **Exception and error message sanitization**
The new `_wait_for_preloaded_service_worker` raises a generic `TimeoutError` that embeds raw CDP target URLs in the message. This exposes potentially sensitive page URLs, query parameters, and internal endpoints to the user. Consider using a typed error class (e.g., `BrowserbaseSessionError`, which this PR already sanitizes elsewhere) and redacting or limiting the target URLs included in the message.</violation>
</file>
<file name="packages/sdk-python/src/stagehand/browserbase_session.py">
<violation number="1" location="packages/sdk-python/src/stagehand/browserbase_session.py:219">
P2: Session-create failures retain the upstream SDK exception as a chained cause, so traceback/error serialization can expose details the sanitized `BrowserbaseSessionError` is meant to hide. Raise the public error with `from None` instead.
(Based on your team's feedback about sanitizing Browserbase session errors.)</violation>
<violation number="2" location="packages/sdk-python/src/stagehand/browserbase_session.py:250">
P2: Session-retrieve failures retain the upstream SDK exception as a chained cause, so traceback/error serialization can expose details the sanitized `BrowserbaseSessionError` is meant to hide. Raise the public error with `from None` instead.
(Based on your team's feedback about sanitizing Browserbase session errors.)</violation>
</file>
<file name="packages/sdk-python/tests/test_browserbase_session.py">
<violation number="1" location="packages/sdk-python/tests/test_browserbase_session.py:332">
P2: Custom agent: **Exception and error message sanitization**
The tests here lock in error behavior that conflicts with the sanitized, typed-error requirement. In packages/sdk-python/src/stagehand/browserbase_session.py, `close()` re-raises the raw upstream release/delete exceptions verbatim (`raise release_error` / `raise extension_error`), so a caller closing a session can receive an un-sanitized, untyped upstream SDK exception (the test even asserts it is the exact same object: `raised.value is release_error`). Separately, the extension-upload failure path raises a generic `RuntimeError("Failed to upload the Stagehand extension to Browserbase")` and chains the raw SDK error via `from error`, rather than using the dedicated `BrowserbaseSessionError` wrapper used everywhere else (create/retrieve) in this file. Re-raising raw SDK exceptions can surface upstream request/endpoint details to the user, and the generic `RuntimeError` for uploads is not a `BrowserbaseSessionError`. Recommend wrapping the upload failure (and the close-time release/delete errors) in `BrowserbaseSessionError` with the sanitized message, and ensure `close()` raises a typed, sanitized error rather than forwarding the raw upstream exception (or at minimum strip the raw cause), so the SDK never leaks untrusted upstream details to callers.</violation>
</file>
<file name="packages/sdk-python/src/stagehand/browser.py">
<violation number="1" location="packages/sdk-python/src/stagehand/browser.py:461">
P1: A transient Browserbase release/delete failure leaves the session or uploaded extension orphaned: subsequent `handle.close()` calls reuse the failed task and never reach `session.close()` again. Preserve retryability at the public handle/source boundary so the owned session cleanup logic can run on a later close.</violation>
</file>
Architecture diagram
sequenceDiagram
participant Caller as Caller Code
participant BrowserFile as browser.py
participant ClientModels as client_models.py
participant API as BrowserbaseSession API
participant OfficialSDK as browserbase PyPI SDK
participant CDPClient as CDP Client
participant ExtAssets as extension_assets.py
participant Browserbase as Browserbase Cloud
Note over Caller,Browserbase: Browserbase Launch Flow
Caller->>BrowserFile: browserbase.launch(api_key, region, ...)
BrowserFile->>ClientModels: Validate BrowserbaseSessionCreateParams
ClientModels-->>BrowserFile: Validated options
BrowserFile->>API: _create_browserbase_session_client(api_key)
API->>OfficialSDK: AsyncBrowserbase(api_key)
alt Caller didn't supply extension_id
API->>ExtAssets: asyncio.to_thread(build_extension_archive)
ExtAssets->>ExtAssets: Zip extension directory
ExtAssets-->>API: archive bytes
API->>OfficialSDK: extensions.create(file=archive)
OfficialSDK->>Browserbase: Upload extension
Browserbase-->>OfficialSDK: extension.id
OfficialSDK-->>API: Uploaded extension ID
else Caller supplied extension_id (top-level or in browser_settings)
API->>API: Skip upload, use caller's extension_id
end
API->>API: Merge user_metadata with stagehand keys
API->>OfficialSDK: sessions.create(**kwargs) with camelCase fingerprint mapping
OfficialSDK->>Browserbase: Create session
Browserbase-->>OfficialSDK: session.id, session.connect_url
OfficialSDK-->>API: Session created
alt Create succeeded
API-->>BrowserFile: _OwnedBrowserbaseSession (owns extension + session)
BrowserFile->>BrowserFile: Build _WorkerInitMetadata (api_key, session_id, region)
BrowserFile->>CDPClient: connect(preloaded_extension=True, ...)
CDPClient->>CDPClient: _wait_for_preloaded_service_worker()
CDPClient->>CDPClient: Poll Target.getTargets for service_worker
loop Until ready or timeout
CDPClient->>Browserbase: Target.attachToTarget
Browserbase-->>CDPClient: Session ID
CDPClient->>Browserbase: Runtime.evaluate (readiness check)
Browserbase-->>CDPClient: Worker ready?
alt Worker not ready
CDPClient->>Browserbase: Target.detachFromTarget
end
end
alt Worker ready
CDPClient-->>BrowserFile: CDPClient with service worker
BrowserFile-->>Caller: StagehandBrowser
else Timeout
CDPClient-->>BrowserFile: TimeoutError
BrowserFile->>API: session.close() (release + delete extension)
API-->>BrowserFile: Cleanup complete
BrowserFile-->>Caller: Error propagated
end
else Create failed or cancelled
API->>API: _delete_extension_best_effort(owned_extension_id)
alt CancelledError
API-->>Caller: CancelledError (re-raised)
else Exception
API-->>Caller: BrowserbaseSessionError wrapped
end
end
Note over Caller,Browserbase: Session Close/Release (when keep_alive=False)
Caller->>BrowserFile: StagehandBrowser.close()
BrowserFile->>API: session.close()
API->>API: Acquire _close_lock
API->>OfficialSDK: sessions.update(status="REQUEST_RELEASE")
OfficialSDK->>Browserbase: Release session
alt Owned extension exists
API->>OfficialSDK: extensions.delete(extension_id, Content-Type: omit)
OfficialSDK->>Browserbase: Delete extension
end
API-->>BrowserFile: Close complete (release error takes precedence)
Note over Caller,Browserbase: Browserbase Connect Flow
Caller->>BrowserFile: browserbase.connect(api_key, session_id, ...)
BrowserFile->>ClientModels: Validate BrowserbaseConnectOptions
ClientModels-->>BrowserFile: Validated options
BrowserFile->>API: _create_browserbase_session_client(api_key)
API->>OfficialSDK: sessions.retrieve(session_id)
OfficialSDK->>Browserbase: Retrieve session
Browserbase-->>OfficialSDK: session.id, connect_url, region
OfficialSDK-->>API: Retrieved session
alt connect_url is empty/missing
API-->>Caller: BrowserbaseSessionError "not available for connection"
else Valid connect_url
API-->>BrowserFile: _BrowserbaseSessionConnection (no ownership)
alt Caller supplied extension_id
CDPClient->>CDPClient: connect(extension_id=caller_extension)
else No extension_id
CDPClient->>CDPClient: connect(preloaded_extension=True)
end
CDPClient-->>BrowserFile: CDPClient connected
BrowserFile-->>Caller: StagehandBrowser (no session release on close)
end
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…-py-browserbase # Conflicts: # packages/sdk-python/tests/test_browser.py
…ion IDs, and build deterministic extension archives
There was a problem hiding this comment.
7 issues found and verified against the latest diff
Confidence score: 2/5
- In
packages/sdk-python/src/stagehand/browserbase_session.py, extension upload andclose()currently surface/chains raw SDK exceptions, which can leak sensitive request/response details and expose unstable upstream error types to users; wrap and sanitize these intoBrowserbaseSessionErrorwith scrubbed messages. - In
packages/sdk-python/src/stagehand/browserbase_session.py, a transient cleanup failure can get cached so laterbrowser.close()calls just rethrow the same failed task, leaving sessions/extensions effectively unretryable and potentially orphaned — reset failed close state or allow a fresh close attempt after failure. - In
packages/sdk-python/src/stagehand/browser.py, launching with a non-Stagehand Browserbase extension ID can skip provisioning while discovery still expects a Stagehand worker, so valid caller configurations may fail to attach Stagehand functionality — keep caller extensions but still provision/select the Stagehand extension path needed for discovery. - In
packages/sdk-python/src/stagehand/cdp_client.py,_wait_for_preloaded_service_workercan markkeep_attachedbeforeServiceWorkerInfo(...)succeeds and also suppress broad exceptions in the polling loop, which risks stale attachments and masked discovery failures that are hard to diagnose — narrow exception handling and only set attachment state after successful worker parsing.
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/browserbase_session.py">
<violation number="1" location="packages/sdk-python/src/stagehand/browserbase_session.py:154">
P1: Transient Browserbase cleanup failures leave launched sessions/extensions unretryable through the public browser handle: later `browser.close()` calls only rethrow the cached failed task. Reset failed close state or otherwise route a later close to this retry-capable cleanup path.</violation>
<violation number="2" location="packages/sdk-python/src/stagehand/browserbase_session.py:174">
P2: Custom agent: **Exception and error message sanitization**
The `close()` method surfaces raw upstream Browserbase SDK exceptions directly to users instead of wrapping them in `BrowserbaseSessionError`. Raw SDK exceptions from `release_session` and `delete_extension` may include sensitive request/response details or connection URLs. The rest of this file consistently wraps user-facing errors in sanitized `BrowserbaseSessionError` instances (e.g., in `create_session` and `connect_session`). Consider wrapping the close-time errors the same way to avoid leaking internal SDK details.</violation>
<violation number="3" location="packages/sdk-python/src/stagehand/browserbase_session.py:198">
P1: Custom agent: **Exception and error message sanitization**
The extension upload failure raises a generic `RuntimeError` and chains the upstream SDK exception via `from error`, which can leak sensitive request/response details to users. The file already defines and consistently uses `BrowserbaseSessionError` with `from None` for all other Browserbase failures (session creation, retrieve, empty results). Align this path with that pattern to keep messages sanitized and avoid exposing upstream SDK internals.
**Suggested fix:**
```python
except Exception:
raise BrowserbaseSessionError(
"Failed to upload the Stagehand extension to Browserbase"
) from None
```</violation>
</file>
<file name="packages/sdk-python/src/stagehand/browser.py">
<violation number="1" location="packages/sdk-python/src/stagehand/browser.py:475">
P2: Launching with a non-Stagehand Browserbase extension ID cannot attach Stagehand: provisioning is suppressed but discovery still requires a Stagehand worker. Preserve caller extensions while provisioning/selecting the packaged Stagehand extension separately.
(Based on your team's feedback about support for non-Stagehand Browserbase extensions.) .</violation>
</file>
<file name="packages/sdk-python/tests/test_browserbase_session.py">
<violation number="1" location="packages/sdk-python/tests/test_browserbase_session.py:178">
P2: This assertion reads the internal snake_case key back out of `_session_create_kwargs`, which cements the wrong wire format. The helper only camelCases the nested `fingerprint`/`screen` blocks; every other `browser_settings` sub-field (`advanced_stealth`, `block_ads`, `log_session`, `record_session`, `solve_captchas`, `captcha_image_selector`) is left snake_case and forwarded verbatim to the Browserbase SDK, whose browser_settings dictionary uses camelCase keys (e.g. captchaImageSelector/captchaInputSelector per the SDK docs). So `advanced_stealth=True` would silently not reach the API. Consider asserting the camelCase key here and camelCasing the remaining browser_settings fields in the helper.</violation>
</file>
<file name="packages/sdk-python/src/stagehand/cdp_client.py">
<violation number="1" location="packages/sdk-python/src/stagehand/cdp_client.py:424">
P3: This new discovery loop has non-obvious semantics that aren't explained in the code: it attaches to candidate service workers and detaches those that aren't ready yet, keeps polling with a broad `except Exception: pass`, and distinguishes 'stale' workers (ready marker but `hasReceiver` false) from ready ones. A short docstring/comment explaining why each candidate is attached then conditionally detached, and why readiness is evaluated via `Runtime.evaluate` with the readiness marker, would help future maintainers avoid regressing the retry/detach behavior.</violation>
<violation number="2" location="packages/sdk-python/src/stagehand/cdp_client.py:476">
P2: In `_wait_for_preloaded_service_worker`, `keep_attached = True` is set before the `ServiceWorkerInfo(...)` constructor runs inside the same try block. If that constructor raises — e.g. `_required_string(target_info, "title", ...)` when a service-worker target reports an empty/missing `title` — the exception is swallowed by `except Exception: pass`, but because `keep_attached` is already True the `finally` block skips the `Target.detachFromTarget`, leaking an attached flat session that is never cleaned up. Note the non-preloaded `_wait_for_service_worker` path does not have this issue: a similar `_required_string` failure there propagates so `connect`'s `except BaseException: await client.close()` runs. Build the `ServiceWorkerInfo` first and only flip `keep_attached` immediately before returning so a construction failure still triggers the detach.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…-py-browserbase # Conflicts: # packages/sdk-python/tests/test_browser.py
There was a problem hiding this comment.
6 issues found across 12 files
Confidence score: 3/5
- In
packages/sdk-python/src/stagehand/browserbase_session.py, extension-upload and teardown paths can re-raise raw Browserbase SDK exceptions (from error/raise ...) instead of sanitized errors, which risks exposing sensitive upstream request details to users and logs—wrap and redact these exceptions before re-raising. - In
packages/sdk-python/src/stagehand/browserbase_session.py(close()/handle-close flow), a failed release or extension-deletion attempt is cached as the close task, so laterhandle.close()calls cannot retry and sessions may remain unreleased—allow retries by resetting failed close state or re-invoking the source close path. - In
packages/sdk-python/README.md,keep_alive=Truebehavior leaves users without a clear way to release the launched session viaStagehandBrowser.close(), increasing the chance of leaked remote sessions and cost surprises—document the release mechanism (or expose a release API/session identifier). packages/sdk-python/src/stagehand/extension_assets.pyandpackages/sdk-python/tests/test_cdp_client.pyleave some de-risking gaps: archive bytes can vary by platform and two reachable extension-negotiation branches are untested, which can cause cross-platform drift and missed runtime edge regressions—pin ZIP metadata and add the missing branch tests.
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/README.md">
<violation number="1" location="packages/sdk-python/README.md:36">
P2: `keep_alive=True` users cannot release the launched session through the returned public `StagehandBrowser`; `close()` only disconnects. Document the available release mechanism (or expose the session ID/release API) so this guidance does not direct callers to an unavailable operation.</violation>
</file>
<file name="packages/sdk-python/src/stagehand/extension_assets.py">
<violation number="1" location="packages/sdk-python/src/stagehand/extension_assets.py:33">
P3: Windows builds produce different archive bytes for identical extension files because `ZipInfo.create_system` inherits the host platform. Pin it to Unix like the server artifact so this upload remains deterministic across developer and CI platforms.</violation>
</file>
<file name="packages/sdk-python/src/stagehand/browserbase_session.py">
<violation number="1" location="packages/sdk-python/src/stagehand/browserbase_session.py:154">
P2: A failed Browserbase release or extension deletion cannot be retried through the returned browser handle: later `handle.close()` calls reuse the failed close task instead of invoking this method. Keep the source/handle closable after a failed cleanup, or retry cleanup internally, so transient failures do not leave owned sessions/extensions orphaned.</violation>
<violation number="2" location="packages/sdk-python/src/stagehand/browserbase_session.py:198">
P1: Custom agent: **Exception and error message sanitization**
Extension-upload failures raise a generic `RuntimeError` and chain the raw upstream Browserbase SDK exception (`from error`), which can leak sensitive request details (API keys, URLs) in user-facing tracebacks. The file already defines `BrowserbaseSessionError` and uses it—along with `from None` to suppress upstream causes—for the analogous session create/retrieve failures just below. The empty-extension-ID check a few lines later also raises a generic `RuntimeError` instead of the typed error. Switch both to `BrowserbaseSessionError(...)` and suppress the SDK cause with `from None` on the upload path so messages stay sanitized and typed.</violation>
</file>
<file name="packages/sdk-python/tests/test_cdp_client.py">
<violation number="1" location="packages/sdk-python/tests/test_cdp_client.py:430">
P3: The new preloaded-extension discovery has good test coverage, but two reachable branches are untested: a candidate whose runtime marker makes `_negotiate_runtime` report incompatible (e.g. a non-Stagehand extension whose service worker URL happens to contain `service-worker.js`, or a protocol-version mismatch), and the `_extension_id_from_url` case that returns `None`. The incompatible-candidate path is the primary reason the discovery loop detaches-and-retries, so a focused test that returns an incompatible marker (then a compatible one) would lock in that behavior and prevent drift from the TS/Go path. Consider adding it alongside the existing stale/missing-title tests.</violation>
</file>
<file name="packages/sdk-python/tests/test_browserbase_session.py">
<violation number="1" location="packages/sdk-python/tests/test_browserbase_session.py:339">
P2: Custom agent: **Exception and error message sanitization**
The close() path re-raises the raw Browserbase SDK exception from session release and owned-extension deletion unchanged (`raise release_error` / `raise extension_error` in browserbase_session.py), so a failure surfaces the SDK's original, unsanitized message — which can carry the session/extension ID or upstream response body — to the caller. This is inconsistent with the create/connect paths in the same PR, which wrap failures in the typed BrowserbaseSessionError with a sanitized message and `__cause__ = None`. The test locks in this behavior with `assert raised.value is release_error`. Consider wrapping the release/delete failures in a sanitized BrowserbaseSessionError (without retaining the upstream cause) to match the sanitization contract the rest of this PR establishes, while still tracking whether release/delete succeeded so the retry-on-next-close behavior is preserved.</violation>
</file>
Architecture diagram
sequenceDiagram
participant Client as SDK Client
participant BBrowser as BrowserbaseBrowser
participant BSession as _BrowserbaseSessionClient
participant BAPI as _OfficialBrowserbaseAPI
participant BSrv as Browserbase Cloud API
participant CDP as CDPClient
Note over Client,CDP: Browserbase Session Launch Flow
Client->>BBrowser: launch(api_key, region, keep_alive, user_metadata)
BBrowser->>BBrowser: Validate inputs (api_key, extension_id)
BBrowser->>BBrowser: Build BrowserbaseSessionCreateParams
BBrowser->>BSession: create_session(options)
alt Caller provided extension_id
BSession->>BSession: Skip extension upload
else No caller extension
BSession->>BAPI: upload_extension(archive)
BAPI->>BSrv: POST /extensions (stagehand-extension.zip)
BSrv-->>BAPI: extension.id
BAPI-->>BSession: uploaded_extension_id
BSession->>BSession: Store owned_extension_id
end
BSession->>BSession: Merge user_metadata with {"stagehand": "true", "stagehand_sdk_language": "python"}
BSession->>BAPI: create_session(options, user_metadata, extension_id)
BAPI->>BSrv: POST /sessions (with api_timeout, camelCase fingerprint)
BSrv-->>BAPI: {id, connect_url}
BAPI-->>BSession: (session_id, cdp_url)
alt Empty session_id or cdp_url
BSession->>BAPI: delete_extension(owned_extension_id)
BSession-->>BBrowser: BrowserbaseSessionError
end
BSession-->>BBrowser: _OwnedBrowserbaseSession(session_id, cdp_url, close_callback)
BBrowser->>CDP: connect(cdp_url, preloaded_extension=True)
Note over CDP: Discover preloaded Stagehand service worker
loop Poll every 100ms until timeout
CDP->>BSrv: Target.getTargets
BSrv-->>CDP: [targetInfos]
alt Found service_worker with chrome-extension:// URL
CDP->>BSrv: Target.attachToTarget(targetId)
BSrv-->>CDP: sessionId
CDP->>BSrv: Runtime.evaluate (readiness check)
BSrv-->>CDP: {result: {value: {hasReceiver: true, marker: ...}}}
Note over CDP: Worker ready - keep attached
CDP-->>BBrowser: ServiceWorkerInfo + session_id
else Incompatible worker (missing title, no receiver)
CDP->>BSrv: Target.detachFromTarget(sessionId)
CDP->>CDP: Continue polling
end
end
CDP-->>BBrowser: Connected with preloaded extension
BBrowser-->>Client: StagehandBrowser
Note over Client,CDP: Session Close / Cleanup (keep_alive=False)
Client->>BBrowser: close()
BBrowser->>BSession: close()
BSession->>BSession: Acquire _close_lock
alt Session not yet released
BSession->>BAPI: release_session(session_id)
BAPI->>BSrv: PATCH /sessions/{id} (status=REQUEST_RELEASE)
BSrv-->>BAPI: Success
BSession->>BSession: Mark released
else Already released
BSession->>BSession: Skip (idempotent)
end
alt Has owned extension and not yet deleted
BSession->>BAPI: delete_extension(extension_id)
BAPI->>BSrv: DELETE /extensions/{id} (Content-Type omitted)
BSrv-->>BAPI: Success
BSession->>BSession: Mark deleted
else No owned extension or already deleted
BSession->>BSession: Skip (idempotent)
end
alt Release error occurred
BSession-->>BBrowser: raise release_error (takes precedence)
else Extension error occurred
BSession-->>BBrowser: raise extension_error
end
Note over Client,CDP: Keep-alive path (keep_alive=True)
Client->>BBrowser: close()
BBrowser->>BSession: close()
BSession->>BSession: _session_released remains False
BSession->>BSession: No release call made
BSession-->>BBrowser: Success (session persists in Browserbase)
Note over Client,CDP: Connect to Existing Session
Client->>BBrowser: connect(api_key, session_id, extension_id)
BBrowser->>BSession: connect_session(session_id)
BSession->>BAPI: retrieve_session(session_id)
BAPI->>BSrv: GET /sessions/{id}
BSrv-->>BAPI: {id, connect_url, region}
BAPI-->>BSession: (id, connect_url, region)
alt connect_url is empty
BSession-->>BBrowser: BrowserbaseSessionError: not available for connection
end
BSession-->>BBrowser: _BrowserbaseSessionConnection(cdp_url, region)
alt Caller provided extension_id
BBrowser->>CDP: connect(cdp_url, extension_id="caller-extension", preloaded_extension=False)
CDP->>CDP: Skip preloaded discovery, use caller extension
else No extension_id
BBrowser->>CDP: connect(cdp_url, preloaded_extension=True)
CDP->>CDP: Discover preloaded extension (same as launch)
end
CDP-->>BBrowser: Connected
BBrowser-->>Client: StagehandBrowser (no ownership, no close side effects)
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
7 issues found across 12 files
Confidence score: 2/5
- In
packages/sdk-python/src/stagehand/browserbase_session.py, the extension-upload failure path raises a genericRuntimeErrorand chains upstream SDK details, which can leak raw internals and break expected typed error handling for callers — raiseBrowserbaseSessionErrorwith sanitized messages and avoid exposing raw upstream exception text. - In
packages/sdk-python/src/stagehand/cdp_client.py(_wait_for_preloaded_service_worker), the newTimeoutErrorincludes raw target URLs fromTarget.getTargets, creating a user-visible data exposure risk in logs and error surfaces — redact or omit target URLs in timeout messages. - In
packages/sdk-python/src/stagehand/browserbase_session.pyandpackages/sdk-python/src/stagehand/browser.py, failure paths around release/delete/launch pluskeep_alive=Truecan orphan sessions or owned extensions, so retries and laterclose()calls may not actually clean up cloud resources — preserve cleanup ownership until launch succeeds and keep a retryable close path for transient teardown failures. - In
packages/sdk-python/src/stagehand/browser.py, constructingBrowserbaseRegion(None)when connect URL has no region causes premature failure before CDP attach, and current tests inpackages/sdk-python/tests/test_browserbase_session.pyreinforce conflicting exception behavior — treat missing region asNoneand update tests to assert sanitized, typed exceptions instead of raw error details.
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:510">
P1: Custom agent: **Exception and error message sanitization**
New `TimeoutError` in `_wait_for_preloaded_service_worker` includes raw browser target URLs from the CDP `Target.getTargets` response in the exception message. Browser target URLs can contain sensitive query-string tokens, session identifiers, or signed URL parameters. The PR's own `browserbase_session.py` follows a sanitization pattern using `BrowserbaseSessionError` with generic messages and `from None` to suppress upstream details — the new CDP client method should align with the same principle. Removing the raw URL dump from the error message prevents leaking potentially sensitive browsing state to end users.</violation>
</file>
<file name="packages/sdk-python/src/stagehand/browserbase_session.py">
<violation number="1" location="packages/sdk-python/src/stagehand/browserbase_session.py:159">
P1: A transient release or extension-delete failure leaves a launched Browserbase session or owned extension orphaned because subsequent public `browser.close()` calls only replay the cached failure. Preserve a retry path in the browser/source close wrapper (or reset its closed task/state after callback failure) so this method's failed-step retry semantics are reachable.</violation>
<violation number="2" location="packages/sdk-python/src/stagehand/browserbase_session.py:198">
P1: Custom agent: **Exception and error message sanitization**
The extension upload failure path raises generic `RuntimeError` instead of the file's typed `BrowserbaseSessionError`, and it chains the upstream SDK exception via `from error`, which can leak sensitive request/response details in tracebacks. The empty-extension-ID check right after also uses `RuntimeError`. For consistency with the error sanitization rule and the rest of this file, use `BrowserbaseSessionError` with `from None` in both places to prevent leaking upstream exception details.</violation>
</file>
<file name="packages/sdk-python/tests/test_browserbase_session.py">
<violation number="1" location="packages/sdk-python/tests/test_browserbase_session.py:339">
P2: Custom agent: **Exception and error message sanitization**
The new tests lock in exception behavior that conflicts with our exception-sanitization rule. During `browserbase_session.create_session`, the extension-upload failure is raised as a generic `RuntimeError` with the upstream SDK error retained as its cause (`raise RuntimeError(...) from error`), and `_OwnedBrowserbaseSession.close()` re-raises the raw exception from `release_session`/`delete_extension` untouched. Both are user-reachable paths (session creation and cleanup), and rule 31c8a33a requires surfaced errors to be typed, sanitized classes that never reflect upstream causes which could expose sensitive details. Consider wrapping the close() release/delete failures and the upload failure in a sanitized `BrowserbaseSessionError` (as already done for create/retrieve via `from None`), suppressing the upstream cause so the raw SDK error never reaches callers. The tests currently assert the unsanitized propagation (`assert raised.value is release_error`, `RuntimeError` match), so they should be updated to assert the sanitized behavior instead.</violation>
</file>
<file name="packages/sdk-python/src/stagehand/browser.py">
<violation number="1" location="packages/sdk-python/src/stagehand/browser.py:472">
P2: When `browserbase.launch(api_key=..., keep_alive=True)` is used without a caller-supplied extension, the SDK auto-uploads and owns a Stagehand extension, but `keep_alive=True` makes the handle's `close()` only disconnect (owns_source is False), so `_OwnedBrowserbaseSession.close()` never runs and the uploaded extension is never deleted. The session itself is intentionally released out of band per the README, but nothing releases the registered extension at that point, so each keep-alive session leaves an orphaned extension on the account. Consider tracking the owned extension and deleting it when the session is released out of band, or documenting this extension-retention tradeoff explicitly next to the keep_alive handling so it is a conscious decision.</violation>
<violation number="2" location="packages/sdk-python/src/stagehand/browser.py:472">
P2: `keep_alive=True` leaks a newly created session when setup fails before `launch()` returns. Keep cleanup ownership through connection setup, then suppress release only for a successfully returned handle.</violation>
<violation number="3" location="packages/sdk-python/src/stagehand/browser.py:507">
P2: Sessions with a connect URL but no returned region fail before CDP attachment because `BrowserbaseRegion(None)` raises. Preserve a missing region as `None` before constructing `BrowserbaseRegion`.</violation>
</file>
Architecture diagram
sequenceDiagram
participant User as User Code
participant BB as BrowserbaseBrowser
participant Ext as extension_assets.py
participant Client as _BrowserbaseSessionClient
participant API as _OfficialBrowserbaseAPI
participant SDK as browserbase SDK (AsyncBrowserbase)
participant CDP as CDPClient
participant WS as WebSocket / Browser
Note over User,WS: NEW: Browserbase Session Launch Flow
User->>BB: browserbase.launch(api_key, region, ...)
BB->>BB: Validate params (api_key, extension_id not empty)
alt Caller provides extension_id (top-level or browser_settings)
BB->>Client: create_session(options) [no upload]
else No caller extension_id
BB->>Ext: build_extension_archive()
Ext-->>BB: deterministic zip bytes
BB->>Client: create_session(options)
Client->>API: upload_extension(archive)
API->>SDK: extensions.create(file=...)
SDK-->>API: extension.id
API-->>Client: uploaded_extension_id
end
Client->>Client: Merge user_metadata with {"stagehand":"true","stagehand_sdk_language":"python"}
Client->>API: create_session(options, user_metadata, extension_id)
API->>SDK: sessions.create(**kwargs)
Note over API,SDK: kwargs include api_timeout, camelCase fingerprint
alt Create fails
SDK-->>API: Exception
API-->>Client: Exception
Client->>Client: _delete_extension_best_effort(owned_extension_id)
Client-->>BB: BrowserbaseSessionError
BB-->>User: Error
else Create succeeds
SDK-->>API: session (id, connect_url)
API-->>Client: (session_id, cdp_url)
Client-->>BB: _OwnedBrowserbaseSession
end
BB->>CDP: connect(preloaded_extension=True, worker_init_metadata)
Note over CDP,WS: NEW: Preloaded Extension Discovery
loop Poll every 100ms until timeout
CDP->>WS: Target.getTargets
WS-->>CDP: targetInfos list
alt Found service_worker with chrome-extension URL
CDP->>WS: Target.attachToTarget(targetId)
WS-->>CDP: sessionId
CDP->>WS: Runtime.evaluate (readiness check)
WS-->>CDP: result value
alt Worker ready and compatible
Note over CDP: Keep attached, return worker
else Worker stale, incompatible, or error
CDP->>WS: Target.detachFromTarget(sessionId)
Note over CDP: Continue polling
end
end
end
alt Discovery timeout
CDP-->>BB: TimeoutError
BB->>Client: session.close() (release session + delete owned extension)
BB-->>User: Error
else Discovery succeeds
CDP-->>BB: ServiceWorkerInfo (extension_id from URL)
BB-->>User: StagehandBrowser
end
Note over User,WS: Browser Handle Close Flow
User->>BB: close()
BB->>BB: Check keep_alive flag
alt keep_alive=True
BB->>CDP: close() (just disconnect WebSocket)
else keep_alive=False (default)
BB->>Client: session.close()
Client->>Client: Lock acquired
alt Session not yet released
Client->>API: release_session(session_id)
API->>SDK: sessions.update(status="REQUEST_RELEASE")
end
alt Owned extension not yet deleted
Client->>API: delete_extension(extension_id)
API->>SDK: extensions.delete(id, Content-Type=omit)
end
alt Any release/delete error
Client->>Client: Collect error, continue cleanup
Client-->>BB: raise release_error (takes precedence)
end
Client->>CDP: close() (disconnect)
end
Note over User,WS: Browserbase Connect Flow (no ownership)
User->>BB: browserbase.connect(api_key, session_id, extension_id?)
BB->>Client: connect_session(session_id)
Client->>API: retrieve_session(session_id)
API->>SDK: sessions.retrieve(session_id)
SDK-->>API: session (id, connect_url, region)
API-->>Client: (id, connect_url, region)
alt connect_url empty or invalid
Client-->>BB: BrowserbaseSessionError
BB-->>User: Error
else Valid connection
Client-->>BB: _BrowserbaseSessionConnection
BB->>CDP: connect(preloaded_extension=(extension_id is None), extension_id=...)
CDP-->>BB: StagehandBrowser (no close_callback, never releases session)
BB-->>User: StagehandBrowser
end
User->>BB: close() (connect case)
BB->>CDP: close() (disconnect only, no session release)
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…alive extension retention
There was a problem hiding this comment.
8 issues found across 12 files
Confidence score: 2/5
- In
packages/sdk-python/src/stagehand/browserbase_session.pyandpackages/sdk-python/src/stagehand/cdp_client.py, multiple error paths surface raw upstream exceptions or full observed target URLs, which can leak Browserbase/CDP request details into logs and user-visible traces. Wrap and sanitize these failures asBrowserbaseSessionError/safeTimeoutErrormessages and raise fromNoneto suppress sensitive causes. close()handling acrosspackages/sdk-python/src/stagehand/browserbase_session.pyandpackages/sdk-python/src/stagehand/browser.pyis not reliably retryable after transient release/delete failures, so owned sessions or extensions can remain allocated. Keep close callbacks and resolved browser state retry-capable so callers can re-attempt cleanup after temporary SDK errors.- Cancellation during launch in
packages/sdk-python/src/stagehand/browserbase_session.pycan race with in-flight session creation and extension upload, allowing resources to be created but never released. Preserve and await shielded create/upload tasks on cancellation, then release returned IDs before propagating cancellation. packages/sdk-python/src/stagehand/browser.pystill chains upstream upload exceptions on launch failure, creating another path for internal request/response leakage and inconsistent error semantics. Normalize this path to sanitizedBrowserbaseSessionErrorbehavior used elsewhere for safer, predictable failures.
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:510">
P1: Custom agent: **Exception and error message sanitization**
The `TimeoutError` in the new `_wait_for_preloaded_service_worker` method includes an `observed` string that dumps full CDP target URLs for every browser target (page, iframe, worker, etc.) without sanitization. Page URLs can contain sensitive query-string parameters such as auth tokens, session IDs, or signed URLs. Per Rule 2, error messages must be sanitized and must not reflect secrets or sensitive URLs. Consider filtering the `observed` list to only service_worker targets (the relevant type for this timeout) or stripping query parameters from any URLs included in the message.</violation>
</file>
<file name="packages/sdk-python/src/stagehand/browserbase_session.py">
<violation number="1" location="packages/sdk-python/src/stagehand/browserbase_session.py:158">
P2: Transient Browserbase cleanup failures cannot be retried through the public browser handle, leaving sessions/extensions allocated until external cleanup. Keep the source/browser close path retryable when this callback raises so `_OwnedBrowserbaseSession.close()` can fulfill its retry logic.</violation>
<violation number="2" location="packages/sdk-python/src/stagehand/browserbase_session.py:164">
P1: Custom agent: **Exception and error message sanitization**
The extension upload failure path raises a generic `RuntimeError` and chains the upstream SDK exception with `from error`, which can leak sensitive Browserbase details (API keys, project IDs, etc.). The file's established convention - followed by every other user-facing error path - is to raise `BrowserbaseSessionError` with `from None` to sanitize the cause. Likewise, the empty-extension-ID guard should use the typed error class for consistency.</violation>
<violation number="3" location="packages/sdk-python/src/stagehand/browserbase_session.py:179">
P1: Custom agent: **Exception and error message sanitization**
`close()` re-raises raw upstream SDK exceptions from session release and extension deletion instead of wrapping them in `BrowserbaseSessionError`, which leaks unsanitized error messages that may contain sensitive details like API keys or CDP URLs. The `create_session` and `connect_session` methods in the same file already wrap failures in a sanitized `BrowserbaseSessionError(...)` with `from None`; `close()` should follow the same pattern so all user-facing Browserbase error paths are consistently sanitized.</violation>
<violation number="4" location="packages/sdk-python/src/stagehand/browserbase_session.py:200">
P2: Cancelling launch during extension upload can orphan the uploaded extension, which has no automatic expiry. Retain and await a shielded upload task during cancellation so its returned ID can be deleted before propagating cancellation.</violation>
<violation number="5" location="packages/sdk-python/src/stagehand/browserbase_session.py:215">
P2: Cancelling launch while Browserbase session creation is in flight can leave a newly created session running until its timeout. Preserve the create task's eventual result during cancellation, release its returned ID, then re-raise cancellation.</violation>
</file>
<file name="packages/sdk-python/src/stagehand/browser.py">
<violation number="1" location="packages/sdk-python/src/stagehand/browser.py:474">
P2: Browserbase launch upload failures retain the upstream SDK exception as a traceback cause, which can expose request/response details. Sanitize this failure as `BrowserbaseSessionError` and raise it from `None` like session create/retrieve failures.
(Based on your team's feedback about sanitizing Browserbase session errors.) [FEEDBACK_USED]</violation>
<violation number="2" location="packages/sdk-python/src/stagehand/browser.py:478">
P2: Transient Browserbase release or extension-delete failures cannot be retried through the returned handle, leaving owned resources orphaned. Allow a failed close to invoke `session.close()` again and mark `ResolvedBrowserSource` closed only after its callback succeeds.</violation>
</file>
Architecture diagram
sequenceDiagram
participant Caller as Caller Code
participant Factory as BrowserbaseBrowser
participant Session as _BrowserbaseSessionClient
participant SDK as Browserbase Async SDK
participant API as Browserbase Cloud API
participant CDP as CDPClient
participant SW as Stagehand Extension SW
Note over Caller,SW: launch(): create and own a Browserbase session
Caller->>Factory: launch(api_key, keep_alive, region, ...)
Factory->>Session: create_session(options)
alt no caller extension_id
Session->>SDK: upload_extension(deterministic zip)
SDK->>API: POST /extensions
API-->>SDK: extension id
else caller extension_id present (top-level or browser_settings)
Note over Session: reuse caller extension, no upload/cleanup
end
Session->>SDK: create_session(Stagehand metadata wins, fingerprint camelCase, api_timeout)
SDK->>API: POST /sessions
API-->>SDK: session id + connect_url
alt creation failed or empty session id/connect_url
Session->>SDK: delete owned extension (best effort)
Session-->>Factory: BrowserbaseSessionError
end
Session-->>Factory: owned session with cdp_url
Factory->>CDP: connect(cdp_url, preloaded_extension=True, metadata session_id/region)
CDP->>SW: discover/attach service worker, evaluate readiness
alt ready + compatible Stagehand marker
SW-->>CDP: protocol marker
else stale/incompatible candidate
CDP->>SW: detach, poll next candidate
end
CDP-->>Factory: CDPClient attached
Factory-->>Caller: StagehandBrowser
Caller->>Factory: close()
alt keep_alive=False
Factory->>Session: close() releases session + deletes owned extension
Session->>SDK: REQUEST_RELEASE + delete_extension
Note over Session: idempotent per resource, retried on next close
else keep_alive=True
Note over Caller,Factory: close() disconnects only, session/extension retained
end
Note over Caller,SW: connect(): attach to existing session, never release on close
Caller->>Factory: connect(api_key, session_id, connect_timeout_ms, extension_id?)
Factory->>Session: connect_session(session_id)
Session->>SDK: retrieve_session(session_id) via SDK
SDK-->>Session: connect_url + region
alt connect_url missing
Session-->>Factory: BrowserbaseSessionError (not available for connection)
else
Session-->>Factory: unowned connection
Factory->>CDP: connect(cdp_url, extension_id or preloaded_extension=True)
CDP-->>Factory: CDPClient
Factory-->>Caller: StagehandBrowser
end
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…re payload Live validation against the real Browserbase API surfaced the worker rejecting "region": null — the wire schema wants the key omitted. Construct BrowserSessionMetadata with region unset so exclude_unset drops it, matching the TypeScript payload.
|
Live validation run against the production Browserbase API: This also verified the |
# why Ports the merged TypeScript browser-lifecycle stack (#2517–#2523) to the Python SDK. This bottom PR lands the entire new-lifecycle machinery as internal, unexported code so it can be reviewed in isolation while the legacy constructor/`init()` lifecycle stays fully green. # what changed - adds `stagehand/browser.py`: nominal `StagehandBrowser` handle (token-guarded construction, one-time Stagehand claim, idempotent memoized `close()`) - implements internal `local_browser.launch()` / `local_browser.connect()` factories that resolve only after the Stagehand extension service worker is ready - enforces launched-versus-connected ownership (`owns_source = launched and not keep_alive`) with `ExceptionGroup` cleanup semantics mirroring the TS `connectBrowser` - implements local downloads via root-session `Browser.setDownloadBehavior` (previously `NotImplementedError`) - reserves `browserbase.launch()` / `browserbase.connect()` API shape as `NotImplementedError` stubs — the Python SDK has never had Browserbase support; worker metadata is shaped `{api_key, browser: BrowserSessionMetadata}` so a future implementation needs no `Stagehand.create` changes - adds `RPCClient.close(close_transport=False)` so Stagehand can later detach from a browser-owned transport - slims `browser_source.py` to a legacy adapter re-binding the moved Chrome launcher; all existing monkeypatch seams keep working # intentionally not included - no public API change; `__init__.py` exports untouched - no changes to the `Stagehand` constructor, `init()`, or async-with - no changes under `_generated/` (already synced by the merged TS stack) # stack 1. **this PR — browser handle, factories, and transport detach (internal)** 2. #2545 — replace the constructor/`init()` lifecycle with `Stagehand.create` + browser factories 3. #2552 — implement Browserbase sessions # test plan - new `tests/test_browser.py` (374 lines): claim/release, ownership matrix, launch flags, downloads, connect extension routing - `RPCClient` `close_transport=False` coverage; existing legacy suites pass unchanged - full package gates green: `uv sync --locked`, `generate.py --check`, `ruff format --check`, `ruff check`, `ty check`, `pytest`
## Summary Completes the Python port of the browser-lifecycle stack (#2517–#2523) by making `await Stagehand.create(browser=..., ...)` the sole construction path, mirroring the TypeScript end state. - makes `Stagehand()` unconstructible directly (module-private token; `TypeError` points at `Stagehand.create`) - removes `init()`, async-with, the flattened browser constructor kwargs, the `BrowserSource` union models, and `browser_source.py` - exports `local_browser`, `browserbase`, and `StagehandBrowser` from the package root - `stagehand.browser` returns the exact handle passed to `create`; a failed `create` releases the claim so the same handle can be retried - `stagehand.close()` never touches the browser or CDP socket — browser/session lifetime is exclusively `browser.close()` - worker init keeps the exact `StagehandInitParams` wire shape; handle metadata overrides caller `api_key`, and local handles omit `browser` entirely - migrates all 7 examples, README, and the CI wheel-smoke script to launch → create → `stagehand.close()` → `browser.close()` (browser close in the outermost `finally`) - updates the Python ast-grep example-parity patterns to the `Stagehand.create` shape ## Reviewer focus 1. `stagehand.close()` stops the runtime; `browser.close()` owns browser cleanup — under no configuration does Stagehand close the transport or process. 2. Browser acquisition options stay client-side and never cross the RPC boundary. 3. One-shot asyncio semantics: memoized `close()`, claim release under failed `create`, concurrent-close safety. ## Stack - #2544 — browser handle, factories, and transport detach (internal) - **this PR** — replace the constructor/`init()` lifecycle with `Stagehand.create` - #2552 — implement Browserbase sessions ## Verification - full Python package gates green: `generate.py --check`, `ruff format --check`, `ruff check`, `ty check`, `pytest` (rewritten lifecycle suite incl. wire-shape, claim-retry, and concurrent-close tests) - repo `pnpm run test:unit` green (ast-grep example-parity + sdk-parity against the migrated sources) - changeset check passed; wheel smoke exercised by CI `python-wheel-smoke` <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Make `await Stagehand.create(browser=...)` the only way to start the Python client. Adds `local_browser` and `browserbase` factories, moves browser lifetime to the browser handle, and hardens close/cancel and launch validation (incl. Python 3.12’s eager task factory). - **Refactors** - `Stagehand()` is no longer constructible; use `Stagehand.create`. - Removed `init()`, async-with usage, and `BrowserSource` models; deleted `browser_source.py`. `ResolvedBrowserSource` now lives in `browser.py` with dead fields removed. - Exported `local_browser`, `browserbase`, and `StagehandBrowser` from `stagehand`. - `stagehand.browser` returns the exact handle passed to `create`. Failed or canceled `create` releases the claim and detaches the RPC client with `close_transport=False`. - `stagehand.close()` stops the runtime only, is safe under Python 3.12’s eager task factory, and never closes the browser or CDP socket. - Removed `connect_rpc_client`; updated README, examples (incl. `model_gateway.py`), tests, smoke script, and ast-grep rules (Python now requires `Stagehand.create`). - Hardened browser handle: stricter launch validation (viewport pairing, list arg types), preserves explicit viewport when default flags are ignored, and treats a vanished Chrome as exited on close. - **Migration** - Launch a browser: `browser = await local_browser.launch(...)` or `browser = await browserbase.launch(api_key=...)`. - Create the client: `stagehand = await Stagehand.create(browser=browser, ...)`. - Close in order: `await stagehand.close()` then `await browser.close()` (put browser close in the outermost finally). - Remove any `async with Stagehand(...)` and calls to `init()`. <sup>Written for commit 9fdb16d. Summary will update on new commits.</sup> <a href="https://cubic.dev/pr/browserbase/stagehand/pull/2545?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="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
why
#2544 reserved
browserbase.launch/browserbase.connectasNotImplementedErrorstubs. This PR makes them real, using the officialbrowserbasePyPI SDK — the Python counterpart of the TS factories'@browserbasehq/sdkusage.what changed
browserbase>=1.15,<2as a runtime dependency (AsyncBrowserbase), the SDK's first beyond pydantic + websocketsbrowserbase.launch(api_key=..., ...): creates a session, provisioning the packaged Stagehand extension via an in-memory deterministic zip upload; caller-supplied extension IDs (top-level orbrowser_settings) suppress provisioning and cleanupuser_metadatawith{"stagehand": "true", "stagehand_sdk_language": "python"}(Stagehand keys win)keep_alive ?? Falsedecides release-on-close; session release and owned-extension delete are idempotent per resource with retry-on-next-close; release errors take precedencebrowserbase.connect(api_key=..., session_id=...):sessions.retrieve-based, errorsBrowserbase session is not available for connectionwhen no connect URL, never takes release ownershipCDPClient.connect(Browserbase sessions have the extension preinstalled; mirrors the TS/Go service-worker readiness polling){api_key, browser: {session_id, region?}}, overriding the caller'sapi_keyper the TS spread orderBrowserbaseSessionErrormessages; official-SDK calls verified against the installed package (camelCasefingerprintwire mapping,api_timeoutkwarg,Content-Typeomitted on the body-less extension DELETE)examples/caching.py's existingbrowserbase.launchcall now works as written — no example inventory changes (ast-grep parity)stack
init()lifecycle withStagehand.createtest plan
_BrowserbaseAPIseam tests: provisioning/suppression matrix, create-failure and cancellation cleanup, empty-id/URL handling, close idempotence + retry, connect paths, kwargs wire-shape tests (fingerprint camelCase,api_timeout), preloaded-extension CDP discovery testsuv lock --check,generate.py --check,ruff format --check,ruff check,ty check,pytest(239 passed), sdist+wheel build, changeset + python-version sync checks, and everyjust check/just testrecipe command run manuallyStagehand.create->stagehand.close->browser.close-> sessionCOMPLETED, uploaded extension deleted (404 on retrieve). Caught and fixed one wire bug:region: nullwas rejected by the worker; the key is now omitted when unset (87369b4)Summary by cubic
Adds Browserbase session support to the Python browser factories via
browserbase.launch(...)andbrowserbase.connect(...). Improves preloaded extension discovery, deterministic extension packaging, and omits an unset Browserbase region from thestagehand.initpayload.New Features
browserbase.launch(api_key, ...)creates a session, uploads the Stagehand extension unless anextension_idis given, respectskeep_alive, and sets worker metadata{api_key, session_id, region}.browserbase.connect(api_key, session_id, ...)attaches to an existing session; uses a callerextension_idor the preloaded extension; never releases the session.browserbase>=1.15,<2as a runtime dependency.Bug Fixes
api_timeout; reject emptyextension_id(top-level or nested); omitContent-Typeon extension delete; clearer errors.regionfrom thestagehand.initpayload (worker schema rejectsregion: null).keep_alivebehavior and extension retention.Written for commit 8234a67. Summary will update on new commits.