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