Skip to content

Commit 0647d5c

Browse files
committed
refactor: move python browser routing rollout to env
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.
1 parent 3ce80e7 commit 0647d5c

5 files changed

Lines changed: 34 additions & 27 deletions

File tree

examples/browser_routing.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,14 @@
44

55
import httpx
66

7-
from kernel import Kernel, BrowserRoutingConfig
7+
from kernel import Kernel
88

99

1010
def main() -> None:
11-
with Kernel(browser_routing=BrowserRoutingConfig(enabled=True, subresources=("process",))) as client:
11+
with Kernel() as client:
1212
browsers = cast(Any, client.browsers)
1313
browser = browsers.create(headless=True)
1414
try:
15-
browsers.process.exec(browser.session_id, command="uname", args=["-a"])
16-
1715
response = cast(httpx.Response, browsers.request(browser.session_id, "GET", "https://example.com"))
1816
print("status", response.status_code)
1917

src/kernel/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
AsyncKernel,
1717
AsyncStream,
1818
RequestOptions,
19-
BrowserRoutingConfig,
2019
)
2120
from ._models import BaseModel
2221
from ._version import __title__, __version__
@@ -80,7 +79,6 @@
8079
"RateLimitError",
8180
"InternalServerError",
8281
"Timeout",
83-
"BrowserRoutingConfig",
8482
"RequestOptions",
8583
"Client",
8684
"AsyncClient",

src/kernel/_client.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
BrowserRoutingConfig,
3535
strip_direct_vm_auth,
3636
rewrite_direct_vm_options,
37+
browser_routing_config_from_env,
3738
)
3839

3940
if TYPE_CHECKING:
@@ -70,7 +71,6 @@
7071
"Transport",
7172
"ProxiesTypes",
7273
"RequestOptions",
73-
"BrowserRoutingConfig",
7474
"Kernel",
7575
"AsyncKernel",
7676
"Client",
@@ -89,7 +89,7 @@ class Kernel(SyncAPIClient):
8989
browser_route_cache: BrowserRouteCache
9090

9191
_environment: Literal["production", "development"] | NotGiven
92-
_browser_routing: BrowserRoutingConfig | None
92+
_browser_routing: BrowserRoutingConfig
9393

9494
def __init__(
9595
self,
@@ -101,7 +101,6 @@ def __init__(
101101
max_retries: int = DEFAULT_MAX_RETRIES,
102102
default_headers: Mapping[str, str] | None = None,
103103
default_query: Mapping[str, object] | None = None,
104-
browser_routing: BrowserRoutingConfig | None = None,
105104
# Configure a custom httpx client.
106105
# We provide a `DefaultHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`.
107106
# See the [httpx documentation](https://www.python-httpx.org/api/#client) for more details.
@@ -166,7 +165,7 @@ def __init__(
166165
_strict_response_validation=_strict_response_validation,
167166
)
168167
self.browser_route_cache = _browser_route_cache or BrowserRouteCache()
169-
self._browser_routing = browser_routing
168+
self._browser_routing = browser_routing_config_from_env()
170169

171170
@cached_property
172171
def deployments(self) -> DeploymentsResource:
@@ -301,7 +300,6 @@ def copy(
301300
set_default_headers: Mapping[str, str] | None = None,
302301
default_query: Mapping[str, object] | None = None,
303302
set_default_query: Mapping[str, object] | None = None,
304-
browser_routing: BrowserRoutingConfig | None = None,
305303
_browser_route_cache: BrowserRouteCache | None = None,
306304
_extra_kwargs: Mapping[str, Any] = {},
307305
) -> Self:
@@ -336,7 +334,6 @@ def copy(
336334
max_retries=max_retries if is_given(max_retries) else self.max_retries,
337335
default_headers=headers,
338336
default_query=params,
339-
browser_routing=browser_routing if browser_routing is not None else self._browser_routing,
340337
_browser_route_cache=_browser_route_cache or self.browser_route_cache,
341338
**_extra_kwargs,
342339
)
@@ -385,7 +382,7 @@ class AsyncKernel(AsyncAPIClient):
385382
browser_route_cache: BrowserRouteCache
386383

387384
_environment: Literal["production", "development"] | NotGiven
388-
_browser_routing: BrowserRoutingConfig | None
385+
_browser_routing: BrowserRoutingConfig
389386

390387
def __init__(
391388
self,
@@ -397,7 +394,6 @@ def __init__(
397394
max_retries: int = DEFAULT_MAX_RETRIES,
398395
default_headers: Mapping[str, str] | None = None,
399396
default_query: Mapping[str, object] | None = None,
400-
browser_routing: BrowserRoutingConfig | None = None,
401397
# Configure a custom httpx client.
402398
# We provide a `DefaultAsyncHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`.
403399
# See the [httpx documentation](https://www.python-httpx.org/api/#asyncclient) for more details.
@@ -462,7 +458,7 @@ def __init__(
462458
_strict_response_validation=_strict_response_validation,
463459
)
464460
self.browser_route_cache = _browser_route_cache or BrowserRouteCache()
465-
self._browser_routing = browser_routing
461+
self._browser_routing = browser_routing_config_from_env()
466462

467463
@cached_property
468464
def deployments(self) -> AsyncDeploymentsResource:
@@ -597,7 +593,6 @@ def copy(
597593
set_default_headers: Mapping[str, str] | None = None,
598594
default_query: Mapping[str, object] | None = None,
599595
set_default_query: Mapping[str, object] | None = None,
600-
browser_routing: BrowserRoutingConfig | None = None,
601596
_browser_route_cache: BrowserRouteCache | None = None,
602597
_extra_kwargs: Mapping[str, Any] = {},
603598
) -> Self:
@@ -632,7 +627,6 @@ def copy(
632627
max_retries=max_retries if is_given(max_retries) else self.max_retries,
633628
default_headers=headers,
634629
default_query=params,
635-
browser_routing=browser_routing if browser_routing is not None else self._browser_routing,
636630
_browser_route_cache=_browser_route_cache or self.browser_route_cache,
637631
**_extra_kwargs,
638632
)

src/kernel/lib/browser_routing/routing.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import os
34
from typing import Any
45
from dataclasses import field, dataclass
56

@@ -24,10 +25,19 @@ class BrowserRoute:
2425

2526
@dataclass
2627
class BrowserRoutingConfig:
27-
enabled: bool = False
2828
subresources: tuple[str, ...] = field(default_factory=tuple)
2929

3030

31+
def browser_routing_config_from_env() -> BrowserRoutingConfig:
32+
raw = os.environ.get("KERNEL_BROWSER_ROUTING_SUBRESOURCES")
33+
if raw is None:
34+
return BrowserRoutingConfig(subresources=("curl",))
35+
if raw.strip() == "":
36+
return BrowserRoutingConfig()
37+
38+
return BrowserRoutingConfig(subresources=tuple(part.strip() for part in raw.split(",") if part.strip()))
39+
40+
3141
class BrowserRouteCache:
3242
def __init__(self) -> None:
3343
self._routes: dict[str, BrowserRoute] = {}
@@ -74,11 +84,8 @@ def rewrite_direct_vm_options(
7484
options: FinalRequestOptions,
7585
*,
7686
cache: BrowserRouteCache,
77-
config: BrowserRoutingConfig | None,
87+
config: BrowserRoutingConfig,
7888
) -> FinalRequestOptions:
79-
if config is None or not config.enabled:
80-
return options
81-
8289
match = match_direct_vm_path(options.url)
8390
if match is None:
8491
return options

tests/test_browser_routing.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from kernel import Kernel
1111
from kernel.lib.browser_routing.util import jwt_from_cdp_ws_url
12-
from kernel.lib.browser_routing.routing import BrowserRoutingConfig, browser_route_from_browser
12+
from kernel.lib.browser_routing.routing import browser_route_from_browser, browser_routing_config_from_env
1313

1414
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
1515
api_key = "sk-123"
@@ -39,14 +39,14 @@ def test_jwt_from_cdp_ws_url() -> None:
3939

4040

4141
@respx.mock
42-
def test_routes_allowlisted_browser_subresources_directly_to_vm() -> None:
42+
def test_routes_allowlisted_browser_subresources_directly_to_vm(monkeypatch: pytest.MonkeyPatch) -> None:
43+
monkeypatch.setenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", "process")
4344
route = respx.post("http://browser-session.test/browser/kernel/process/exec").mock(
4445
return_value=httpx.Response(200, json={"exit_code": 0, "stdout_b64": "", "stderr_b64": ""})
4546
)
4647
with Kernel(
4748
base_url=base_url,
4849
api_key=api_key,
49-
browser_routing=BrowserRoutingConfig(enabled=True, subresources=("process",)),
5050
_strict_response_validation=True,
5151
) as client:
5252
_cache_browser(client)
@@ -60,14 +60,14 @@ def test_routes_allowlisted_browser_subresources_directly_to_vm() -> None:
6060

6161

6262
@respx.mock
63-
def test_skips_direct_vm_routing_outside_allowlist() -> None:
63+
def test_skips_direct_vm_routing_outside_allowlist(monkeypatch: pytest.MonkeyPatch) -> None:
64+
monkeypatch.setenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", "computer")
6465
route = respx.post(f"{base_url}/browsers/sess-1/process/exec").mock(
6566
return_value=httpx.Response(200, json={"exit_code": 0, "stdout_b64": "", "stderr_b64": ""})
6667
)
6768
with Kernel(
6869
base_url=base_url,
6970
api_key=api_key,
70-
browser_routing=BrowserRoutingConfig(enabled=True, subresources=("computer",)),
7171
_strict_response_validation=True,
7272
) as client:
7373
_cache_browser(client)
@@ -123,3 +123,13 @@ def test_browser_request_requires_cached_route() -> None:
123123
def test_browser_route_from_browser_requires_base_url_and_jwt() -> None:
124124
assert browser_route_from_browser({**_fake_browser(), "base_url": None}) is None
125125
assert browser_route_from_browser({**_fake_browser(), "cdp_ws_url": None}) is None
126+
127+
128+
def test_browser_routing_config_from_env_defaults_to_curl(monkeypatch: pytest.MonkeyPatch) -> None:
129+
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
130+
assert browser_routing_config_from_env().subresources == ("curl",)
131+
132+
133+
def test_browser_routing_config_from_env_empty_string_disables_routing(monkeypatch: pytest.MonkeyPatch) -> None:
134+
monkeypatch.setenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", "")
135+
assert browser_routing_config_from_env().subresources == ()

0 commit comments

Comments
 (0)