From 82d9f8400a761a37debabeb79e6e5d8bec2cbe32 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 16 Sep 2026 17:50:00 +0000
Subject: [PATCH 1/4] feat: Return Link spend request errors directly
Stainless-Generated-From: 9598a45cc204663dc64d611c88b0a7a457b642f0
---
src/kernel/resources/vaults/items.py | 48 ++++++++++++-------
.../types/vaults/card_vault_item_spec.py | 5 +-
.../vaults/card_vault_item_spec_param.py | 5 +-
3 files changed, 40 insertions(+), 18 deletions(-)
diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py
index cd53e796..8c720ee5 100644
--- a/src/kernel/resources/vaults/items.py
+++ b/src/kernel/resources/vaults/items.py
@@ -409,8 +409,10 @@ def perform_operation(
device approval before native Square Pay. Keep the returned approval page open,
poll until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
- creation is rate limited, returns HTTP 429 with code
- `spend_request_rate_limited`; stop and back off before retrying.
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
@@ -454,8 +456,10 @@ def perform_operation(
device approval before native Square Pay. Keep the returned approval page open,
poll until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
- creation is rate limited, returns HTTP 429 with code
- `spend_request_rate_limited`; stop and back off before retrying.
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
@@ -500,8 +504,10 @@ def perform_operation(
device approval before native Square Pay. Keep the returned approval page open,
poll until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
- creation is rate limited, returns HTTP 429 with code
- `spend_request_rate_limited`; stop and back off before retrying.
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
@@ -555,8 +561,10 @@ def perform_operation(
device approval before native Square Pay. Keep the returned approval page open,
poll until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
- creation is rate limited, returns HTTP 429 with code
- `spend_request_rate_limited`; stop and back off before retrying.
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
@@ -1172,8 +1180,10 @@ async def perform_operation(
device approval before native Square Pay. Keep the returned approval page open,
poll until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
- creation is rate limited, returns HTTP 429 with code
- `spend_request_rate_limited`; stop and back off before retrying.
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
@@ -1217,8 +1227,10 @@ async def perform_operation(
device approval before native Square Pay. Keep the returned approval page open,
poll until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
- creation is rate limited, returns HTTP 429 with code
- `spend_request_rate_limited`; stop and back off before retrying.
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
@@ -1263,8 +1275,10 @@ async def perform_operation(
device approval before native Square Pay. Keep the returned approval page open,
poll until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
- creation is rate limited, returns HTTP 429 with code
- `spend_request_rate_limited`; stop and back off before retrying.
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
@@ -1318,8 +1332,10 @@ async def perform_operation(
device approval before native Square Pay. Keep the returned approval page open,
poll until ready_to_submit, then submit before preparation.expires_at. Unused
preparations expire automatically and cannot be reused. If spend-request
- creation is rate limited, returns HTTP 429 with code
- `spend_request_rate_limited`; stop and back off before retrying.
+ creation is rejected with a non-retryable provider error, the card item is
+ deleted and the provider's error code and message are returned. Rate limits
+ return HTTP 429 and retain the card item; stop, back off, and retry the same
+ authorize operation.
Fill returns a value-free execution result. Validation failures before writing
return 400 (invalid request or targets), 403 (access or destination denied), 404
diff --git a/src/kernel/types/vaults/card_vault_item_spec.py b/src/kernel/types/vaults/card_vault_item_spec.py
index d66a5453..0bb0b77a 100644
--- a/src/kernel/types/vaults/card_vault_item_spec.py
+++ b/src/kernel/types/vaults/card_vault_item_spec.py
@@ -59,7 +59,10 @@ class LinkCardVaultItemSpec(BaseModel):
"""Live payment card. Test-mode card creation is not supported."""
amount: int
- """Integer amount in minor currency units."""
+ """Integer amount in minor currency units.
+
+ Link permits at most 50000 per spend request.
+ """
context: str
diff --git a/src/kernel/types/vaults/card_vault_item_spec_param.py b/src/kernel/types/vaults/card_vault_item_spec_param.py
index 3cced6ea..a1680703 100644
--- a/src/kernel/types/vaults/card_vault_item_spec_param.py
+++ b/src/kernel/types/vaults/card_vault_item_spec_param.py
@@ -58,7 +58,10 @@ class LinkCardVaultItemSpec(TypedDict, total=False):
"""Live payment card. Test-mode card creation is not supported."""
amount: Required[int]
- """Integer amount in minor currency units."""
+ """Integer amount in minor currency units.
+
+ Link permits at most 50000 per spend request.
+ """
context: Required[str]
From 755dba5d1273ef9083ffbc4bc7f4adf30aea2fe9 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 16 Sep 2026 18:10:48 +0000
Subject: [PATCH 2/4] feat: Expose persistent Browser REPL through the API
Stainless-Generated-From: 1422c8f3402ddd8a238fda63746dda102aa53937
---
.stats.yml | 2 +-
api.md | 6 +
src/kernel/_client.py | 6 -
src/kernel/resources/browsers/browsers.py | 152 +++++++++++++++++-
src/kernel/types/__init__.py | 5 +
src/kernel/types/browser_repl_content.py | 14 ++
.../types/browser_repl_image_content.py | 15 ++
src/kernel/types/browser_repl_params.py | 25 +++
src/kernel/types/browser_repl_result.py | 43 +++++
src/kernel/types/browser_repl_text_content.py | 19 +++
tests/api_resources/test_browsers.py | 115 +++++++++++++
11 files changed, 391 insertions(+), 11 deletions(-)
create mode 100644 src/kernel/types/browser_repl_content.py
create mode 100644 src/kernel/types/browser_repl_image_content.py
create mode 100644 src/kernel/types/browser_repl_params.py
create mode 100644 src/kernel/types/browser_repl_result.py
create mode 100644 src/kernel/types/browser_repl_text_content.py
diff --git a/.stats.yml b/.stats.yml
index 88ce9db5..40c9d3b0 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1 @@
-configured_endpoints: 164
+configured_endpoints: 165
diff --git a/api.md b/api.md
index 0679de62..90bb1465 100644
--- a/api.md
+++ b/api.md
@@ -124,6 +124,11 @@ from kernel.types import (
BrowserProxy,
BrowserProxyConfig,
BrowserProxyMode,
+ BrowserReplContent,
+ BrowserReplImageContent,
+ BrowserReplRequest,
+ BrowserReplResult,
+ BrowserReplTextContent,
BrowserUsage,
Profile,
Tags,
@@ -145,6 +150,7 @@ Methods:
- client.browsers.curl(id_or_name, \*\*params) -> BrowserCurlResponse
- client.browsers.delete_by_id(id_or_name) -> None
- client.browsers.load_extensions(id_or_name, \*\*params) -> None
+- client.browsers.repl(id_or_name, \*\*params) -> BrowserReplResult
## Telemetry
diff --git a/src/kernel/_client.py b/src/kernel/_client.py
index f4c9e48f..13a6e3dc 100644
--- a/src/kernel/_client.py
+++ b/src/kernel/_client.py
@@ -247,7 +247,6 @@ def config_registry(self) -> ConfigRegistryResource:
@cached_property
def browsers(self) -> BrowsersResource:
- """Create and manage browser sessions."""
from .resources.browsers import BrowsersResource
return BrowsersResource(self)
@@ -652,7 +651,6 @@ def config_registry(self) -> AsyncConfigRegistryResource:
@cached_property
def browsers(self) -> AsyncBrowsersResource:
- """Create and manage browser sessions."""
from .resources.browsers import AsyncBrowsersResource
return AsyncBrowsersResource(self)
@@ -959,7 +957,6 @@ def config_registry(self) -> config_registry.ConfigRegistryResourceWithRawRespon
@cached_property
def browsers(self) -> browsers.BrowsersResourceWithRawResponse:
- """Create and manage browser sessions."""
from .resources.browsers import BrowsersResourceWithRawResponse
return BrowsersResourceWithRawResponse(self._client.browsers)
@@ -1098,7 +1095,6 @@ def config_registry(self) -> config_registry.AsyncConfigRegistryResourceWithRawR
@cached_property
def browsers(self) -> browsers.AsyncBrowsersResourceWithRawResponse:
- """Create and manage browser sessions."""
from .resources.browsers import AsyncBrowsersResourceWithRawResponse
return AsyncBrowsersResourceWithRawResponse(self._client.browsers)
@@ -1237,7 +1233,6 @@ def config_registry(self) -> config_registry.ConfigRegistryResourceWithStreaming
@cached_property
def browsers(self) -> browsers.BrowsersResourceWithStreamingResponse:
- """Create and manage browser sessions."""
from .resources.browsers import BrowsersResourceWithStreamingResponse
return BrowsersResourceWithStreamingResponse(self._client.browsers)
@@ -1376,7 +1371,6 @@ def config_registry(self) -> config_registry.AsyncConfigRegistryResourceWithStre
@cached_property
def browsers(self) -> browsers.AsyncBrowsersResourceWithStreamingResponse:
- """Create and manage browser sessions."""
from .resources.browsers import AsyncBrowsersResourceWithStreamingResponse
return AsyncBrowsersResourceWithStreamingResponse(self._client.browsers)
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index 49dc4063..d2e519bd 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -36,6 +36,7 @@
BrowserMemoryRequest,
browser_curl_params,
browser_list_params,
+ browser_repl_params,
browser_create_params,
browser_update_params,
browser_retrieve_params,
@@ -95,6 +96,7 @@
from ...pagination import SyncOffsetPagination, AsyncOffsetPagination
from ..._base_client import AsyncPaginator, make_request_options
from ...types.tags_param import TagsParam
+from ...types.browser_repl_result import BrowserReplResult
from ...types.browser_curl_response import BrowserCurlResponse
from ...types.browser_list_response import BrowserListResponse
from ...types.vault_reference_param import VaultReferenceParam
@@ -118,8 +120,6 @@
class BrowsersResource(SyncAPIResource):
- """Create and manage browser sessions."""
-
@cached_property
def telemetry(self) -> TelemetryResource:
"""
@@ -753,10 +753,75 @@ def load_extensions(
cast_to=NoneType,
)
+ def repl(
+ self,
+ id_or_name: str,
+ *,
+ code: str,
+ reset: bool | Omit = omit,
+ timeout_sec: int | 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,
+ ) -> BrowserReplResult:
+ """
+ Execute JavaScript in a persistent Node.js runtime inside the browser VM.
+ Top-level bindings, closures, mutations, and dynamically imported modules
+ persist across calls until the REPL is reset or replaced. Start with
+ `repl.help()` to list available methods, or call `repl.help("click")` for
+ detailed help.
+
+ Expression values are ignored. Emit ordered text or image output with
+ `repl.write(...)`, console methods, or `repl.emitImage(...)`. The runtime also
+ exposes browser-control helpers, WebMCP, Patchright, Playwright, and raw CDP.
+
+ Executions are serialized. A timeout, crash, OOM, or protocol failure terminates
+ the REPL and changes its `repl_id`. This is unrestricted code execution inside
+ the browser VM and is not sandboxed.
+
+ Args:
+ code: JavaScript evaluated in a persistent Node.js runtime. Top-level bindings persist
+ until the browser VM's API process exits, the REPL is reset, or the REPL is
+ terminated after a crash or timeout. Static top-level imports are unsupported;
+ use dynamic `import()`. Expression values are ignored; emit output with
+ `repl.write(...)`, console methods, or `repl.emitImage(...)`. May be empty only
+ when `reset` is true.
+
+ reset: Terminate the current REPL, start a fresh one, and then evaluate code.
+
+ timeout_sec: Maximum execution time in seconds. Default is 60.
+
+ 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}/repl", id_or_name=id_or_name),
+ body=maybe_transform(
+ {
+ "code": code,
+ "reset": reset,
+ "timeout_sec": timeout_sec,
+ },
+ browser_repl_params.BrowserReplParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=BrowserReplResult,
+ )
-class AsyncBrowsersResource(AsyncAPIResource):
- """Create and manage browser sessions."""
+class AsyncBrowsersResource(AsyncAPIResource):
@cached_property
def telemetry(self) -> AsyncTelemetryResource:
"""
@@ -1390,6 +1455,73 @@ async def load_extensions(
cast_to=NoneType,
)
+ async def repl(
+ self,
+ id_or_name: str,
+ *,
+ code: str,
+ reset: bool | Omit = omit,
+ timeout_sec: int | 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,
+ ) -> BrowserReplResult:
+ """
+ Execute JavaScript in a persistent Node.js runtime inside the browser VM.
+ Top-level bindings, closures, mutations, and dynamically imported modules
+ persist across calls until the REPL is reset or replaced. Start with
+ `repl.help()` to list available methods, or call `repl.help("click")` for
+ detailed help.
+
+ Expression values are ignored. Emit ordered text or image output with
+ `repl.write(...)`, console methods, or `repl.emitImage(...)`. The runtime also
+ exposes browser-control helpers, WebMCP, Patchright, Playwright, and raw CDP.
+
+ Executions are serialized. A timeout, crash, OOM, or protocol failure terminates
+ the REPL and changes its `repl_id`. This is unrestricted code execution inside
+ the browser VM and is not sandboxed.
+
+ Args:
+ code: JavaScript evaluated in a persistent Node.js runtime. Top-level bindings persist
+ until the browser VM's API process exits, the REPL is reset, or the REPL is
+ terminated after a crash or timeout. Static top-level imports are unsupported;
+ use dynamic `import()`. Expression values are ignored; emit output with
+ `repl.write(...)`, console methods, or `repl.emitImage(...)`. May be empty only
+ when `reset` is true.
+
+ reset: Terminate the current REPL, start a fresh one, and then evaluate code.
+
+ timeout_sec: Maximum execution time in seconds. Default is 60.
+
+ 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}/repl", id_or_name=id_or_name),
+ body=await async_maybe_transform(
+ {
+ "code": code,
+ "reset": reset,
+ "timeout_sec": timeout_sec,
+ },
+ browser_repl_params.BrowserReplParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=BrowserReplResult,
+ )
+
class BrowsersResourceWithRawResponse:
def __init__(self, browsers: BrowsersResource) -> None:
@@ -1416,6 +1548,9 @@ def __init__(self, browsers: BrowsersResource) -> None:
self.load_extensions = to_raw_response_wrapper(
browsers.load_extensions,
)
+ self.repl = to_raw_response_wrapper(
+ browsers.repl,
+ )
@cached_property
def telemetry(self) -> TelemetryResourceWithRawResponse:
@@ -1485,6 +1620,9 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None:
self.load_extensions = async_to_raw_response_wrapper(
browsers.load_extensions,
)
+ self.repl = async_to_raw_response_wrapper(
+ browsers.repl,
+ )
@cached_property
def telemetry(self) -> AsyncTelemetryResourceWithRawResponse:
@@ -1554,6 +1692,9 @@ def __init__(self, browsers: BrowsersResource) -> None:
self.load_extensions = to_streamed_response_wrapper(
browsers.load_extensions,
)
+ self.repl = to_streamed_response_wrapper(
+ browsers.repl,
+ )
@cached_property
def telemetry(self) -> TelemetryResourceWithStreamingResponse:
@@ -1623,6 +1764,9 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None:
self.load_extensions = async_to_streamed_response_wrapper(
browsers.load_extensions,
)
+ self.repl = async_to_streamed_response_wrapper(
+ browsers.repl,
+ )
@cached_property
def telemetry(self) -> AsyncTelemetryResourceWithStreamingResponse:
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index 70e9581a..0d25350f 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -48,6 +48,8 @@
from .api_key_list_params import APIKeyListParams as APIKeyListParams
from .browser_curl_params import BrowserCurlParams as BrowserCurlParams
from .browser_list_params import BrowserListParams as BrowserListParams
+from .browser_repl_params import BrowserReplParams as BrowserReplParams
+from .browser_repl_result import BrowserReplResult as BrowserReplResult
from .credential_provider import CredentialProvider as CredentialProvider
from .profile_list_params import ProfileListParams as ProfileListParams
from .project_list_params import ProjectListParams as ProjectListParams
@@ -56,6 +58,7 @@
from .proxy_update_params import ProxyUpdateParams as ProxyUpdateParams
from .vault_upsert_params import VaultUpsertParams as VaultUpsertParams
from .browser_proxy_config import BrowserProxyConfig as BrowserProxyConfig
+from .browser_repl_content import BrowserReplContent as BrowserReplContent
from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse
from .api_key_create_params import APIKeyCreateParams as APIKeyCreateParams
from .api_key_rotate_params import APIKeyRotateParams as APIKeyRotateParams
@@ -104,12 +107,14 @@
from .invocation_follow_params import InvocationFollowParams as InvocationFollowParams
from .invocation_list_response import InvocationListResponse as InvocationListResponse
from .invocation_update_params import InvocationUpdateParams as InvocationUpdateParams
+from .browser_repl_text_content import BrowserReplTextContent as BrowserReplTextContent
from .browser_retrieve_response import BrowserRetrieveResponse as BrowserRetrieveResponse
from .extension_upload_response import ExtensionUploadResponse as ExtensionUploadResponse
from .browser_pool_create_params import BrowserPoolCreateParams as BrowserPoolCreateParams
from .browser_pool_delete_params import BrowserPoolDeleteParams as BrowserPoolDeleteParams
from .browser_pool_update_params import BrowserPoolUpdateParams as BrowserPoolUpdateParams
from .browser_proxy_config_param import BrowserProxyConfigParam as BrowserProxyConfigParam
+from .browser_repl_image_content import BrowserReplImageContent as BrowserReplImageContent
from .deployment_create_response import DeploymentCreateResponse as DeploymentCreateResponse
from .deployment_follow_response import DeploymentFollowResponse as DeploymentFollowResponse
from .invocation_create_response import InvocationCreateResponse as InvocationCreateResponse
diff --git a/src/kernel/types/browser_repl_content.py b/src/kernel/types/browser_repl_content.py
new file mode 100644
index 00000000..70af04d7
--- /dev/null
+++ b/src/kernel/types/browser_repl_content.py
@@ -0,0 +1,14 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Union
+from typing_extensions import Annotated, TypeAlias
+
+from .._utils import PropertyInfo
+from .browser_repl_text_content import BrowserReplTextContent
+from .browser_repl_image_content import BrowserReplImageContent
+
+__all__ = ["BrowserReplContent"]
+
+BrowserReplContent: TypeAlias = Annotated[
+ Union[BrowserReplTextContent, BrowserReplImageContent], PropertyInfo(discriminator="type")
+]
diff --git a/src/kernel/types/browser_repl_image_content.py b/src/kernel/types/browser_repl_image_content.py
new file mode 100644
index 00000000..4af25aa9
--- /dev/null
+++ b/src/kernel/types/browser_repl_image_content.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from .._models import BaseModel
+
+__all__ = ["BrowserReplImageContent"]
+
+
+class BrowserReplImageContent(BaseModel):
+ data_b64: str
+
+ mime_type: str
+
+ type: Literal["image"]
diff --git a/src/kernel/types/browser_repl_params.py b/src/kernel/types/browser_repl_params.py
new file mode 100644
index 00000000..28df3a41
--- /dev/null
+++ b/src/kernel/types/browser_repl_params.py
@@ -0,0 +1,25 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+__all__ = ["BrowserReplParams"]
+
+
+class BrowserReplParams(TypedDict, total=False):
+ code: Required[str]
+ """JavaScript evaluated in a persistent Node.js runtime.
+
+ Top-level bindings persist until the browser VM's API process exits, the REPL is
+ reset, or the REPL is terminated after a crash or timeout. Static top-level
+ imports are unsupported; use dynamic `import()`. Expression values are ignored;
+ emit output with `repl.write(...)`, console methods, or `repl.emitImage(...)`.
+ May be empty only when `reset` is true.
+ """
+
+ reset: bool
+ """Terminate the current REPL, start a fresh one, and then evaluate code."""
+
+ timeout_sec: int
+ """Maximum execution time in seconds. Default is 60."""
diff --git a/src/kernel/types/browser_repl_result.py b/src/kernel/types/browser_repl_result.py
new file mode 100644
index 00000000..d5815501
--- /dev/null
+++ b/src/kernel/types/browser_repl_result.py
@@ -0,0 +1,43 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+
+from .._models import BaseModel
+from .browser_repl_content import BrowserReplContent
+
+__all__ = ["BrowserReplResult"]
+
+
+class BrowserReplResult(BaseModel):
+ """Result of Browser REPL code execution."""
+
+ repl_id: str
+ """CUID2 identifying the exact state-holding REPL process used for this execution.
+
+ Stable across calls and Chromium reconnects; changes after an API restart,
+ explicit reset, execution timeout, or REPL crash.
+ """
+
+ success: bool
+ """Whether the code executed successfully."""
+
+ content: Optional[List[BrowserReplContent]] = None
+ """Optional ordered text/image output produced by the execution."""
+
+ content_truncated: Optional[bool] = None
+ """True if text or image output was dropped or truncated due to response limits."""
+
+ duration_ms: Optional[int] = None
+ """Wall-clock execution time in milliseconds."""
+
+ error: Optional[str] = None
+ """Error message if execution failed."""
+
+ repl_terminated: Optional[bool] = None
+ """
+ True if the REPL identified by `repl_id` was terminated by this request. The
+ next request lazily starts a fresh REPL with a new `repl_id`.
+ """
+
+ stack: Optional[str] = None
+ """Stack trace if execution failed."""
diff --git a/src/kernel/types/browser_repl_text_content.py b/src/kernel/types/browser_repl_text_content.py
new file mode 100644
index 00000000..034d3691
--- /dev/null
+++ b/src/kernel/types/browser_repl_text_content.py
@@ -0,0 +1,19 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from .._models import BaseModel
+
+__all__ = ["BrowserReplTextContent"]
+
+
+class BrowserReplTextContent(BaseModel):
+ channel: Literal["write", "stdout", "stderr"]
+ """
+ `write` is emitted by `repl.write`; `stdout` and `stderr` are emitted by console
+ methods.
+ """
+
+ text: str
+
+ type: Literal["text"]
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index 28189f15..ffa5f2e9 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -10,6 +10,7 @@
from kernel import Kernel, AsyncKernel
from tests.utils import assert_matches_type
from kernel.types import (
+ BrowserReplResult,
BrowserCurlResponse,
BrowserListResponse,
BrowserCreateResponse,
@@ -492,6 +493,63 @@ def test_path_params_load_extensions(self, client: Kernel) -> None:
],
)
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_repl(self, client: Kernel) -> None:
+ browser = client.browsers.repl(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ code="code",
+ )
+ assert_matches_type(BrowserReplResult, browser, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_repl_with_all_params(self, client: Kernel) -> None:
+ browser = client.browsers.repl(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ code="code",
+ reset=True,
+ timeout_sec=1,
+ )
+ assert_matches_type(BrowserReplResult, browser, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_repl(self, client: Kernel) -> None:
+ response = client.browsers.with_raw_response.repl(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ code="code",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ browser = response.parse()
+ assert_matches_type(BrowserReplResult, browser, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_repl(self, client: Kernel) -> None:
+ with client.browsers.with_streaming_response.repl(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ code="code",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ browser = response.parse()
+ assert_matches_type(BrowserReplResult, browser, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_repl(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.browsers.with_raw_response.repl(
+ id_or_name="",
+ code="code",
+ )
+
class TestAsyncBrowsers:
parametrize = pytest.mark.parametrize(
@@ -965,3 +1023,60 @@ async def test_path_params_load_extensions(self, async_client: AsyncKernel) -> N
}
],
)
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_repl(self, async_client: AsyncKernel) -> None:
+ browser = await async_client.browsers.repl(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ code="code",
+ )
+ assert_matches_type(BrowserReplResult, browser, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_repl_with_all_params(self, async_client: AsyncKernel) -> None:
+ browser = await async_client.browsers.repl(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ code="code",
+ reset=True,
+ timeout_sec=1,
+ )
+ assert_matches_type(BrowserReplResult, browser, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_repl(self, async_client: AsyncKernel) -> None:
+ response = await async_client.browsers.with_raw_response.repl(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ code="code",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ browser = await response.parse()
+ assert_matches_type(BrowserReplResult, browser, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_repl(self, async_client: AsyncKernel) -> None:
+ async with async_client.browsers.with_streaming_response.repl(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ code="code",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ browser = await response.parse()
+ assert_matches_type(BrowserReplResult, browser, 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_repl(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.with_raw_response.repl(
+ id_or_name="",
+ code="code",
+ )
From 12ced8647c23aa43c625b288ed48c7cdf8da9125 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 16 Sep 2026 18:31:29 +0000
Subject: [PATCH 3/4] feat: Classify unevaluable config registry targets
Stainless-Generated-From: 89ffaef01313311d22f91e6eba840096c085be85
---
src/kernel/types/no_recommendation.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/kernel/types/no_recommendation.py b/src/kernel/types/no_recommendation.py
index 05faec86..40edb9f0 100644
--- a/src/kernel/types/no_recommendation.py
+++ b/src/kernel/types/no_recommendation.py
@@ -8,7 +8,7 @@
class NoRecommendation(BaseModel):
- code: Literal["proxy_restricted", "no_working_configuration", "inconclusive"]
+ code: Literal["proxy_restricted", "target_not_evaluable", "no_working_configuration", "inconclusive"]
"""
Machine-readable reason Kernel cannot currently provide a config recommendation.
"""
From 7c83d879bad3151782085d507aad48c3b1ea5b85 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 16 Sep 2026 18:36:15 +0000
Subject: [PATCH 4/4] release: 0.107.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 9 +++++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index f371d275..bd34dce0 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.106.0"
+ ".": "0.107.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cb10a2df..235126cc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## [0.107.0](https://github.com/kernel/kernel-python-sdk/compare/v0.106.0...v0.107.0) (2026-09-16)
+
+
+### Features
+
+* Classify unevaluable config registry targets ([12ced86](https://github.com/kernel/kernel-python-sdk/commit/12ced8647c23aa43c625b288ed48c7cdf8da9125))
+* Expose persistent Browser REPL through the API ([755dba5](https://github.com/kernel/kernel-python-sdk/commit/755dba5d1273ef9083ffbc4bc7f4adf30aea2fe9))
+* Return Link spend request errors directly ([82d9f84](https://github.com/kernel/kernel-python-sdk/commit/82d9f8400a761a37debabeb79e6e5d8bec2cbe32))
+
## 0.106.0 (2026-09-16)
Full Changelog: [v0.104.0...v0.106.0](https://github.com/kernel/kernel-python-sdk/compare/v0.104.0...v0.106.0)
diff --git a/pyproject.toml b/pyproject.toml
index a89b3709..885ce9e3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.106.0"
+version = "0.107.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 66f10fd8..9445a514 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.106.0" # x-release-please-version
+__version__ = "0.107.0" # x-release-please-version