Skip to content

Commit 5a0a461

Browse files
feat: Route browser destinations through selected proxies
Stainless-Generated-From: 63b6ecc138cd3513679f9b07de2e6e8f6e0af813
1 parent cbbbe4c commit 5a0a461

5 files changed

Lines changed: 163 additions & 15 deletions

File tree

‎src/kernel/types/browser_network_config.py‎

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,28 @@
44

55
from .._models import BaseModel
66

7-
__all__ = ["BrowserNetworkConfig"]
7+
__all__ = ["BrowserNetworkConfig", "ProxyRoute", "ProxyRouteProxy"]
8+
9+
10+
class ProxyRouteProxy(BaseModel):
11+
"""Select an active non-direct proxy by ID or name. Responses always use ID."""
12+
13+
id: Optional[str] = None
14+
15+
name: Optional[str] = None
16+
17+
18+
class ProxyRoute(BaseModel):
19+
hosts: List[str]
20+
"""Exact hostnames or leading \\**.
21+
22+
wildcard patterns (subdomains only); patterns cannot include ports, and matching
23+
ignores the destination port. Hosts not matched by any route use the session's
24+
top-level proxy (or the browser default when proxy is omitted).
25+
"""
26+
27+
proxy: ProxyRouteProxy
28+
"""Select an active non-direct proxy by ID or name. Responses always use ID."""
829

930

1031
class BrowserNetworkConfig(BaseModel):
@@ -35,3 +56,22 @@ class BrowserNetworkConfig(BaseModel):
3556
destinations. Not related to a proxy's bypass_hosts, which selects between
3657
upstream-proxy and Kernel-managed direct egress and cannot reach into a VPN.
3758
"""
59+
60+
proxy_routes: Optional[List[ProxyRoute]] = None
61+
"""Per-destination proxy routes for a browser session.
62+
63+
After setup, a destination hostname is matched against every route's hosts,
64+
regardless of port; route order does not matter. An exact hostname beats a
65+
wildcard, and a longer wildcard suffix beats a shorter one (for a.b.example.com:
66+
"a.b.example.com" > "_.b.example.com" > "_.example.com"). A host pattern may
67+
appear in only one route. "\\**.example.com" matches subdomains only, not
68+
example.com. A matched request selects the route's proxy instead of the
69+
session's top-level proxy (including mode: direct); the route proxy's own
70+
bypass_hosts still apply. If the route proxy becomes unavailable, matched
71+
requests fail closed without falling back. Requests that match no route use the
72+
session's default egress from the top-level proxy field (or the browser default
73+
when proxy is omitted: stealth proxy or direct egress). Routes take effect once
74+
the session is created; start_url and other traffic during browser setup use the
75+
top-level proxy. Setting routes requires proxy v3. Not supported on browser
76+
pools.
77+
"""

‎src/kernel/types/browser_network_config_param.py‎

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,33 @@
22

33
from __future__ import annotations
44

5-
from typing_extensions import TypedDict
5+
from typing import Iterable
6+
from typing_extensions import Required, TypedDict
67

78
from .._types import SequenceNotStr
89

9-
__all__ = ["BrowserNetworkConfigParam"]
10+
__all__ = ["BrowserNetworkConfigParam", "ProxyRoute", "ProxyRouteProxy"]
11+
12+
13+
class ProxyRouteProxy(TypedDict, total=False):
14+
"""Select an active non-direct proxy by ID or name. Responses always use ID."""
15+
16+
id: str
17+
18+
name: str
19+
20+
21+
class ProxyRoute(TypedDict, total=False):
22+
hosts: Required[SequenceNotStr[str]]
23+
"""Exact hostnames or leading \\**.
24+
25+
wildcard patterns (subdomains only); patterns cannot include ports, and matching
26+
ignores the destination port. Hosts not matched by any route use the session's
27+
top-level proxy (or the browser default when proxy is omitted).
28+
"""
29+
30+
proxy: Required[ProxyRouteProxy]
31+
"""Select an active non-direct proxy by ID or name. Responses always use ID."""
1032

1133

1234
class BrowserNetworkConfigParam(TypedDict, total=False):
@@ -37,3 +59,22 @@ class BrowserNetworkConfigParam(TypedDict, total=False):
3759
destinations. Not related to a proxy's bypass_hosts, which selects between
3860
upstream-proxy and Kernel-managed direct egress and cannot reach into a VPN.
3961
"""
62+
63+
proxy_routes: Iterable[ProxyRoute]
64+
"""Per-destination proxy routes for a browser session.
65+
66+
After setup, a destination hostname is matched against every route's hosts,
67+
regardless of port; route order does not matter. An exact hostname beats a
68+
wildcard, and a longer wildcard suffix beats a shorter one (for a.b.example.com:
69+
"a.b.example.com" > "_.b.example.com" > "_.example.com"). A host pattern may
70+
appear in only one route. "\\**.example.com" matches subdomains only, not
71+
example.com. A matched request selects the route's proxy instead of the
72+
session's top-level proxy (including mode: direct); the route proxy's own
73+
bypass_hosts still apply. If the route proxy becomes unavailable, matched
74+
requests fail closed without falling back. Requests that match no route use the
75+
session's default egress from the top-level proxy field (or the browser default
76+
when proxy is omitted: stealth proxy or direct egress). Routes take effect once
77+
the session is created; start_url and other traffic during browser setup use the
78+
top-level proxy. Setting routes requires proxy v3. Not supported on browser
79+
pools.
80+
"""

‎src/kernel/types/browsers/browser_proxy_error_event.py‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class Data(BrowserEventContext):
1818

1919
code: Literal[
2020
"destination_blocked",
21+
"destination_route_unavailable",
2122
"provider_blacklisted",
2223
"provider_unreachable",
2324
"provider_rejected",
@@ -33,12 +34,12 @@ class Data(BrowserEventContext):
3334
"""
3435
Proxy-layer error code: the X-Kernel-Proxy-Error response header value from a
3536
branded 5xx error page served by the metro egress host-proxy. Values mirror what
36-
the proxy emits: destination_blocked, provider_blacklisted,
37-
provider_unreachable, provider_rejected, origin_tls_timeout,
38-
origin_response_incomplete, proxy_unavailable, restricted_route_unavailable,
39-
upstream_timeout, upstream_dns_failure, upstream_connect_failed. A header value
40-
the browser image does not recognize is reported as unknown, with the header
41-
value in raw_code.
37+
the proxy emits: destination_blocked, destination_route_unavailable,
38+
provider_blacklisted, provider_unreachable, provider_rejected,
39+
origin_tls_timeout, origin_response_incomplete, proxy_unavailable,
40+
restricted_route_unavailable, upstream_timeout, upstream_dns_failure,
41+
upstream_connect_failed. A header value the browser image does not recognize is
42+
reported as unknown, with the header value in raw_code.
4243
"""
4344

4445
request_id: str

‎tests/api_resources/test_browser_pools.py‎

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,18 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
4545
headless=False,
4646
kiosk_mode=True,
4747
name="my-pool",
48-
network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
48+
network={
49+
"private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
50+
"proxy_routes": [
51+
{
52+
"hosts": ["string"],
53+
"proxy": {
54+
"id": "x",
55+
"name": "x",
56+
},
57+
}
58+
],
59+
},
4960
profile={
5061
"id": "id",
5162
"name": "name",
@@ -184,7 +195,18 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
184195
headless=False,
185196
kiosk_mode=True,
186197
name="my-pool",
187-
network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
198+
network={
199+
"private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
200+
"proxy_routes": [
201+
{
202+
"hosts": ["string"],
203+
"proxy": {
204+
"id": "x",
205+
"name": "x",
206+
},
207+
}
208+
],
209+
},
188210
profile={
189211
"id": "id",
190212
"name": "name",
@@ -572,7 +594,18 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
572594
headless=False,
573595
kiosk_mode=True,
574596
name="my-pool",
575-
network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
597+
network={
598+
"private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
599+
"proxy_routes": [
600+
{
601+
"hosts": ["string"],
602+
"proxy": {
603+
"id": "x",
604+
"name": "x",
605+
},
606+
}
607+
],
608+
},
576609
profile={
577610
"id": "id",
578611
"name": "name",
@@ -711,7 +744,18 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
711744
headless=False,
712745
kiosk_mode=True,
713746
name="my-pool",
714-
network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
747+
network={
748+
"private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
749+
"proxy_routes": [
750+
{
751+
"hosts": ["string"],
752+
"proxy": {
753+
"id": "x",
754+
"name": "x",
755+
},
756+
}
757+
],
758+
},
715759
profile={
716760
"id": "id",
717761
"name": "name",

‎tests/api_resources/test_browsers.py‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,18 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
4848
kiosk_mode=True,
4949
memory="8GiB",
5050
name="checkout-flow-1",
51-
network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
51+
network={
52+
"private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
53+
"proxy_routes": [
54+
{
55+
"hosts": ["string"],
56+
"proxy": {
57+
"id": "x",
58+
"name": "x",
59+
},
60+
}
61+
],
62+
},
5263
profile={
5364
"id": "id",
5465
"name": "name",
@@ -579,7 +590,18 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
579590
kiosk_mode=True,
580591
memory="8GiB",
581592
name="checkout-flow-1",
582-
network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
593+
network={
594+
"private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
595+
"proxy_routes": [
596+
{
597+
"hosts": ["string"],
598+
"proxy": {
599+
"id": "x",
600+
"name": "x",
601+
},
602+
}
603+
],
604+
},
583605
profile={
584606
"id": "id",
585607
"name": "name",

0 commit comments

Comments
 (0)