Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7ca6887
feat: add browser-scoped session client
rgarcia Apr 13, 2026
b2c7aac
fix: reserve internal browser request query params
rgarcia Apr 13, 2026
cfff5b4
fix: type-check browser-scoped helpers
rgarcia Apr 13, 2026
fc34859
chore: fix browser-scoped test import order
rgarcia Apr 13, 2026
8e8dde2
fix: satisfy browser-scoped lint checks
rgarcia Apr 13, 2026
53b17c8
feat: generate browser-scoped resource bindings
rgarcia Apr 13, 2026
0bdf85e
fix: quiet generator-script pyright noise
rgarcia Apr 13, 2026
b410245
fix: satisfy generated browser-scoped type checks
rgarcia Apr 13, 2026
a80716b
chore: keep browser-scoped generator lint clean
rgarcia Apr 13, 2026
ca5d188
docs: flesh out browser-scoped example
rgarcia Apr 21, 2026
dba503e
refactor: drop browser-scoped wrapper clients
rgarcia Apr 22, 2026
de0476f
refactor: simplify browser routing cache
rgarcia Apr 22, 2026
3ae9dab
refactor: rename browser routing subresources config
rgarcia Apr 22, 2026
622f844
refactor: clean up python browser routing diff
rgarcia Apr 22, 2026
694907a
fix: finish python browser routing cleanup
rgarcia Apr 23, 2026
9690923
fix: address python browser routing ci follow-ups
rgarcia Apr 23, 2026
3ce80e7
fix: normalize python browser request string bodies
rgarcia Apr 23, 2026
6c9cdf3
chore(internal): more robust bootstrap script
stainless-app[bot] Apr 23, 2026
0647d5c
refactor: move python browser routing rollout to env
rgarcia Apr 24, 2026
f4c247b
fix: normalize browser route cache session IDs
rgarcia Apr 24, 2026
0ccb507
feat: Expose browser_session_id on managed auth connection
stainless-app[bot] Apr 24, 2026
563de7d
refactor: sniff browser routes in response hooks
rgarcia Apr 24, 2026
a873a18
fix: evict deleted browser routes
rgarcia Apr 24, 2026
02a2f59
refactor: inline browser resource passthrough returns
rgarcia Apr 24, 2026
5328730
fix: sniff browser pool route cache updates
rgarcia Apr 24, 2026
9817c9f
Merge pull request #93 from kernel/raf/browser-scoped-client
rgarcia Apr 24, 2026
7781a3b
feat: Expire stuck IN_PROGRESS managed auth sessions via background w…
stainless-app[bot] Apr 25, 2026
e7a5e5e
release: 0.51.0
stainless-app[bot] Apr 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.50.0"
".": "0.51.0"
}
4 changes: 2 additions & 2 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 112
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-686a9addd4f9356ca26ff3ff04e1a11466d77a412859829075566394922b715d.yml
openapi_spec_hash: 7a9e9c2023400d44bcbfb87b7ec07708
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-a674e3c4c0063942621d1b4e7f67b72f7e240c12dd88564fe16627618ba33dd6.yml
openapi_spec_hash: 8b97c87f0dafe5fc5e5a7365f3687755
config_hash: 08d55086449943a8fec212b870061a3f
49 changes: 49 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,54 @@
# Changelog

## 0.51.0 (2026-04-25)

Full Changelog: [v0.50.0...v0.51.0](https://github.com/kernel/kernel-python-sdk/compare/v0.50.0...v0.51.0)

### Features

* add browser-scoped session client ([7ca6887](https://github.com/kernel/kernel-python-sdk/commit/7ca68877e7011bb83862b7cc810a20d8254ea7dd))
* Expire stuck IN_PROGRESS managed auth sessions via background worker ([7781a3b](https://github.com/kernel/kernel-python-sdk/commit/7781a3b4635ded02dea60adf85878f50f7b7fb27))
* Expose browser_session_id on managed auth connection ([0ccb507](https://github.com/kernel/kernel-python-sdk/commit/0ccb50744032b4c31e0575fa7b06fb20503c8f55))
* generate browser-scoped resource bindings ([53b17c8](https://github.com/kernel/kernel-python-sdk/commit/53b17c8241cc71261d1e96f5929cbd4f05b2064b))


### Bug Fixes

* address python browser routing ci follow-ups ([9690923](https://github.com/kernel/kernel-python-sdk/commit/9690923666cfe07de76267eee050d7743a8bad6f))
* evict deleted browser routes ([a873a18](https://github.com/kernel/kernel-python-sdk/commit/a873a18eba3f36937dc177ab981372e395722f8b))
* finish python browser routing cleanup ([694907a](https://github.com/kernel/kernel-python-sdk/commit/694907ab3419477e7058b85a7365ac4cce941105))
* normalize browser route cache session IDs ([f4c247b](https://github.com/kernel/kernel-python-sdk/commit/f4c247b425680b54d0ce3c7738fb82313bca7918))
* normalize python browser request string bodies ([3ce80e7](https://github.com/kernel/kernel-python-sdk/commit/3ce80e767d373b638ba1c2959bf18bf999629db0))
* quiet generator-script pyright noise ([0bdf85e](https://github.com/kernel/kernel-python-sdk/commit/0bdf85e0c38d4813056b61599273e88c7a64713a))
* reserve internal browser request query params ([b2c7aac](https://github.com/kernel/kernel-python-sdk/commit/b2c7aacac09a1bb7680cf493e9985438b169286c))
* satisfy browser-scoped lint checks ([8e8dde2](https://github.com/kernel/kernel-python-sdk/commit/8e8dde241c8817944baaacd155fe196f200868e8))
* satisfy generated browser-scoped type checks ([b410245](https://github.com/kernel/kernel-python-sdk/commit/b410245e1ad4bf8e29c17c59a5931654567b141f))
* sniff browser pool route cache updates ([5328730](https://github.com/kernel/kernel-python-sdk/commit/532873072f0400029768d1b7cf54b9fb1428ada9))
* type-check browser-scoped helpers ([cfff5b4](https://github.com/kernel/kernel-python-sdk/commit/cfff5b4c3635d327dd1ac0779d4e17e395efbec0))


### Chores

* fix browser-scoped test import order ([fc34859](https://github.com/kernel/kernel-python-sdk/commit/fc34859c4f60f84038b425d9930c512e58134dea))
* **internal:** more robust bootstrap script ([6c9cdf3](https://github.com/kernel/kernel-python-sdk/commit/6c9cdf3ce828fab358c7e060f4e3313408cad257))
* keep browser-scoped generator lint clean ([a80716b](https://github.com/kernel/kernel-python-sdk/commit/a80716b791bf1f707aa7869290c47caefb0d9e27))


### Documentation

* flesh out browser-scoped example ([ca5d188](https://github.com/kernel/kernel-python-sdk/commit/ca5d1884b590634df5623945e9585e0a66228ec3))


### Refactors

* clean up python browser routing diff ([622f844](https://github.com/kernel/kernel-python-sdk/commit/622f8448a8a32f00b41d6e4890bfaf0a9374bd3e))
* drop browser-scoped wrapper clients ([dba503e](https://github.com/kernel/kernel-python-sdk/commit/dba503e832d54aa8d462d3d74b3027f8a9e865b6))
* inline browser resource passthrough returns ([02a2f59](https://github.com/kernel/kernel-python-sdk/commit/02a2f595c7e76ae7f0cea2ec1e88075df3a25be1))
* move python browser routing rollout to env ([0647d5c](https://github.com/kernel/kernel-python-sdk/commit/0647d5cab166e680bcb3436d1b502c3215492400))
* rename browser routing subresources config ([3ae9dab](https://github.com/kernel/kernel-python-sdk/commit/3ae9dab6b841e6f1191cdde073e36696f97feb39))
* simplify browser routing cache ([de0476f](https://github.com/kernel/kernel-python-sdk/commit/de0476fc043df48a58dd4067bb4b3c0fe7a83f0e))
* sniff browser routes in response hooks ([563de7d](https://github.com/kernel/kernel-python-sdk/commit/563de7d0ac8f141320edb060b4671935808e473a))

## 0.50.0 (2026-04-20)

Full Changelog: [v0.49.0...v0.50.0](https://github.com/kernel/kernel-python-sdk/compare/v0.49.0...v0.50.0)
Expand Down
25 changes: 25 additions & 0 deletions examples/browser_routing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Example: direct-to-VM browser routing for process exec and raw HTTP."""

from typing import Any, cast

import httpx

from kernel import Kernel


def main() -> None:
with Kernel() as client:
browsers = cast(Any, client.browsers)
browser = browsers.create(headless=True)
try:
response = cast(httpx.Response, browsers.request(browser.session_id, "GET", "https://example.com"))
print("status", response.status_code)

with browsers.stream(browser.session_id, "GET", "https://example.com") as streamed:
print("streamed-bytes", len(streamed.read()))
finally:
browsers.delete_by_id(browser.session_id)


if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "kernel"
version = "0.50.0"
version = "0.51.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
Expand Down
2 changes: 1 addition & 1 deletion scripts/bootstrap
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ set -e

cd "$(dirname "$0")/.."

if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then
if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then
brew bundle check >/dev/null 2>&1 || {
echo -n "==> Install Homebrew dependencies? (y/N): "
read -r response
Expand Down
89 changes: 88 additions & 1 deletion src/kernel/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

import os
from typing import TYPE_CHECKING, Any, Dict, Mapping, cast
from typing import TYPE_CHECKING, Any, Dict, Type, Mapping, cast
from typing_extensions import Self, Literal, override

import httpx
Expand All @@ -14,13 +14,15 @@
Omit,
Timeout,
NotGiven,
ResponseT,
Transport,
ProxiesTypes,
RequestOptions,
not_given,
)
from ._utils import is_given, get_async_library
from ._compat import cached_property
from ._models import FinalRequestOptions
from ._version import __version__
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
from ._exceptions import KernelError, APIStatusError
Expand All @@ -29,6 +31,15 @@
SyncAPIClient,
AsyncAPIClient,
)
from .lib.browser_routing.routing import (
BrowserRouteCache,
BrowserRoutingConfig,
strip_direct_vm_auth,
rewrite_direct_vm_options,
browser_routing_config_from_env,
maybe_evict_browser_route_from_response,
maybe_populate_browser_route_cache_from_response,
)

if TYPE_CHECKING:
from .resources import (
Expand Down Expand Up @@ -79,8 +90,10 @@
class Kernel(SyncAPIClient):
# client options
api_key: str
browser_route_cache: BrowserRouteCache

_environment: Literal["production", "development"] | NotGiven
_browser_routing: BrowserRoutingConfig

def __init__(
self,
Expand All @@ -105,6 +118,7 @@ def __init__(
# outlining your use-case to help us decide if it should be
# part of our public interface in the future.
_strict_response_validation: bool = False,
_browser_route_cache: BrowserRouteCache | None = None,
) -> None:
"""Construct a new synchronous Kernel client instance.

Expand Down Expand Up @@ -154,6 +168,8 @@ def __init__(
custom_query=default_query,
_strict_response_validation=_strict_response_validation,
)
self.browser_route_cache = _browser_route_cache or BrowserRouteCache()
self._browser_routing = browser_routing_config_from_env()

@cached_property
def deployments(self) -> DeploymentsResource:
Expand Down Expand Up @@ -266,6 +282,37 @@ def default_headers(self) -> dict[str, str | Omit]:
**self._custom_headers,
}

@override
def _prepare_options(self, options: Any) -> Any:
options = cast(Any, super()._prepare_options(options))
return rewrite_direct_vm_options(options, cache=self.browser_route_cache, config=self._browser_routing)

@override
def _prepare_request(self, request: httpx.Request) -> None:
strip_direct_vm_auth(request, cache=self.browser_route_cache)

@override
def _process_response(
self,
*,
cast_to: Type[ResponseT],
options: FinalRequestOptions,
response: httpx.Response,
stream: bool,
stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None,
retries_taken: int = 0,
) -> ResponseT:
maybe_populate_browser_route_cache_from_response(response, cache=self.browser_route_cache)
maybe_evict_browser_route_from_response(response, cache=self.browser_route_cache)
return super()._process_response(
cast_to=cast_to,
options=options,
response=response,
stream=stream,
stream_cls=stream_cls,
retries_taken=retries_taken,
)

def copy(
self,
*,
Expand All @@ -279,6 +326,7 @@ def copy(
set_default_headers: Mapping[str, str] | None = None,
default_query: Mapping[str, object] | None = None,
set_default_query: Mapping[str, object] | None = None,
_browser_route_cache: BrowserRouteCache | None = None,
_extra_kwargs: Mapping[str, Any] = {},
) -> Self:
"""
Expand Down Expand Up @@ -312,6 +360,7 @@ def copy(
max_retries=max_retries if is_given(max_retries) else self.max_retries,
default_headers=headers,
default_query=params,
_browser_route_cache=_browser_route_cache or self.browser_route_cache,
**_extra_kwargs,
)

Expand Down Expand Up @@ -356,8 +405,10 @@ def _make_status_error(
class AsyncKernel(AsyncAPIClient):
# client options
api_key: str
browser_route_cache: BrowserRouteCache

_environment: Literal["production", "development"] | NotGiven
_browser_routing: BrowserRoutingConfig

def __init__(
self,
Expand All @@ -382,6 +433,7 @@ def __init__(
# outlining your use-case to help us decide if it should be
# part of our public interface in the future.
_strict_response_validation: bool = False,
_browser_route_cache: BrowserRouteCache | None = None,
) -> None:
"""Construct a new async AsyncKernel client instance.

Expand Down Expand Up @@ -431,6 +483,8 @@ def __init__(
custom_query=default_query,
_strict_response_validation=_strict_response_validation,
)
self.browser_route_cache = _browser_route_cache or BrowserRouteCache()
self._browser_routing = browser_routing_config_from_env()

@cached_property
def deployments(self) -> AsyncDeploymentsResource:
Expand Down Expand Up @@ -543,6 +597,37 @@ def default_headers(self) -> dict[str, str | Omit]:
**self._custom_headers,
}

@override
async def _prepare_options(self, options: Any) -> Any:
options = cast(Any, await super()._prepare_options(options))
return rewrite_direct_vm_options(options, cache=self.browser_route_cache, config=self._browser_routing)

@override
async def _prepare_request(self, request: httpx.Request) -> None:
strip_direct_vm_auth(request, cache=self.browser_route_cache)

@override
async def _process_response(
self,
*,
cast_to: Type[ResponseT],
options: FinalRequestOptions,
response: httpx.Response,
stream: bool,
stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None,
retries_taken: int = 0,
) -> ResponseT:
maybe_populate_browser_route_cache_from_response(response, cache=self.browser_route_cache)
maybe_evict_browser_route_from_response(response, cache=self.browser_route_cache)
return await super()._process_response(
cast_to=cast_to,
options=options,
response=response,
stream=stream,
stream_cls=stream_cls,
retries_taken=retries_taken,
)

def copy(
self,
*,
Expand All @@ -556,6 +641,7 @@ def copy(
set_default_headers: Mapping[str, str] | None = None,
default_query: Mapping[str, object] | None = None,
set_default_query: Mapping[str, object] | None = None,
_browser_route_cache: BrowserRouteCache | None = None,
_extra_kwargs: Mapping[str, Any] = {},
) -> Self:
"""
Expand Down Expand Up @@ -589,6 +675,7 @@ def copy(
max_retries=max_retries if is_given(max_retries) else self.max_retries,
default_headers=headers,
default_query=params,
_browser_route_cache=_browser_route_cache or self.browser_route_cache,
**_extra_kwargs,
)

Expand Down
2 changes: 1 addition & 1 deletion src/kernel/_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

__title__ = "kernel"
__version__ = "0.50.0" # x-release-please-version
__version__ = "0.51.0" # x-release-please-version
3 changes: 3 additions & 0 deletions src/kernel/lib/browser_routing/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from __future__ import annotations

__all__: list[str] = []
Loading