diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 2969a756..820b5c1a 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.111.0"
+ ".": "0.112.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index e8ac4eb6..ec91ee34 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1 @@
-configured_endpoints: 169
+configured_endpoints: 172
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 587846b9..284e73ea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,18 @@
# Changelog
+## [0.112.0](https://github.com/kernel/kernel-python-sdk/compare/v0.111.0...v0.112.0) (2026-09-24)
+
+
+### Features
+
+* Add profiles to browser pool acquire ([f5c540c](https://github.com/kernel/kernel-python-sdk/commit/f5c540cdd1b0cee422cf47825dbc2212a5b6532f))
+* Correct monitor_disconnected description on computed state ([2b637f5](https://github.com/kernel/kernel-python-sdk/commit/2b637f562b34ad0963b8ba4d0ecca73fa1dd6349))
+* Expose Search rollout access in org entitlements ([cbbbe4c](https://github.com/kernel/kernel-python-sdk/commit/cbbbe4c865eb72c10be0f95e472921cf61898abc))
+* Filter archived telemetry events by type ([1fdfce2](https://github.com/kernel/kernel-python-sdk/commit/1fdfce2a3b05c5af330c93cd8e37a217f4d0f99b))
+* Proxy custom WebMCP tools in the control plane ([179dcd9](https://github.com/kernel/kernel-python-sdk/commit/179dcd9f2c9c27a46bc5f37d241da0f0626ccf82))
+* Publish iframe and worker browser target types ([848805a](https://github.com/kernel/kernel-python-sdk/commit/848805abcfa585e4299742cd8f7a3ff0306febfe))
+* Route browser destinations through selected proxies ([5a0a461](https://github.com/kernel/kernel-python-sdk/commit/5a0a461f25cbb59b43b2695fea8294357f3655fb))
+
## [0.111.0](https://github.com/kernel/kernel-python-sdk/compare/v0.110.0...v0.111.0) (2026-09-22)
diff --git a/api.md b/api.md
index 7e82ddfb..1b130bb7 100644
--- a/api.md
+++ b/api.md
@@ -346,12 +346,14 @@ Types:
```python
from kernel.types.browsers import (
+ CustomToolSource,
InvocationFailure,
InvocationResult,
InvokeRequest,
Tool,
ToolAnnotations,
ToolFrame,
+ ToolMetadata,
ToolSource,
ToolsResponse,
)
@@ -359,8 +361,22 @@ from kernel.types.browsers import (
Methods:
-- client.browsers.webmcp.invoke_tool(id_or_name, \*\*params) -> InvocationResult
-- client.browsers.webmcp.list_tools(id_or_name) -> ToolsResponse
+- client.browsers.webmcp.invoke_tool(id_or_name, \*\*params) -> InvocationResult
+- client.browsers.webmcp.list_tools(id_or_name, \*\*params) -> ToolsResponse
+
+### CustomTools
+
+Types:
+
+```python
+from kernel.types.browsers.webmcp import AddRequest, CustomToolsResponse, Definition, Match
+```
+
+Methods:
+
+- client.browsers.webmcp.custom_tools.list(id_or_name) -> CustomToolsResponse
+- client.browsers.webmcp.custom_tools.add(id_or_name, \*\*params) -> CustomToolsResponse
+- client.browsers.webmcp.custom_tools.remove(id, \*, id_or_name) -> None
# Profiles
diff --git a/pyproject.toml b/pyproject.toml
index c8bc1630..55d2a163 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.111.0"
+version = "0.112.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index bd2462d5..8211aa6c 100644
--- a/src/kernel/_version.py
+++ b/src/kernel/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "kernel"
-__version__ = "0.111.0" # x-release-please-version
+__version__ = "0.112.0" # x-release-please-version
diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py
index 5351d638..13dbceca 100644
--- a/src/kernel/resources/browser_pools.py
+++ b/src/kernel/resources/browser_pools.py
@@ -31,6 +31,7 @@
from ..types.browser_pool import BrowserPool
from ..types.browser_network_config_param import BrowserNetworkConfigParam
from ..types.browser_pool_acquire_response import BrowserPoolAcquireResponse
+from ..types.shared_params.browser_profile import BrowserProfile
from ..types.shared_params.browser_viewport import BrowserViewport
from ..types.shared_params.browser_extension import BrowserExtension
@@ -509,6 +510,7 @@ def acquire(
*,
acquire_timeout_seconds: int | Omit = omit,
name: str | Omit = omit,
+ profile: BrowserProfile | Omit = omit,
start_url: str | Omit = omit,
tags: TagsParam | Omit = omit,
telemetry: Optional[browser_pool_acquire_params.Telemetry] | Omit = omit,
@@ -536,6 +538,10 @@ def acquire(
project. Applies to this lease only and is cleared when the browser is released
back to the pool.
+ profile: Profile selection for the browser session. Provide either id or name. If
+ specified, the matching profile will be loaded into the browser session.
+ Profiles must be created beforehand.
+
start_url: Optional URL to navigate the acquired browser to. Overrides the pool's start_url
for this acquire only. Best-effort: failures to navigate do not fail the
acquire.
@@ -569,6 +575,7 @@ def acquire(
{
"acquire_timeout_seconds": acquire_timeout_seconds,
"name": name,
+ "profile": profile,
"start_url": start_url,
"tags": tags,
"telemetry": telemetry,
@@ -638,7 +645,8 @@ def release(
Defaults to true. A reused browser keeps the configuration it was created with,
so it does not pick up pool configuration changes made while it was in use.
Release with `reuse: false`, or flush the pool afterward, to rebuild it with the
- current configuration.
+ current configuration. Browsers loaded with an acquire-time profile are always
+ destroyed and replaced, even when reuse is true.
extra_headers: Send extra headers
@@ -1139,6 +1147,7 @@ async def acquire(
*,
acquire_timeout_seconds: int | Omit = omit,
name: str | Omit = omit,
+ profile: BrowserProfile | Omit = omit,
start_url: str | Omit = omit,
tags: TagsParam | Omit = omit,
telemetry: Optional[browser_pool_acquire_params.Telemetry] | Omit = omit,
@@ -1166,6 +1175,10 @@ async def acquire(
project. Applies to this lease only and is cleared when the browser is released
back to the pool.
+ profile: Profile selection for the browser session. Provide either id or name. If
+ specified, the matching profile will be loaded into the browser session.
+ Profiles must be created beforehand.
+
start_url: Optional URL to navigate the acquired browser to. Overrides the pool's start_url
for this acquire only. Best-effort: failures to navigate do not fail the
acquire.
@@ -1199,6 +1212,7 @@ async def acquire(
{
"acquire_timeout_seconds": acquire_timeout_seconds,
"name": name,
+ "profile": profile,
"start_url": start_url,
"tags": tags,
"telemetry": telemetry,
@@ -1268,7 +1282,8 @@ async def release(
Defaults to true. A reused browser keeps the configuration it was created with,
so it does not pick up pool configuration changes made while it was in use.
Release with `reuse: false`, or flush the pool afterward, to rebuild it with the
- current configuration.
+ current configuration. Browsers loaded with an acquire-time profile are always
+ destroyed and replaced, even when reuse is true.
extra_headers: Send extra headers
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index d2e519bd..b1cf74d6 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -24,14 +24,6 @@
FsResourceWithStreamingResponse,
AsyncFsResourceWithStreamingResponse,
)
-from .webmcp import (
- WebmcpResource,
- AsyncWebmcpResource,
- WebmcpResourceWithRawResponse,
- AsyncWebmcpResourceWithRawResponse,
- WebmcpResourceWithStreamingResponse,
- AsyncWebmcpResourceWithStreamingResponse,
-)
from ...types import (
BrowserMemoryRequest,
browser_curl_params,
@@ -94,6 +86,14 @@
async_to_streamed_response_wrapper,
)
from ...pagination import SyncOffsetPagination, AsyncOffsetPagination
+from .webmcp.webmcp import (
+ WebmcpResource,
+ AsyncWebmcpResource,
+ WebmcpResourceWithRawResponse,
+ AsyncWebmcpResourceWithRawResponse,
+ WebmcpResourceWithStreamingResponse,
+ AsyncWebmcpResourceWithStreamingResponse,
+)
from ..._base_client import AsyncPaginator, make_request_options
from ...types.tags_param import TagsParam
from ...types.browser_repl_result import BrowserReplResult
diff --git a/src/kernel/resources/browsers/telemetry.py b/src/kernel/resources/browsers/telemetry.py
index 367ac734..45cc3516 100644
--- a/src/kernel/resources/browsers/telemetry.py
+++ b/src/kernel/resources/browsers/telemetry.py
@@ -7,7 +7,7 @@
import httpx
-from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
+from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
from ..._utils import path_template, maybe_transform, strip_not_given, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
@@ -75,6 +75,7 @@ def events(
offset: int | Omit = omit,
order: str | Omit = omit,
since: str | Omit = omit,
+ type: SequenceNotStr[str] | Omit = omit,
until: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -87,8 +88,9 @@ def events(
To page through
results, pass the X-Next-Offset value from the previous response as offset and
- repeat while X-Has-More is true. Returns an empty list when telemetry data is
- unavailable.
+ repeat while X-Has-More is true. The category and type filters apply within each
+ page, so a filtered page may be empty while X-Has-More is true. Returns an empty
+ list when telemetry data is unavailable.
Args:
category: Restrict results to these event categories. Repeat the parameter for multiple
@@ -104,13 +106,15 @@ def events(
order: Read direction. asc (default) reads oldest first, starting from since or the
offset cursor. desc reads newest first: each request returns one page of up to
limit records ending at the offset cursor (or until, or the newest archived
- event); combining desc with since is rejected with a 400. In either direction
- the category filter applies within the page, so a filtered page may be empty
- while X-Has-More is true.
+ event); combining desc with since is rejected with a 400.
since: Start of the window: an RFC-3339 timestamp, or a duration like 5m meaning that
long ago. Defaults to 5m. Ignored when offset is set.
+ type: Restrict results to these event types, such as page_crashed or
+ captcha_challenge_result. Repeat the parameter for multiple values. Combines
+ with category: when both are set an event must match both.
+
until: End of the window (exclusive): an RFC-3339 timestamp, or a duration like 5m
meaning that long ago.
@@ -139,6 +143,7 @@ def events(
"offset": offset,
"order": order,
"since": since,
+ "type": type,
"until": until,
},
telemetry_events_params.TelemetryEventsParams,
@@ -252,6 +257,7 @@ def events(
offset: int | Omit = omit,
order: str | Omit = omit,
since: str | Omit = omit,
+ type: SequenceNotStr[str] | Omit = omit,
until: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -264,8 +270,9 @@ def events(
To page through
results, pass the X-Next-Offset value from the previous response as offset and
- repeat while X-Has-More is true. Returns an empty list when telemetry data is
- unavailable.
+ repeat while X-Has-More is true. The category and type filters apply within each
+ page, so a filtered page may be empty while X-Has-More is true. Returns an empty
+ list when telemetry data is unavailable.
Args:
category: Restrict results to these event categories. Repeat the parameter for multiple
@@ -281,13 +288,15 @@ def events(
order: Read direction. asc (default) reads oldest first, starting from since or the
offset cursor. desc reads newest first: each request returns one page of up to
limit records ending at the offset cursor (or until, or the newest archived
- event); combining desc with since is rejected with a 400. In either direction
- the category filter applies within the page, so a filtered page may be empty
- while X-Has-More is true.
+ event); combining desc with since is rejected with a 400.
since: Start of the window: an RFC-3339 timestamp, or a duration like 5m meaning that
long ago. Defaults to 5m. Ignored when offset is set.
+ type: Restrict results to these event types, such as page_crashed or
+ captcha_challenge_result. Repeat the parameter for multiple values. Combines
+ with category: when both are set an event must match both.
+
until: End of the window (exclusive): an RFC-3339 timestamp, or a duration like 5m
meaning that long ago.
@@ -316,6 +325,7 @@ def events(
"offset": offset,
"order": order,
"since": since,
+ "type": type,
"until": until,
},
telemetry_events_params.TelemetryEventsParams,
diff --git a/src/kernel/resources/browsers/webmcp/__init__.py b/src/kernel/resources/browsers/webmcp/__init__.py
new file mode 100644
index 00000000..890b007a
--- /dev/null
+++ b/src/kernel/resources/browsers/webmcp/__init__.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .webmcp import (
+ WebmcpResource,
+ AsyncWebmcpResource,
+ WebmcpResourceWithRawResponse,
+ AsyncWebmcpResourceWithRawResponse,
+ WebmcpResourceWithStreamingResponse,
+ AsyncWebmcpResourceWithStreamingResponse,
+)
+from .custom_tools import (
+ CustomToolsResource,
+ AsyncCustomToolsResource,
+ CustomToolsResourceWithRawResponse,
+ AsyncCustomToolsResourceWithRawResponse,
+ CustomToolsResourceWithStreamingResponse,
+ AsyncCustomToolsResourceWithStreamingResponse,
+)
+
+__all__ = [
+ "CustomToolsResource",
+ "AsyncCustomToolsResource",
+ "CustomToolsResourceWithRawResponse",
+ "AsyncCustomToolsResourceWithRawResponse",
+ "CustomToolsResourceWithStreamingResponse",
+ "AsyncCustomToolsResourceWithStreamingResponse",
+ "WebmcpResource",
+ "AsyncWebmcpResource",
+ "WebmcpResourceWithRawResponse",
+ "AsyncWebmcpResourceWithRawResponse",
+ "WebmcpResourceWithStreamingResponse",
+ "AsyncWebmcpResourceWithStreamingResponse",
+]
diff --git a/src/kernel/resources/browsers/webmcp/custom_tools.py b/src/kernel/resources/browsers/webmcp/custom_tools.py
new file mode 100644
index 00000000..e5126306
--- /dev/null
+++ b/src/kernel/resources/browsers/webmcp/custom_tools.py
@@ -0,0 +1,401 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import httpx
+
+from ...._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
+from ...._utils import path_template, maybe_transform, async_maybe_transform
+from ...._compat import cached_property
+from ...._resource import SyncAPIResource, AsyncAPIResource
+from ...._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ...._base_client import make_request_options
+from ....types.browsers.webmcp import custom_tool_add_params
+from ....types.browsers.webmcp.custom_tools_response import CustomToolsResponse
+
+__all__ = ["CustomToolsResource", "AsyncCustomToolsResource"]
+
+
+class CustomToolsResource(SyncAPIResource):
+ """Discover and invoke native page tools across the browser instance."""
+
+ @cached_property
+ def with_raw_response(self) -> CustomToolsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return CustomToolsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> CustomToolsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return CustomToolsResourceWithStreamingResponse(self)
+
+ def list(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> CustomToolsResponse:
+ """
+ Returns every registered custom tool with its generated ID, namespace, matcher,
+ and MCP tool metadata.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return self._get(
+ path_template("/browsers/{id_or_name}/webmcp/custom-tools", id_or_name=id_or_name),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=CustomToolsResponse,
+ )
+
+ def add(
+ self,
+ id_or_name: str,
+ *,
+ namespace: str,
+ source: str,
+ force_overwrite_namespace: bool | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> CustomToolsResponse:
+ """Add a namespaced batch of custom tools.
+
+ A custom tool can be page-backed or
+ CDP-backed. Page-backed tools execute in the page via JavaScript. CDP-backed
+ tools execute via CDP and can use all browser REPL tools (see `/repl`). The
+ source must evaluate to a non-empty array of definitions with URL matchers, tool
+ metadata (including an optional output schema), and execute functions. The batch
+ is added atomically. Matchers apply to top-level documents and nested frames,
+ including out-of-process iframes; each matching tool is exposed once on the
+ tab's top-level document and appears in the WebMCP tool snapshot.
+
+ To update one tool, list the tools, delete its ID, and add its replacement. Set
+ force_overwrite_namespace to replace every existing tool in the namespace
+ atomically; omitted or false adds tools without replacing existing ones.
+ Existing invocations continue.
+
+ Args:
+ source: JavaScript expression that evaluates to a non-empty array of custom tool
+ definitions. Limited to 8,000,000 bytes when UTF-8 encoded, so multi-byte
+ characters reduce the allowed character count.
+
+ force_overwrite_namespace: Atomically replace all existing tools in this namespace with this batch when
+ true.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return self._post(
+ path_template("/browsers/{id_or_name}/webmcp/custom-tools", id_or_name=id_or_name),
+ body=maybe_transform(
+ {
+ "namespace": namespace,
+ "source": source,
+ "force_overwrite_namespace": force_overwrite_namespace,
+ },
+ custom_tool_add_params.CustomToolAddParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=CustomToolsResponse,
+ )
+
+ def remove(
+ self,
+ id: str,
+ *,
+ id_or_name: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """Removes one custom tool by generated ID.
+
+ An invocation already in progress is
+ not canceled.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return self._delete(
+ path_template("/browsers/{id_or_name}/webmcp/custom-tools/{id}", id_or_name=id_or_name, id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+
+class AsyncCustomToolsResource(AsyncAPIResource):
+ """Discover and invoke native page tools across the browser instance."""
+
+ @cached_property
+ def with_raw_response(self) -> AsyncCustomToolsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncCustomToolsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncCustomToolsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response
+ """
+ return AsyncCustomToolsResourceWithStreamingResponse(self)
+
+ async def list(
+ self,
+ id_or_name: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> CustomToolsResponse:
+ """
+ Returns every registered custom tool with its generated ID, namespace, matcher,
+ and MCP tool metadata.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return await self._get(
+ path_template("/browsers/{id_or_name}/webmcp/custom-tools", id_or_name=id_or_name),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=CustomToolsResponse,
+ )
+
+ async def add(
+ self,
+ id_or_name: str,
+ *,
+ namespace: str,
+ source: str,
+ force_overwrite_namespace: bool | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> CustomToolsResponse:
+ """Add a namespaced batch of custom tools.
+
+ A custom tool can be page-backed or
+ CDP-backed. Page-backed tools execute in the page via JavaScript. CDP-backed
+ tools execute via CDP and can use all browser REPL tools (see `/repl`). The
+ source must evaluate to a non-empty array of definitions with URL matchers, tool
+ metadata (including an optional output schema), and execute functions. The batch
+ is added atomically. Matchers apply to top-level documents and nested frames,
+ including out-of-process iframes; each matching tool is exposed once on the
+ tab's top-level document and appears in the WebMCP tool snapshot.
+
+ To update one tool, list the tools, delete its ID, and add its replacement. Set
+ force_overwrite_namespace to replace every existing tool in the namespace
+ atomically; omitted or false adds tools without replacing existing ones.
+ Existing invocations continue.
+
+ Args:
+ source: JavaScript expression that evaluates to a non-empty array of custom tool
+ definitions. Limited to 8,000,000 bytes when UTF-8 encoded, so multi-byte
+ characters reduce the allowed character count.
+
+ force_overwrite_namespace: Atomically replace all existing tools in this namespace with this batch when
+ true.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return await self._post(
+ path_template("/browsers/{id_or_name}/webmcp/custom-tools", id_or_name=id_or_name),
+ body=await async_maybe_transform(
+ {
+ "namespace": namespace,
+ "source": source,
+ "force_overwrite_namespace": force_overwrite_namespace,
+ },
+ custom_tool_add_params.CustomToolAddParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=CustomToolsResponse,
+ )
+
+ async def remove(
+ self,
+ id: str,
+ *,
+ id_or_name: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """Removes one custom tool by generated ID.
+
+ An invocation already in progress is
+ not canceled.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ if not id:
+ raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return await self._delete(
+ path_template("/browsers/{id_or_name}/webmcp/custom-tools/{id}", id_or_name=id_or_name, id=id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+
+class CustomToolsResourceWithRawResponse:
+ def __init__(self, custom_tools: CustomToolsResource) -> None:
+ self._custom_tools = custom_tools
+
+ self.list = to_raw_response_wrapper(
+ custom_tools.list,
+ )
+ self.add = to_raw_response_wrapper(
+ custom_tools.add,
+ )
+ self.remove = to_raw_response_wrapper(
+ custom_tools.remove,
+ )
+
+
+class AsyncCustomToolsResourceWithRawResponse:
+ def __init__(self, custom_tools: AsyncCustomToolsResource) -> None:
+ self._custom_tools = custom_tools
+
+ self.list = async_to_raw_response_wrapper(
+ custom_tools.list,
+ )
+ self.add = async_to_raw_response_wrapper(
+ custom_tools.add,
+ )
+ self.remove = async_to_raw_response_wrapper(
+ custom_tools.remove,
+ )
+
+
+class CustomToolsResourceWithStreamingResponse:
+ def __init__(self, custom_tools: CustomToolsResource) -> None:
+ self._custom_tools = custom_tools
+
+ self.list = to_streamed_response_wrapper(
+ custom_tools.list,
+ )
+ self.add = to_streamed_response_wrapper(
+ custom_tools.add,
+ )
+ self.remove = to_streamed_response_wrapper(
+ custom_tools.remove,
+ )
+
+
+class AsyncCustomToolsResourceWithStreamingResponse:
+ def __init__(self, custom_tools: AsyncCustomToolsResource) -> None:
+ self._custom_tools = custom_tools
+
+ self.list = async_to_streamed_response_wrapper(
+ custom_tools.list,
+ )
+ self.add = async_to_streamed_response_wrapper(
+ custom_tools.add,
+ )
+ self.remove = async_to_streamed_response_wrapper(
+ custom_tools.remove,
+ )
diff --git a/src/kernel/resources/browsers/webmcp.py b/src/kernel/resources/browsers/webmcp/webmcp.py
similarity index 71%
rename from src/kernel/resources/browsers/webmcp.py
rename to src/kernel/resources/browsers/webmcp/webmcp.py
index 450a5c4f..dc030e34 100644
--- a/src/kernel/resources/browsers/webmcp.py
+++ b/src/kernel/resources/browsers/webmcp/webmcp.py
@@ -6,20 +6,28 @@
import httpx
-from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
-from ..._utils import path_template, maybe_transform, async_maybe_transform
-from ..._compat import cached_property
-from ..._resource import SyncAPIResource, AsyncAPIResource
-from ..._response import (
+from ...._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
+from ...._utils import path_template, maybe_transform, async_maybe_transform
+from ...._compat import cached_property
+from ...._resource import SyncAPIResource, AsyncAPIResource
+from ...._response import (
to_raw_response_wrapper,
to_streamed_response_wrapper,
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from ..._base_client import make_request_options
-from ...types.browsers import webmcp_invoke_tool_params
-from ...types.browsers.tools_response import ToolsResponse
-from ...types.browsers.invocation_result import InvocationResult
+from .custom_tools import (
+ CustomToolsResource,
+ AsyncCustomToolsResource,
+ CustomToolsResourceWithRawResponse,
+ AsyncCustomToolsResourceWithRawResponse,
+ CustomToolsResourceWithStreamingResponse,
+ AsyncCustomToolsResourceWithStreamingResponse,
+)
+from ...._base_client import make_request_options
+from ....types.browsers import webmcp_list_tools_params, webmcp_invoke_tool_params
+from ....types.browsers.tools_response import ToolsResponse
+from ....types.browsers.invocation_result import InvocationResult
__all__ = ["WebmcpResource", "AsyncWebmcpResource"]
@@ -27,6 +35,11 @@
class WebmcpResource(SyncAPIResource):
"""Discover and invoke native page tools across the browser instance."""
+ @cached_property
+ def custom_tools(self) -> CustomToolsResource:
+ """Discover and invoke native page tools across the browser instance."""
+ return CustomToolsResource(self._client)
+
@cached_property
def with_raw_response(self) -> WebmcpResourceWithRawResponse:
"""
@@ -69,7 +82,8 @@ def invoke_tool(
then submit through Playwright or computer interaction without invoking the tool
again. If the tab or embedded frame disappears, or the request times out after
invocation begins, the response reports outcome_unknown and the tool is not
- retried.
+ retried. CDP-backed custom tool outputs above 240 KiB return an error rather
+ than a truncated result.
Args:
input: Tool input, limited to 1 MiB after JSON serialization.
@@ -104,6 +118,7 @@ def list_tools(
self,
id_or_name: str,
*,
+ exclude_custom: bool | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -112,12 +127,16 @@ def list_tools(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> ToolsResponse:
"""
- Returns a snapshot of native WebMCP tools available across every open tab and
- embedded frame in the browser. Each tool includes an opaque tool_ref for
- invoking that exact live registration. Tools disappear when their document
- closes or navigates away.
+ Returns a snapshot of native and custom WebMCP tools available across every open
+ tab and embedded frame in the browser. Each tool includes an opaque tool_ref for
+ invoking that exact live registration, nested tool metadata, and source
+ information. Custom tools include their generated ID, namespace, and CDP
+ target_id in source. Tools disappear when their document closes or navigates
+ away. Use exclude_custom to return only page-provided tools.
Args:
+ exclude_custom: Exclude custom tools when true.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -131,7 +150,13 @@ def list_tools(
return self._get(
path_template("/browsers/{id_or_name}/webmcp/tools", id_or_name=id_or_name),
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {"exclude_custom": exclude_custom}, webmcp_list_tools_params.WebmcpListToolsParams
+ ),
),
cast_to=ToolsResponse,
)
@@ -140,6 +165,11 @@ def list_tools(
class AsyncWebmcpResource(AsyncAPIResource):
"""Discover and invoke native page tools across the browser instance."""
+ @cached_property
+ def custom_tools(self) -> AsyncCustomToolsResource:
+ """Discover and invoke native page tools across the browser instance."""
+ return AsyncCustomToolsResource(self._client)
+
@cached_property
def with_raw_response(self) -> AsyncWebmcpResourceWithRawResponse:
"""
@@ -182,7 +212,8 @@ async def invoke_tool(
then submit through Playwright or computer interaction without invoking the tool
again. If the tab or embedded frame disappears, or the request times out after
invocation begins, the response reports outcome_unknown and the tool is not
- retried.
+ retried. CDP-backed custom tool outputs above 240 KiB return an error rather
+ than a truncated result.
Args:
input: Tool input, limited to 1 MiB after JSON serialization.
@@ -217,6 +248,7 @@ async def list_tools(
self,
id_or_name: str,
*,
+ exclude_custom: bool | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -225,12 +257,16 @@ async def list_tools(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> ToolsResponse:
"""
- Returns a snapshot of native WebMCP tools available across every open tab and
- embedded frame in the browser. Each tool includes an opaque tool_ref for
- invoking that exact live registration. Tools disappear when their document
- closes or navigates away.
+ Returns a snapshot of native and custom WebMCP tools available across every open
+ tab and embedded frame in the browser. Each tool includes an opaque tool_ref for
+ invoking that exact live registration, nested tool metadata, and source
+ information. Custom tools include their generated ID, namespace, and CDP
+ target_id in source. Tools disappear when their document closes or navigates
+ away. Use exclude_custom to return only page-provided tools.
Args:
+ exclude_custom: Exclude custom tools when true.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -244,7 +280,13 @@ async def list_tools(
return await self._get(
path_template("/browsers/{id_or_name}/webmcp/tools", id_or_name=id_or_name),
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform(
+ {"exclude_custom": exclude_custom}, webmcp_list_tools_params.WebmcpListToolsParams
+ ),
),
cast_to=ToolsResponse,
)
@@ -261,6 +303,11 @@ def __init__(self, webmcp: WebmcpResource) -> None:
webmcp.list_tools,
)
+ @cached_property
+ def custom_tools(self) -> CustomToolsResourceWithRawResponse:
+ """Discover and invoke native page tools across the browser instance."""
+ return CustomToolsResourceWithRawResponse(self._webmcp.custom_tools)
+
class AsyncWebmcpResourceWithRawResponse:
def __init__(self, webmcp: AsyncWebmcpResource) -> None:
@@ -273,6 +320,11 @@ def __init__(self, webmcp: AsyncWebmcpResource) -> None:
webmcp.list_tools,
)
+ @cached_property
+ def custom_tools(self) -> AsyncCustomToolsResourceWithRawResponse:
+ """Discover and invoke native page tools across the browser instance."""
+ return AsyncCustomToolsResourceWithRawResponse(self._webmcp.custom_tools)
+
class WebmcpResourceWithStreamingResponse:
def __init__(self, webmcp: WebmcpResource) -> None:
@@ -285,6 +337,11 @@ def __init__(self, webmcp: WebmcpResource) -> None:
webmcp.list_tools,
)
+ @cached_property
+ def custom_tools(self) -> CustomToolsResourceWithStreamingResponse:
+ """Discover and invoke native page tools across the browser instance."""
+ return CustomToolsResourceWithStreamingResponse(self._webmcp.custom_tools)
+
class AsyncWebmcpResourceWithStreamingResponse:
def __init__(self, webmcp: AsyncWebmcpResource) -> None:
@@ -296,3 +353,8 @@ def __init__(self, webmcp: AsyncWebmcpResource) -> None:
self.list_tools = async_to_streamed_response_wrapper(
webmcp.list_tools,
)
+
+ @cached_property
+ def custom_tools(self) -> AsyncCustomToolsResourceWithStreamingResponse:
+ """Discover and invoke native page tools across the browser instance."""
+ return AsyncCustomToolsResourceWithStreamingResponse(self._webmcp.custom_tools)
diff --git a/src/kernel/types/browser_network_config.py b/src/kernel/types/browser_network_config.py
index e1941816..ec2c7fbf 100644
--- a/src/kernel/types/browser_network_config.py
+++ b/src/kernel/types/browser_network_config.py
@@ -4,7 +4,28 @@
from .._models import BaseModel
-__all__ = ["BrowserNetworkConfig"]
+__all__ = ["BrowserNetworkConfig", "ProxyRoute", "ProxyRouteProxy"]
+
+
+class ProxyRouteProxy(BaseModel):
+ """Select an active non-direct proxy by ID or name. Responses always use ID."""
+
+ id: Optional[str] = None
+
+ name: Optional[str] = None
+
+
+class ProxyRoute(BaseModel):
+ hosts: List[str]
+ """Exact hostnames or leading \\**.
+
+ wildcard patterns (subdomains only); patterns cannot include ports, and matching
+ ignores the destination port. Hosts not matched by any route use the session's
+ top-level proxy (or the browser default when proxy is omitted).
+ """
+
+ proxy: ProxyRouteProxy
+ """Select an active non-direct proxy by ID or name. Responses always use ID."""
class BrowserNetworkConfig(BaseModel):
@@ -35,3 +56,22 @@ class BrowserNetworkConfig(BaseModel):
destinations. Not related to a proxy's bypass_hosts, which selects between
upstream-proxy and Kernel-managed direct egress and cannot reach into a VPN.
"""
+
+ proxy_routes: Optional[List[ProxyRoute]] = None
+ """Per-destination proxy routes for a browser session.
+
+ After setup, a destination hostname is matched against every route's hosts,
+ regardless of port; route order does not matter. An exact hostname beats a
+ wildcard, and a longer wildcard suffix beats a shorter one (for a.b.example.com:
+ "a.b.example.com" > "_.b.example.com" > "_.example.com"). A host pattern may
+ appear in only one route. "\\**.example.com" matches subdomains only, not
+ example.com. A matched request selects the route's proxy instead of the
+ session's top-level proxy (including mode: direct); the route proxy's own
+ bypass_hosts still apply. If the route proxy becomes unavailable, matched
+ requests fail closed without falling back. Requests that match no route use the
+ session's default egress from the top-level proxy field (or the browser default
+ when proxy is omitted: stealth proxy or direct egress). Routes take effect once
+ the session is created; start_url and other traffic during browser setup use the
+ top-level proxy. Setting routes requires proxy v3. Not supported on browser
+ pools.
+ """
diff --git a/src/kernel/types/browser_network_config_param.py b/src/kernel/types/browser_network_config_param.py
index 49f7ea64..298f9d13 100644
--- a/src/kernel/types/browser_network_config_param.py
+++ b/src/kernel/types/browser_network_config_param.py
@@ -2,11 +2,33 @@
from __future__ import annotations
-from typing_extensions import TypedDict
+from typing import Iterable
+from typing_extensions import Required, TypedDict
from .._types import SequenceNotStr
-__all__ = ["BrowserNetworkConfigParam"]
+__all__ = ["BrowserNetworkConfigParam", "ProxyRoute", "ProxyRouteProxy"]
+
+
+class ProxyRouteProxy(TypedDict, total=False):
+ """Select an active non-direct proxy by ID or name. Responses always use ID."""
+
+ id: str
+
+ name: str
+
+
+class ProxyRoute(TypedDict, total=False):
+ hosts: Required[SequenceNotStr[str]]
+ """Exact hostnames or leading \\**.
+
+ wildcard patterns (subdomains only); patterns cannot include ports, and matching
+ ignores the destination port. Hosts not matched by any route use the session's
+ top-level proxy (or the browser default when proxy is omitted).
+ """
+
+ proxy: Required[ProxyRouteProxy]
+ """Select an active non-direct proxy by ID or name. Responses always use ID."""
class BrowserNetworkConfigParam(TypedDict, total=False):
@@ -37,3 +59,22 @@ class BrowserNetworkConfigParam(TypedDict, total=False):
destinations. Not related to a proxy's bypass_hosts, which selects between
upstream-proxy and Kernel-managed direct egress and cannot reach into a VPN.
"""
+
+ proxy_routes: Iterable[ProxyRoute]
+ """Per-destination proxy routes for a browser session.
+
+ After setup, a destination hostname is matched against every route's hosts,
+ regardless of port; route order does not matter. An exact hostname beats a
+ wildcard, and a longer wildcard suffix beats a shorter one (for a.b.example.com:
+ "a.b.example.com" > "_.b.example.com" > "_.example.com"). A host pattern may
+ appear in only one route. "\\**.example.com" matches subdomains only, not
+ example.com. A matched request selects the route's proxy instead of the
+ session's top-level proxy (including mode: direct); the route proxy's own
+ bypass_hosts still apply. If the route proxy becomes unavailable, matched
+ requests fail closed without falling back. Requests that match no route use the
+ session's default egress from the top-level proxy field (or the browser default
+ when proxy is omitted: stealth proxy or direct egress). Routes take effect once
+ the session is created; start_url and other traffic during browser setup use the
+ top-level proxy. Setting routes requires proxy v3. Not supported on browser
+ pools.
+ """
diff --git a/src/kernel/types/browser_pool_acquire_params.py b/src/kernel/types/browser_pool_acquire_params.py
index b5bd708d..823d05b9 100644
--- a/src/kernel/types/browser_pool_acquire_params.py
+++ b/src/kernel/types/browser_pool_acquire_params.py
@@ -6,6 +6,7 @@
from typing_extensions import TypedDict
from .tags_param import TagsParam
+from .shared_params.browser_profile import BrowserProfile
from .browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam
__all__ = [
@@ -33,6 +34,13 @@ class BrowserPoolAcquireParams(TypedDict, total=False):
back to the pool.
"""
+ profile: BrowserProfile
+ """Profile selection for the browser session.
+
+ Provide either id or name. If specified, the matching profile will be loaded
+ into the browser session. Profiles must be created beforehand.
+ """
+
start_url: str
"""Optional URL to navigate the acquired browser to.
diff --git a/src/kernel/types/browser_pool_release_params.py b/src/kernel/types/browser_pool_release_params.py
index 944e734b..5d5e6655 100644
--- a/src/kernel/types/browser_pool_release_params.py
+++ b/src/kernel/types/browser_pool_release_params.py
@@ -17,5 +17,6 @@ class BrowserPoolReleaseParams(TypedDict, total=False):
Defaults to true. A reused browser keeps the configuration it was created with,
so it does not pick up pool configuration changes made while it was in use.
Release with `reuse: false`, or flush the pool afterward, to rebuild it with the
- current configuration.
+ current configuration. Browsers loaded with an acquire-time profile are always
+ destroyed and replaced, even when reuse is true.
"""
diff --git a/src/kernel/types/browsers/__init__.py b/src/kernel/types/browsers/__init__.py
index 34ed9b73..169e6ccf 100644
--- a/src/kernel/types/browsers/__init__.py
+++ b/src/kernel/types/browsers/__init__.py
@@ -6,12 +6,14 @@
from .tool_frame import ToolFrame as ToolFrame
from .tool_source import ToolSource as ToolSource
from .f_move_params import FMoveParams as FMoveParams
+from .tool_metadata import ToolMetadata as ToolMetadata
from .tools_response import ToolsResponse as ToolsResponse
from .f_upload_params import FUploadParams as FUploadParams
from .tool_annotations import ToolAnnotations as ToolAnnotations
from .invocation_result import InvocationResult as InvocationResult
from .log_stream_params import LogStreamParams as LogStreamParams
from .browser_call_stack import BrowserCallStack as BrowserCallStack
+from .custom_tool_source import CustomToolSource as CustomToolSource
from .f_file_info_params import FFileInfoParams as FFileInfoParams
from .f_read_file_params import FReadFileParams as FReadFileParams
from .f_list_files_params import FListFilesParams as FListFilesParams
@@ -46,6 +48,7 @@
from .telemetry_events_params import TelemetryEventsParams as TelemetryEventsParams
from .telemetry_stream_params import TelemetryStreamParams as TelemetryStreamParams
from .browser_telemetry_config import BrowserTelemetryConfig as BrowserTelemetryConfig
+from .webmcp_list_tools_params import WebmcpListToolsParams as WebmcpListToolsParams
from .browser_cdp_command_event import BrowserCdpCommandEvent as BrowserCdpCommandEvent
from .browser_cdp_connect_event import BrowserCdpConnectEvent as BrowserCdpConnectEvent
from .browser_console_log_event import BrowserConsoleLogEvent as BrowserConsoleLogEvent
diff --git a/src/kernel/types/browsers/browser_event_context.py b/src/kernel/types/browsers/browser_event_context.py
index 6f2e9549..1342799e 100644
--- a/src/kernel/types/browsers/browser_event_context.py
+++ b/src/kernel/types/browsers/browser_event_context.py
@@ -32,7 +32,9 @@ class BrowserEventContext(BaseModel):
target_id: Optional[str] = None
"""Browser target identifier (stable across navigations within a tab)."""
- target_type: Optional[Literal["page", "background_page", "service_worker", "shared_worker", "other"]] = None
+ target_type: Optional[
+ Literal["page", "iframe", "worker", "background_page", "service_worker", "shared_worker", "other"]
+ ] = None
"""CDP target type of the page that produced the event."""
url: Optional[str] = None
diff --git a/src/kernel/types/browsers/browser_monitor_disconnected_event.py b/src/kernel/types/browsers/browser_monitor_disconnected_event.py
index 2ea3afeb..de4afe5e 100644
--- a/src/kernel/types/browsers/browser_monitor_disconnected_event.py
+++ b/src/kernel/types/browsers/browser_monitor_disconnected_event.py
@@ -17,7 +17,7 @@ class Data(BaseModel):
class BrowserMonitorDisconnectedEvent(BaseModel):
"""The CDP connection to Chrome was lost.
- Telemetry events may be dropped until monitor_reconnected arrives. Treat any in-progress computed state (network_idle, page_layout_settled) as unreliable until then.
+ Telemetry events may be dropped until monitor_reconnected arrives. In-progress computed state is discarded rather than paused, so computed events still pending for the current navigation (network_idle, page_layout_settled, page_navigation_settled) never fire. monitor_reconnected does not restore them. After reattachment a fresh state machine starts, so computed events can resume before the next navigation and carry empty navigation context until one occurs.
"""
category: Literal["monitor"]
diff --git a/src/kernel/types/browsers/browser_page_crashed_event.py b/src/kernel/types/browsers/browser_page_crashed_event.py
index 46e84f32..86c31400 100644
--- a/src/kernel/types/browsers/browser_page_crashed_event.py
+++ b/src/kernel/types/browsers/browser_page_crashed_event.py
@@ -13,7 +13,7 @@ class Data(BaseModel):
target_id: str
"""CDP target identifier of the crashed page."""
- target_type: Literal["page", "background_page", "service_worker", "shared_worker", "other"]
+ target_type: Literal["page", "iframe", "worker", "background_page", "service_worker", "shared_worker", "other"]
"""CDP target type of the page that produced the event."""
url: str
diff --git a/src/kernel/types/browsers/browser_page_navigation_event.py b/src/kernel/types/browsers/browser_page_navigation_event.py
index 5835d6d4..b4504ef5 100644
--- a/src/kernel/types/browsers/browser_page_navigation_event.py
+++ b/src/kernel/types/browsers/browser_page_navigation_event.py
@@ -28,7 +28,9 @@ class Data(BaseModel):
target_id: Optional[str] = None
"""Browser target identifier."""
- target_type: Optional[Literal["page", "background_page", "service_worker", "shared_worker", "other"]] = None
+ target_type: Optional[
+ Literal["page", "iframe", "worker", "background_page", "service_worker", "shared_worker", "other"]
+ ] = None
"""CDP target type of the page that produced the event."""
url: Optional[str] = None
diff --git a/src/kernel/types/browsers/browser_page_tab_opened_event.py b/src/kernel/types/browsers/browser_page_tab_opened_event.py
index 144be476..9fcac2b4 100644
--- a/src/kernel/types/browsers/browser_page_tab_opened_event.py
+++ b/src/kernel/types/browsers/browser_page_tab_opened_event.py
@@ -16,7 +16,9 @@ class Data(BaseModel):
target_id: Optional[str] = None
"""CDP target identifier for the newly opened tab."""
- target_type: Optional[Literal["page", "background_page", "service_worker", "shared_worker", "other"]] = None
+ target_type: Optional[
+ Literal["page", "iframe", "worker", "background_page", "service_worker", "shared_worker", "other"]
+ ] = None
"""CDP target type of the page that produced the event."""
title: Optional[str] = None
diff --git a/src/kernel/types/browsers/browser_proxy_error_event.py b/src/kernel/types/browsers/browser_proxy_error_event.py
index 5d40f9ed..44961139 100644
--- a/src/kernel/types/browsers/browser_proxy_error_event.py
+++ b/src/kernel/types/browsers/browser_proxy_error_event.py
@@ -18,6 +18,7 @@ class Data(BrowserEventContext):
code: Literal[
"destination_blocked",
+ "destination_route_unavailable",
"provider_blacklisted",
"provider_unreachable",
"provider_rejected",
@@ -33,12 +34,12 @@ class Data(BrowserEventContext):
"""
Proxy-layer error code: the X-Kernel-Proxy-Error response header value from a
branded 5xx error page served by the metro egress host-proxy. Values mirror what
- the proxy emits: destination_blocked, provider_blacklisted,
- provider_unreachable, provider_rejected, origin_tls_timeout,
- origin_response_incomplete, proxy_unavailable, restricted_route_unavailable,
- upstream_timeout, upstream_dns_failure, upstream_connect_failed. A header value
- the browser image does not recognize is reported as unknown, with the header
- value in raw_code.
+ the proxy emits: destination_blocked, destination_route_unavailable,
+ provider_blacklisted, provider_unreachable, provider_rejected,
+ origin_tls_timeout, origin_response_incomplete, proxy_unavailable,
+ restricted_route_unavailable, upstream_timeout, upstream_dns_failure,
+ upstream_connect_failed. A header value the browser image does not recognize is
+ reported as unknown, with the header value in raw_code.
"""
request_id: str
diff --git a/src/kernel/types/browsers/custom_tool_source.py b/src/kernel/types/browsers/custom_tool_source.py
new file mode 100644
index 00000000..0ab59ab7
--- /dev/null
+++ b/src/kernel/types/browsers/custom_tool_source.py
@@ -0,0 +1,11 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from ..._models import BaseModel
+
+__all__ = ["CustomToolSource"]
+
+
+class CustomToolSource(BaseModel):
+ id: str
+
+ namespace: str
diff --git a/src/kernel/types/browsers/telemetry_events_params.py b/src/kernel/types/browsers/telemetry_events_params.py
index ec385bb5..1253aa1a 100644
--- a/src/kernel/types/browsers/telemetry_events_params.py
+++ b/src/kernel/types/browsers/telemetry_events_params.py
@@ -5,6 +5,8 @@
from typing import List
from typing_extensions import Literal, TypedDict
+from ..._types import SequenceNotStr
+
__all__ = ["TelemetryEventsParams"]
@@ -46,9 +48,7 @@ class TelemetryEventsParams(TypedDict, total=False):
asc (default) reads oldest first, starting from since or the offset cursor. desc
reads newest first: each request returns one page of up to limit records ending
at the offset cursor (or until, or the newest archived event); combining desc
- with since is rejected with a 400. In either direction the category filter
- applies within the page, so a filtered page may be empty while X-Has-More is
- true.
+ with since is rejected with a 400.
"""
since: str
@@ -57,6 +57,13 @@ class TelemetryEventsParams(TypedDict, total=False):
long ago. Defaults to 5m. Ignored when offset is set.
"""
+ type: SequenceNotStr[str]
+ """
+ Restrict results to these event types, such as page_crashed or
+ captcha_challenge_result. Repeat the parameter for multiple values. Combines
+ with category: when both are set an event must match both.
+ """
+
until: str
"""
End of the window (exclusive): an RFC-3339 timestamp, or a duration like 5m
diff --git a/src/kernel/types/browsers/tool.py b/src/kernel/types/browsers/tool.py
index 12e63767..5bde27d0 100644
--- a/src/kernel/types/browsers/tool.py
+++ b/src/kernel/types/browsers/tool.py
@@ -1,31 +1,26 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Dict, Optional
-
from ..._models import BaseModel
from .tool_source import ToolSource
-from .tool_annotations import ToolAnnotations
+from .tool_metadata import ToolMetadata
__all__ = ["Tool"]
class Tool(BaseModel):
- description: str
-
- input_schema: Dict[str, object]
-
- name: str
-
source: ToolSource
+ tool: ToolMetadata
+ """
+ Tool metadata follows the
+ [MCP Tool definition](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool)
+ and the
+ [WebMCP RegisteredTool definition](https://webmachinelearning.github.io/webmcp/#dictdef-registeredtool).
+ outputSchema is optional for page and custom tools.
+ """
+
tool_ref: str
"""Opaque reference for invoking this exact live registration.
It becomes invalid when its document or browser process is replaced.
"""
-
- annotations: Optional[ToolAnnotations] = None
- """Page-provided behavioral hints.
-
- These values are untrusted and are not enforced by Kernel.
- """
diff --git a/src/kernel/types/browsers/tool_annotations.py b/src/kernel/types/browsers/tool_annotations.py
index 9f9b097d..7c12d425 100644
--- a/src/kernel/types/browsers/tool_annotations.py
+++ b/src/kernel/types/browsers/tool_annotations.py
@@ -1,20 +1,31 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+from typing import Optional
+
+from pydantic import Field as FieldInfo
+
from ..._models import BaseModel
__all__ = ["ToolAnnotations"]
class ToolAnnotations(BaseModel):
- """Page-provided behavioral hints.
-
- These values are untrusted and are not enforced by Kernel.
"""
+ Tool-provided behavioral hints from the [MCP tool specification](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool)
+ and the [WebMCP ToolAnnotations definition](https://webmachinelearning.github.io/webmcp/#dictdef-toolannotations).
+ These hints are untrusted and are not enforced by Kernel.
+ """
+
+ autosubmit: Optional[bool] = None
+
+ consequential_hint: Optional[bool] = FieldInfo(alias="consequentialHint", default=None)
+
+ destructive_hint: Optional[bool] = FieldInfo(alias="destructiveHint", default=None)
- autosubmit: bool
+ idempotent_hint: Optional[bool] = FieldInfo(alias="idempotentHint", default=None)
- consequential: bool
+ open_world_hint: Optional[bool] = FieldInfo(alias="openWorldHint", default=None)
- read_only: bool
+ read_only_hint: Optional[bool] = FieldInfo(alias="readOnlyHint", default=None)
- untrusted_content: bool
+ untrusted_content_hint: Optional[bool] = FieldInfo(alias="untrustedContentHint", default=None)
diff --git a/src/kernel/types/browsers/tool_metadata.py b/src/kernel/types/browsers/tool_metadata.py
new file mode 100644
index 00000000..747f461e
--- /dev/null
+++ b/src/kernel/types/browsers/tool_metadata.py
@@ -0,0 +1,37 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, Optional
+
+from pydantic import Field as FieldInfo
+
+from ..._models import BaseModel
+from .tool_annotations import ToolAnnotations
+
+__all__ = ["ToolMetadata"]
+
+
+class ToolMetadata(BaseModel):
+ """
+ Tool metadata follows the [MCP Tool definition](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool)
+ and the [WebMCP RegisteredTool definition](https://webmachinelearning.github.io/webmcp/#dictdef-registeredtool).
+ outputSchema is optional for page and custom tools.
+ """
+
+ description: str
+
+ input_schema: Dict[str, object] = FieldInfo(alias="inputSchema")
+
+ name: str
+
+ annotations: Optional[ToolAnnotations] = None
+ """
+ Tool-provided behavioral hints from the
+ [MCP tool specification](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool)
+ and the
+ [WebMCP ToolAnnotations definition](https://webmachinelearning.github.io/webmcp/#dictdef-toolannotations).
+ These hints are untrusted and are not enforced by Kernel.
+ """
+
+ output_schema: Optional[Dict[str, object]] = FieldInfo(alias="outputSchema", default=None)
+
+ title: Optional[str] = None
diff --git a/src/kernel/types/browsers/tool_source.py b/src/kernel/types/browsers/tool_source.py
index ed95f856..65b57412 100644
--- a/src/kernel/types/browsers/tool_source.py
+++ b/src/kernel/types/browsers/tool_source.py
@@ -4,6 +4,7 @@
from ..._models import BaseModel
from .tool_frame import ToolFrame
+from .custom_tool_source import CustomToolSource
__all__ = ["ToolSource"]
@@ -32,3 +33,11 @@ class ToolSource(BaseModel):
Monotonically increasing identifier for the browser window during the current
browser process.
"""
+
+ custom: Optional[CustomToolSource] = None
+
+ target_id: Optional[str] = None
+ """
+ CDP target ID for a custom tool's registration tab; omitted for page-provided
+ tools.
+ """
diff --git a/src/kernel/types/browsers/webmcp/__init__.py b/src/kernel/types/browsers/webmcp/__init__.py
new file mode 100644
index 00000000..cbad2766
--- /dev/null
+++ b/src/kernel/types/browsers/webmcp/__init__.py
@@ -0,0 +1,8 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from .match import Match as Match
+from .definition import Definition as Definition
+from .custom_tools_response import CustomToolsResponse as CustomToolsResponse
+from .custom_tool_add_params import CustomToolAddParams as CustomToolAddParams
diff --git a/src/kernel/types/browsers/webmcp/custom_tool_add_params.py b/src/kernel/types/browsers/webmcp/custom_tool_add_params.py
new file mode 100644
index 00000000..3fc195e2
--- /dev/null
+++ b/src/kernel/types/browsers/webmcp/custom_tool_add_params.py
@@ -0,0 +1,24 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+__all__ = ["CustomToolAddParams"]
+
+
+class CustomToolAddParams(TypedDict, total=False):
+ namespace: Required[str]
+
+ source: Required[str]
+ """
+ JavaScript expression that evaluates to a non-empty array of custom tool
+ definitions. Limited to 8,000,000 bytes when UTF-8 encoded, so multi-byte
+ characters reduce the allowed character count.
+ """
+
+ force_overwrite_namespace: bool
+ """
+ Atomically replace all existing tools in this namespace with this batch when
+ true.
+ """
diff --git a/src/kernel/types/browsers/webmcp/custom_tools_response.py b/src/kernel/types/browsers/webmcp/custom_tools_response.py
new file mode 100644
index 00000000..4b5df6bd
--- /dev/null
+++ b/src/kernel/types/browsers/webmcp/custom_tools_response.py
@@ -0,0 +1,12 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+
+from ...._models import BaseModel
+from .definition import Definition
+
+__all__ = ["CustomToolsResponse"]
+
+
+class CustomToolsResponse(BaseModel):
+ tools: List[Definition]
diff --git a/src/kernel/types/browsers/webmcp/definition.py b/src/kernel/types/browsers/webmcp/definition.py
new file mode 100644
index 00000000..3a5cbeaa
--- /dev/null
+++ b/src/kernel/types/browsers/webmcp/definition.py
@@ -0,0 +1,26 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .match import Match
+from ...._models import BaseModel
+from ..tool_metadata import ToolMetadata
+
+__all__ = ["Definition"]
+
+
+class Definition(BaseModel):
+ id: str
+
+ kind: str
+
+ match: Match
+
+ namespace: str
+
+ tool: ToolMetadata
+ """
+ Tool metadata follows the
+ [MCP Tool definition](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool)
+ and the
+ [WebMCP RegisteredTool definition](https://webmachinelearning.github.io/webmcp/#dictdef-registeredtool).
+ outputSchema is optional for page and custom tools.
+ """
diff --git a/src/kernel/types/browsers/webmcp/match.py b/src/kernel/types/browsers/webmcp/match.py
new file mode 100644
index 00000000..51523b24
--- /dev/null
+++ b/src/kernel/types/browsers/webmcp/match.py
@@ -0,0 +1,11 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+
+from ...._models import BaseModel
+
+__all__ = ["Match"]
+
+
+class Match(BaseModel):
+ url_patterns: List[str]
diff --git a/src/kernel/types/browsers/webmcp_list_tools_params.py b/src/kernel/types/browsers/webmcp_list_tools_params.py
new file mode 100644
index 00000000..f499c9ca
--- /dev/null
+++ b/src/kernel/types/browsers/webmcp_list_tools_params.py
@@ -0,0 +1,12 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["WebmcpListToolsParams"]
+
+
+class WebmcpListToolsParams(TypedDict, total=False):
+ exclude_custom: bool
+ """Exclude custom tools when true."""
diff --git a/src/kernel/types/organization/org_entitlements.py b/src/kernel/types/organization/org_entitlements.py
index 614ce2fa..fd871249 100644
--- a/src/kernel/types/organization/org_entitlements.py
+++ b/src/kernel/types/organization/org_entitlements.py
@@ -21,6 +21,7 @@
"FeaturesManagedProxies",
"FeaturesProfiles",
"FeaturesProxyBypassHosts",
+ "FeaturesSearch",
"FeaturesVaults",
"Limits",
"Plan",
@@ -115,6 +116,15 @@ class FeaturesProxyBypassHosts(BaseModel):
"""Whether the organization is entitled to use this feature."""
+class FeaturesSearch(BaseModel):
+ """
+ Whether the Search API is enabled for the organization by its rollout feature flag.
+ """
+
+ enabled: bool
+ """Whether the organization is entitled to use this feature."""
+
+
class FeaturesVaults(BaseModel):
"""
Whether the organization can access vaults, using the same access check as vault API routes.
@@ -149,6 +159,12 @@ class Features(BaseModel):
proxy_bypass_hosts: FeaturesProxyBypassHosts
+ search: FeaturesSearch
+ """
+ Whether the Search API is enabled for the organization by its rollout feature
+ flag.
+ """
+
vaults: FeaturesVaults
"""
Whether the organization can access vaults, using the same access check as vault
diff --git a/tests/api_resources/browsers/test_telemetry.py b/tests/api_resources/browsers/test_telemetry.py
index b54dfbc1..6bf65cc9 100644
--- a/tests/api_resources/browsers/test_telemetry.py
+++ b/tests/api_resources/browsers/test_telemetry.py
@@ -36,6 +36,7 @@ def test_method_events_with_all_params(self, client: Kernel) -> None:
offset=0,
order="order",
since="since",
+ type=["string"],
until="until",
)
assert_matches_type(SyncOffsetPagination[TelemetryEventsResponse], telemetry, path=["response"])
@@ -149,6 +150,7 @@ async def test_method_events_with_all_params(self, async_client: AsyncKernel) ->
offset=0,
order="order",
since="since",
+ type=["string"],
until="until",
)
assert_matches_type(AsyncOffsetPagination[TelemetryEventsResponse], telemetry, path=["response"])
diff --git a/tests/api_resources/browsers/test_webmcp.py b/tests/api_resources/browsers/test_webmcp.py
index a55d692c..ac354bca 100644
--- a/tests/api_resources/browsers/test_webmcp.py
+++ b/tests/api_resources/browsers/test_webmcp.py
@@ -82,7 +82,16 @@ def test_path_params_invoke_tool(self, client: Kernel) -> None:
@parametrize
def test_method_list_tools(self, client: Kernel) -> None:
webmcp = client.browsers.webmcp.list_tools(
- "htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ )
+ assert_matches_type(ToolsResponse, webmcp, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list_tools_with_all_params(self, client: Kernel) -> None:
+ webmcp = client.browsers.webmcp.list_tools(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ exclude_custom=True,
)
assert_matches_type(ToolsResponse, webmcp, path=["response"])
@@ -90,7 +99,7 @@ def test_method_list_tools(self, client: Kernel) -> None:
@parametrize
def test_raw_response_list_tools(self, client: Kernel) -> None:
response = client.browsers.webmcp.with_raw_response.list_tools(
- "htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -102,7 +111,7 @@ def test_raw_response_list_tools(self, client: Kernel) -> None:
@parametrize
def test_streaming_response_list_tools(self, client: Kernel) -> None:
with client.browsers.webmcp.with_streaming_response.list_tools(
- "htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -117,7 +126,7 @@ def test_streaming_response_list_tools(self, client: Kernel) -> None:
def test_path_params_list_tools(self, client: Kernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
client.browsers.webmcp.with_raw_response.list_tools(
- "",
+ id_or_name="",
)
@@ -191,7 +200,16 @@ async def test_path_params_invoke_tool(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_method_list_tools(self, async_client: AsyncKernel) -> None:
webmcp = await async_client.browsers.webmcp.list_tools(
- "htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ )
+ assert_matches_type(ToolsResponse, webmcp, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list_tools_with_all_params(self, async_client: AsyncKernel) -> None:
+ webmcp = await async_client.browsers.webmcp.list_tools(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ exclude_custom=True,
)
assert_matches_type(ToolsResponse, webmcp, path=["response"])
@@ -199,7 +217,7 @@ async def test_method_list_tools(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_raw_response_list_tools(self, async_client: AsyncKernel) -> None:
response = await async_client.browsers.webmcp.with_raw_response.list_tools(
- "htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
)
assert response.is_closed is True
@@ -211,7 +229,7 @@ async def test_raw_response_list_tools(self, async_client: AsyncKernel) -> None:
@parametrize
async def test_streaming_response_list_tools(self, async_client: AsyncKernel) -> None:
async with async_client.browsers.webmcp.with_streaming_response.list_tools(
- "htzv5orfit78e1m2biiifpbv",
+ id_or_name="htzv5orfit78e1m2biiifpbv",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -226,5 +244,5 @@ async def test_streaming_response_list_tools(self, async_client: AsyncKernel) ->
async def test_path_params_list_tools(self, async_client: AsyncKernel) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
await async_client.browsers.webmcp.with_raw_response.list_tools(
- "",
+ id_or_name="",
)
diff --git a/tests/api_resources/browsers/webmcp/__init__.py b/tests/api_resources/browsers/webmcp/__init__.py
new file mode 100644
index 00000000..fd8019a9
--- /dev/null
+++ b/tests/api_resources/browsers/webmcp/__init__.py
@@ -0,0 +1 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/browsers/webmcp/test_custom_tools.py b/tests/api_resources/browsers/webmcp/test_custom_tools.py
new file mode 100644
index 00000000..49b3b4ee
--- /dev/null
+++ b/tests/api_resources/browsers/webmcp/test_custom_tools.py
@@ -0,0 +1,334 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from kernel import Kernel, AsyncKernel
+from tests.utils import assert_matches_type
+from kernel.types.browsers.webmcp import CustomToolsResponse
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestCustomTools:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Kernel) -> None:
+ custom_tool = client.browsers.webmcp.custom_tools.list(
+ "id_or_name",
+ )
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Kernel) -> None:
+ response = client.browsers.webmcp.custom_tools.with_raw_response.list(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ custom_tool = response.parse()
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Kernel) -> None:
+ with client.browsers.webmcp.custom_tools.with_streaming_response.list(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ custom_tool = response.parse()
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_list(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.browsers.webmcp.custom_tools.with_raw_response.list(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_add(self, client: Kernel) -> None:
+ custom_tool = client.browsers.webmcp.custom_tools.add(
+ id_or_name="id_or_name",
+ namespace="namespace",
+ source="source",
+ )
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_add_with_all_params(self, client: Kernel) -> None:
+ custom_tool = client.browsers.webmcp.custom_tools.add(
+ id_or_name="id_or_name",
+ namespace="namespace",
+ source="source",
+ force_overwrite_namespace=True,
+ )
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_add(self, client: Kernel) -> None:
+ response = client.browsers.webmcp.custom_tools.with_raw_response.add(
+ id_or_name="id_or_name",
+ namespace="namespace",
+ source="source",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ custom_tool = response.parse()
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_add(self, client: Kernel) -> None:
+ with client.browsers.webmcp.custom_tools.with_streaming_response.add(
+ id_or_name="id_or_name",
+ namespace="namespace",
+ source="source",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ custom_tool = response.parse()
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_add(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.browsers.webmcp.custom_tools.with_raw_response.add(
+ id_or_name="",
+ namespace="namespace",
+ source="source",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_remove(self, client: Kernel) -> None:
+ custom_tool = client.browsers.webmcp.custom_tools.remove(
+ id="ct_n10b9798ad53ecc4y69z31e1",
+ id_or_name="id_or_name",
+ )
+ assert custom_tool is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_remove(self, client: Kernel) -> None:
+ response = client.browsers.webmcp.custom_tools.with_raw_response.remove(
+ id="ct_n10b9798ad53ecc4y69z31e1",
+ id_or_name="id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ custom_tool = response.parse()
+ assert custom_tool is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_remove(self, client: Kernel) -> None:
+ with client.browsers.webmcp.custom_tools.with_streaming_response.remove(
+ id="ct_n10b9798ad53ecc4y69z31e1",
+ id_or_name="id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ custom_tool = response.parse()
+ assert custom_tool is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_remove(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.browsers.webmcp.custom_tools.with_raw_response.remove(
+ id="ct_n10b9798ad53ecc4y69z31e1",
+ id_or_name="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ client.browsers.webmcp.custom_tools.with_raw_response.remove(
+ id="",
+ id_or_name="id_or_name",
+ )
+
+
+class TestAsyncCustomTools:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list(self, async_client: AsyncKernel) -> None:
+ custom_tool = await async_client.browsers.webmcp.custom_tools.list(
+ "id_or_name",
+ )
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncKernel) -> None:
+ response = await async_client.browsers.webmcp.custom_tools.with_raw_response.list(
+ "id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ custom_tool = await response.parse()
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncKernel) -> None:
+ async with async_client.browsers.webmcp.custom_tools.with_streaming_response.list(
+ "id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ custom_tool = await response.parse()
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_list(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.browsers.webmcp.custom_tools.with_raw_response.list(
+ "",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_add(self, async_client: AsyncKernel) -> None:
+ custom_tool = await async_client.browsers.webmcp.custom_tools.add(
+ id_or_name="id_or_name",
+ namespace="namespace",
+ source="source",
+ )
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_add_with_all_params(self, async_client: AsyncKernel) -> None:
+ custom_tool = await async_client.browsers.webmcp.custom_tools.add(
+ id_or_name="id_or_name",
+ namespace="namespace",
+ source="source",
+ force_overwrite_namespace=True,
+ )
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_add(self, async_client: AsyncKernel) -> None:
+ response = await async_client.browsers.webmcp.custom_tools.with_raw_response.add(
+ id_or_name="id_or_name",
+ namespace="namespace",
+ source="source",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ custom_tool = await response.parse()
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_add(self, async_client: AsyncKernel) -> None:
+ async with async_client.browsers.webmcp.custom_tools.with_streaming_response.add(
+ id_or_name="id_or_name",
+ namespace="namespace",
+ source="source",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ custom_tool = await response.parse()
+ assert_matches_type(CustomToolsResponse, custom_tool, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_add(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.browsers.webmcp.custom_tools.with_raw_response.add(
+ id_or_name="",
+ namespace="namespace",
+ source="source",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_remove(self, async_client: AsyncKernel) -> None:
+ custom_tool = await async_client.browsers.webmcp.custom_tools.remove(
+ id="ct_n10b9798ad53ecc4y69z31e1",
+ id_or_name="id_or_name",
+ )
+ assert custom_tool is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_remove(self, async_client: AsyncKernel) -> None:
+ response = await async_client.browsers.webmcp.custom_tools.with_raw_response.remove(
+ id="ct_n10b9798ad53ecc4y69z31e1",
+ id_or_name="id_or_name",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ custom_tool = await response.parse()
+ assert custom_tool is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_remove(self, async_client: AsyncKernel) -> None:
+ async with async_client.browsers.webmcp.custom_tools.with_streaming_response.remove(
+ id="ct_n10b9798ad53ecc4y69z31e1",
+ id_or_name="id_or_name",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ custom_tool = await response.parse()
+ assert custom_tool is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_remove(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.browsers.webmcp.custom_tools.with_raw_response.remove(
+ id="ct_n10b9798ad53ecc4y69z31e1",
+ id_or_name="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"):
+ await async_client.browsers.webmcp.custom_tools.with_raw_response.remove(
+ id="",
+ id_or_name="id_or_name",
+ )
diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py
index 1f541959..e42664c4 100644
--- a/tests/api_resources/test_browser_pools.py
+++ b/tests/api_resources/test_browser_pools.py
@@ -45,7 +45,18 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
headless=False,
kiosk_mode=True,
name="my-pool",
- network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
+ network={
+ "private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
+ "proxy_routes": [
+ {
+ "hosts": ["string"],
+ "proxy": {
+ "id": "x",
+ "name": "x",
+ },
+ }
+ ],
+ },
profile={
"id": "id",
"name": "name",
@@ -184,7 +195,18 @@ def test_method_update_with_all_params(self, client: Kernel) -> None:
headless=False,
kiosk_mode=True,
name="my-pool",
- network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
+ network={
+ "private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
+ "proxy_routes": [
+ {
+ "hosts": ["string"],
+ "proxy": {
+ "id": "x",
+ "name": "x",
+ },
+ }
+ ],
+ },
profile={
"id": "id",
"name": "name",
@@ -370,6 +392,11 @@ def test_method_acquire_with_all_params(self, client: Kernel) -> None:
id_or_name="id_or_name",
acquire_timeout_seconds=0,
name="checkout-flow-1",
+ profile={
+ "id": "id",
+ "name": "name",
+ "save_changes": True,
+ },
start_url="https://example.com",
tags={
"team": "backend",
@@ -567,7 +594,18 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
headless=False,
kiosk_mode=True,
name="my-pool",
- network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
+ network={
+ "private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
+ "proxy_routes": [
+ {
+ "hosts": ["string"],
+ "proxy": {
+ "id": "x",
+ "name": "x",
+ },
+ }
+ ],
+ },
profile={
"id": "id",
"name": "name",
@@ -706,7 +744,18 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) ->
headless=False,
kiosk_mode=True,
name="my-pool",
- network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
+ network={
+ "private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
+ "proxy_routes": [
+ {
+ "hosts": ["string"],
+ "proxy": {
+ "id": "x",
+ "name": "x",
+ },
+ }
+ ],
+ },
profile={
"id": "id",
"name": "name",
@@ -892,6 +941,11 @@ async def test_method_acquire_with_all_params(self, async_client: AsyncKernel) -
id_or_name="id_or_name",
acquire_timeout_seconds=0,
name="checkout-flow-1",
+ profile={
+ "id": "id",
+ "name": "name",
+ "save_changes": True,
+ },
start_url="https://example.com",
tags={
"team": "backend",
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index ffa5f2e9..5caa570e 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -48,7 +48,18 @@ def test_method_create_with_all_params(self, client: Kernel) -> None:
kiosk_mode=True,
memory="8GiB",
name="checkout-flow-1",
- network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
+ network={
+ "private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
+ "proxy_routes": [
+ {
+ "hosts": ["string"],
+ "proxy": {
+ "id": "x",
+ "name": "x",
+ },
+ }
+ ],
+ },
profile={
"id": "id",
"name": "name",
@@ -579,7 +590,18 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) ->
kiosk_mode=True,
memory="8GiB",
name="checkout-flow-1",
- network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]},
+ network={
+ "private_hosts": ["*.example.ts.net", "100.64.0.0/10"],
+ "proxy_routes": [
+ {
+ "hosts": ["string"],
+ "proxy": {
+ "id": "x",
+ "name": "x",
+ },
+ }
+ ],
+ },
profile={
"id": "id",
"name": "name",