Conversation
Bind browser subresource calls to a browser session's base_url and expose raw HTTP through request and stream helpers so metro-routed access feels like normal httpx usage. Made-with: Cursor
Prevent browser-scoped raw HTTP helpers from letting user params override internal routing query keys, and clean up wording around browser session base_url routing. Made-with: Cursor
Keep the browser-scoped request helpers aligned with repo linting and reserve internal raw-request query keys without exposing implementation details. Made-with: Cursor
Keep the browser-scoped test file aligned with the repo lint configuration so the follow-up typing fixes pass CI. Made-with: Cursor
Tighten browser-scoped helper typing and test casts so the Python SDK passes the repository's lint and pyright checks cleanly. Made-with: Cursor
Replace the handwritten Python browser-scoped façade with deterministic generated bindings from the browser resource graph, and enforce regeneration during lint. Made-with: Cursor
Keep the browser-scoped Python generator compatible with the repo lint pipeline by suppressing strict pyright diagnostics that are not meaningful for the AST-walking build script. Made-with: Cursor
Keep the Python generator and generated browser-scoped façade aligned with pyright and mypy so the deterministic regeneration path passes the repo lint pipeline. Made-with: Cursor
Sort the generator script imports and keep the deterministic browser-scoped generation path aligned with the repo lint pipeline. Made-with: Cursor
Turn the browser-scoped Python example into a runnable demonstration of both process execution and /curl/raw-backed request and stream usage. Made-with: Cursor
Move browser raw HTTP and direct-to-VM routing onto the main browsers resource so the SDK uses the shared browser route cache instead of a generated wrapper layer. Made-with: Cursor
Remove the public cache priming helpers, keep jwt-required routes, and rename the example and tests so the python browser routing diff stays focused on cache-backed direct-to-VM behavior. Made-with: Cursor
Shorten the browser_routing allowlist field to subresources so the direct-to-VM configuration stays concise while keeping the same routing behavior. Made-with: Cursor
Move the handwritten routing helpers out of the old browser_scoped package, delete the unused browser session clone helper, warm the async browser list cache, and drop the generated type churn from the branch. Made-with: Cursor
Preserve browser routing settings across copy(), skip cache warming for raw response wrappers, and clean up the handwritten routing files so lint can pass on the current branch. Made-with: Cursor
Make the browser routing helpers type-check cleanly in CI, keep copy() signatures aligned with __init__, and avoid cache-warming errors on raw response wrappers. Made-with: Cursor
Encode string request bodies before building raw /curl/raw request options so the browser routing helpers satisfy CI type checks while preserving the public request API. Made-with: Cursor
|
Firetiger deploy monitoring skipped This PR didn't match the auto-monitor filter configured on your GitHub connection:
Reason: This is an automated release PR for the Python SDK with only internal chores, not a change to kernel API endpoints or Temporal workflows. To monitor this PR anyway, reply with |
|
🧪 Testing To try out this version of the SDK: Expires at: Mon, 25 May 2026 16:29:58 GMT |
Remove the public browser routing constructor config and derive direct-to-VM subresource routing from KERNEL_BROWSER_ROUTING_SUBRESOURCES instead, defaulting to curl while keeping the raw request helpers direct to the browser.
Store browser routes under a normalized session ID so cache lookups and deletes stay consistent when route data includes surrounding whitespace. Add a regression test to lock in the normalization behavior. Made-with: Cursor
5a6d7c8 to
cfb54ec
Compare
cfb54ec to
d66ba1b
Compare
Move browser route cache warming into the shared sync and async response hooks so browser metadata endpoints populate the cache consistently, including raw responses. Remove the handwritten browsers resource priming and cover the narrowed sniffing behavior with focused routing tests. Made-with: Cursor
Drop cached browser routes after successful DELETE /browsers/{id} responses so stale direct-to-VM session URLs are not reused. Cover both the success and failure paths with focused browser routing regressions.
Made-with: Cursor
Remove the leftover result/page wrappers from browser create, retrieve, update, and list now that route cache warming lives in the shared response hooks. Keep the actual curl and raw HTTP browser routing surface unchanged. Made-with: Cursor
Keep the browser route cache in sync for pool acquire and release flows so leased sessions can use direct VM routing without resource-specific cache handling. Made-with: Cursor
feat: add browser routing cache
d66ba1b to
c0728c8
Compare
| populate_browser_route_cache_from_value(response.json(), cache=cache) | ||
| except Exception: | ||
| # Ignore malformed JSON in routing cache population. | ||
| return |
There was a problem hiding this comment.
Cache populated from error responses
Low Severity
maybe_populate_browser_route_cache_from_response runs before super()._process_response, and unlike the eviction helper it never checks response.is_success. A 4xx/5xx JSON error body returned from /browsers, /browsers/{id}, or /browser_pools/{id}/acquire is still walked by populate_browser_route_cache_from_value, and any dict nested in the error payload that happens to expose session_id, base_url, and cdp_ws_url will be cached as a live route. Mirroring the is_success guard used for eviction would keep the cache in sync with actual successful allocations.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit c0728c8. Configure here.
| params.update(options.params) | ||
| params["jwt"] = route.jwt | ||
| rewritten.params = params or options.params | ||
| return rewritten |
There was a problem hiding this comment.
Dead fallback in params rewrite
Low Severity
In rewrite_direct_vm_options, params is built fresh as an empty dict, copied from options.params, and then always has jwt assigned, so params is unconditionally truthy at the point of rewritten.params = params or options.params. The or options.params fallback can never be taken and only obscures intent. Dropping the dead branch (or explicitly handling a non-mapping options.params) would make the rewrite easier to reason about.
Reviewed by Cursor Bugbot for commit c0728c8. Configure here.
| params.update(options.params) | ||
| params["jwt"] = route.jwt | ||
| rewritten.params = params or options.params | ||
| return rewritten |
There was a problem hiding this comment.
Implicit rewrite leaks url query override
Low Severity
For the raw HTTP helpers, sanitize_curl_raw_params strips url and jwt from user-supplied params so the target URL and token cannot be overridden. The implicit path in rewrite_direct_vm_options only overwrites jwt but forwards any caller-supplied url query parameter as-is to the direct-VM endpoint. If a subresource on the browser VM ever interprets a url query parameter (today curl/raw does; future subresources might), a caller-supplied value would silently reach the VM instead of being reserved. Sharing the CURL_RAW_RESERVED_QUERY_KEYS filter here would keep the two routing paths consistent.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit c0728c8. Configure here.
| json_data=json, | ||
| timeout=_normalize_timeout(timeout), | ||
| ) | ||
| return cast(httpx.Response, parent.request(httpx.Response, options)) |
There was a problem hiding this comment.
Direct-VM request helper silently disables client timeout
High Severity
When callers invoke browsers.request (and the async variant) without specifying timeout, the helper passes _normalize_timeout(not_given) which collapses to None and stores it on FinalRequestOptions.timeout. In _build_request, the client-level self.timeout is only consulted when options.timeout is NotGiven, so the explicit None bypasses the configured client timeout and httpx is handed no timeout at all — requests can hang indefinitely. The stream helper correctly captures parent.timeout before normalizing, but the non-streaming path does not.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit c0728c8. Configure here.
c0728c8 to
e7a5e5e
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
There are 7 total unresolved issues (including 4 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e7a5e5e. Configure here.
| return None | ||
|
|
||
| normalized = session_id.strip() | ||
| return normalized or None |
There was a problem hiding this comment.
Unprotected dict access in pool release eviction
Low Severity
In _session_id_from_browser_pool_release_request, after json.loads succeeds, the code calls body.get("session_id") outside the try block. If the parsed JSON body is anything other than an object (for example a list, string, number, or null), the .get call raises AttributeError, which propagates out of _process_response and surfaces as an unexpected exception to callers, even though the underlying API call itself succeeded.
Reviewed by Cursor Bugbot for commit e7a5e5e. Configure here.
| self._routes.pop(_normalize_session_id(session_id), None) | ||
|
|
||
| def values(self) -> list[BrowserRoute]: | ||
| return list(self._routes.values()) |
There was a problem hiding this comment.
BrowserRouteCache grows unbounded for stale sessions
Low Severity
BrowserRouteCache only evicts entries when the SDK observes a successful DELETE /browsers/{id} or POST /browser_pools/{id}/release. For long-lived Kernel/AsyncKernel instances that create or list many browsers (or whose sessions expire server-side without an explicit delete), entries accumulate forever. There is no size cap, TTL, or LRU eviction, and strip_direct_vm_auth walks the entire cache on every outgoing request.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit e7a5e5e. Configure here.
| return None | ||
|
|
||
| session_id = unquote(match.group(1)).strip() | ||
| return session_id or None |
There was a problem hiding this comment.
Path session ID is double URL-decoded
Low Severity
_session_id_from_browser_delete_path calls unquote(match.group(1)) on a segment extracted from httpx.Request.url.path, but httpx's URL.path is already URL-decoded. Any literal %XX in a session id is decoded twice, so a session id containing % (or that happens to look like a percent-encoded sequence) will be normalized differently from how it was set in the cache and the eviction silently fails to remove the entry.
Reviewed by Cursor Bugbot for commit e7a5e5e. Configure here.


Automated Release PR
0.51.0 (2026-04-25)
Full Changelog: v0.50.0...v0.51.0
Features
Bug Fixes
Chores
Documentation
Refactors
This pull request is managed by Stainless's GitHub App.
The semver version number is based on included commit messages. Alternatively, you can manually set the version number in the title of this pull request.
For a better experience, it is recommended to use either rebase-merge or squash-merge when merging this pull request.
🔗 Stainless website
📚 Read the docs
🙋 Reach out for help or questions
Note
Medium Risk
Medium risk because it changes core client request/response handling to rewrite certain
/browsers/{id}/...calls and manage a shared browser route cache, which could affect request routing and auth header behavior across the SDK.Overview
Bumps the SDK to
0.51.0(manifest,pyproject.toml,_version.py, changelog, spec metadata).Adds browser-scoped routing support: the client now maintains a
BrowserRouteCache, rewrites allowlisted browser subresource requests to go directly to the browser VM using cachedbase_url+jwt, stripsAuthorizationfor those direct calls, and warms/evicts the cache based on successful/browsersand browser pool acquire/release responses.Extends
BrowsersResourcewithbrowsers.request()andbrowsers.stream()(sync/async) helpers that proxy raw HTTP via/curl/rawwhile preventing userparamsfrom overriding reservedurl/jwt, and includes a newexamples/browser_routing.pyplus comprehensive routing/cache tests. Also updates managed auth types to exposebrowser_session_idandlast_auth_check_at(with clarified/deprecated docs).Reviewed by Cursor Bugbot for commit e7a5e5e. Bugbot is set up for automated code reviews on this repo. Configure here.