From 7aff31c3e715e7b32d87167bbbf48d6abcd88131 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 07:27:20 +0000 Subject: [PATCH 01/12] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 33f284f..324dd3f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-4ce09d1a7546ab36f578cb27d819187eeb90c580b11834c7ff7a375aa22f9a20.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-4ce09d1a7546ab36f578cb27d819187eeb90c580b11834c7ff7a375aa22f9a20.yml openapi_spec_hash: 1043ab2d699f6c828680c3352cd4cece config_hash: 08d55086449943a8fec212b870061a3f From 82ae224a85a22b16e48a26a8058cc992ef772cf2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:55:39 +0000 Subject: [PATCH 02/12] feat: Add 'switch' MFA option type for generic method-switcher links --- .stats.yml | 4 ++-- src/kernel/types/auth/connection_follow_response.py | 11 +++++++---- src/kernel/types/auth/managed_auth.py | 11 +++++++---- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/.stats.yml b/.stats.yml index 324dd3f..d0f4a0f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-4ce09d1a7546ab36f578cb27d819187eeb90c580b11834c7ff7a375aa22f9a20.yml -openapi_spec_hash: 1043ab2d699f6c828680c3352cd4cece +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-81659c4d18e7992d17a0930d6c13c8592a0ff5bb974ea9e2e4b3f46d43b117d2.yml +openapi_spec_hash: f3d12a3a0a5e9ce711fb1c571ee36f9c config_hash: 08d55086449943a8fec212b870061a3f diff --git a/src/kernel/types/auth/connection_follow_response.py b/src/kernel/types/auth/connection_follow_response.py index a9bda35..7cabc1c 100644 --- a/src/kernel/types/auth/connection_follow_response.py +++ b/src/kernel/types/auth/connection_follow_response.py @@ -40,7 +40,7 @@ class ManagedAuthStateEventDiscoveredField(BaseModel): "Enter the phone ending in (**_) _**-\\**\\**92") """ - linked_mfa_type: Optional[Literal["sms", "call", "email", "totp", "push", "password"]] = None + linked_mfa_type: Optional[Literal["sms", "call", "email", "totp", "push", "password", "switch"]] = None """ If this field is associated with an MFA option, the type of that option (e.g., password field linked to "Enter password" option) @@ -59,9 +59,12 @@ class ManagedAuthStateEventMfaOption(BaseModel): label: str """The visible option text""" - type: Literal["sms", "call", "email", "totp", "push", "password"] - """ - The MFA delivery method type (includes password for auth method selection pages) + type: Literal["sms", "call", "email", "totp", "push", "password", "switch"] + """The MFA delivery method type. + + Includes 'password' for auth method selection pages and 'switch' for generic + method-switcher links like "Use another method" that do not name a specific + method. """ description: Optional[str] = None diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py index 59b2d57..622c3bd 100644 --- a/src/kernel/types/auth/managed_auth.py +++ b/src/kernel/types/auth/managed_auth.py @@ -52,7 +52,7 @@ class DiscoveredField(BaseModel): "Enter the phone ending in (**_) _**-\\**\\**92") """ - linked_mfa_type: Optional[Literal["sms", "call", "email", "totp", "push", "password"]] = None + linked_mfa_type: Optional[Literal["sms", "call", "email", "totp", "push", "password", "switch"]] = None """ If this field is associated with an MFA option, the type of that option (e.g., password field linked to "Enter password" option) @@ -71,9 +71,12 @@ class MfaOption(BaseModel): label: str """The visible option text""" - type: Literal["sms", "call", "email", "totp", "push", "password"] - """ - The MFA delivery method type (includes password for auth method selection pages) + type: Literal["sms", "call", "email", "totp", "push", "password", "switch"] + """The MFA delivery method type. + + Includes 'password' for auth method selection pages and 'switch' for generic + method-switcher links like "Use another method" that do not name a specific + method. """ description: Optional[str] = None From d6e80232172ca69b9e24bc5d9a52ec14f52d46d8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 03:39:52 +0000 Subject: [PATCH 03/12] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index d0f4a0f..d2492a7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-81659c4d18e7992d17a0930d6c13c8592a0ff5bb974ea9e2e4b3f46d43b117d2.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-718b49461ceaa1d6cac7854c29dfef9036a83f6632e756c9d1ecf31fd77c57f6.yml openapi_spec_hash: f3d12a3a0a5e9ce711fb1c571ee36f9c config_hash: 08d55086449943a8fec212b870061a3f From 27c799be452cb66af59b79d24b3d5147a85b70f7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 03:43:46 +0000 Subject: [PATCH 04/12] chore(internal): reformat pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 13b5e4c..5d43220 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -168,7 +168,7 @@ show_error_codes = true # # We also exclude our `tests` as mypy doesn't always infer # types correctly and Pyright will still catch any type errors. -exclude = ['src/kernel/_files.py', '_dev/.*.py', 'tests/.*'] +exclude = ["src/kernel/_files.py", "_dev/.*.py", "tests/.*"] strict_equality = true implicit_reexport = true From cacb057a5d1d3bcb26b6df1f6fe83067f936d642 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 2 May 2026 16:48:46 +0000 Subject: [PATCH 05/12] feat(api): server-side search on GET /projects --- .stats.yml | 4 ++-- src/kernel/resources/projects/projects.py | 8 ++++++++ src/kernel/types/project_list_params.py | 3 +++ tests/api_resources/test_projects.py | 2 ++ 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index d2492a7..229ced7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-718b49461ceaa1d6cac7854c29dfef9036a83f6632e756c9d1ecf31fd77c57f6.yml -openapi_spec_hash: f3d12a3a0a5e9ce711fb1c571ee36f9c +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-86e061884d273a27064593a0de3a4ba366a12a1001181741addb4f781be18ec9.yml +openapi_spec_hash: e0a4ddf4a3302599376756127099488c config_hash: 08d55086449943a8fec212b870061a3f diff --git a/src/kernel/resources/projects/projects.py b/src/kernel/resources/projects/projects.py index 9a7f13b..f73e70d 100644 --- a/src/kernel/resources/projects/projects.py +++ b/src/kernel/resources/projects/projects.py @@ -179,6 +179,7 @@ def list( *, limit: int | Omit = omit, offset: int | Omit = omit, + query: 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. extra_headers: Headers | None = None, @@ -194,6 +195,8 @@ def list( offset: Number of results to skip + query: Case-insensitive substring match against project name + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -214,6 +217,7 @@ def list( { "limit": limit, "offset": offset, + "query": query, }, project_list_params.ProjectListParams, ), @@ -404,6 +408,7 @@ def list( *, limit: int | Omit = omit, offset: int | Omit = omit, + query: 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. extra_headers: Headers | None = None, @@ -419,6 +424,8 @@ def list( offset: Number of results to skip + query: Case-insensitive substring match against project name + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -439,6 +446,7 @@ def list( { "limit": limit, "offset": offset, + "query": query, }, project_list_params.ProjectListParams, ), diff --git a/src/kernel/types/project_list_params.py b/src/kernel/types/project_list_params.py index ea10f07..9e740e6 100644 --- a/src/kernel/types/project_list_params.py +++ b/src/kernel/types/project_list_params.py @@ -13,3 +13,6 @@ class ProjectListParams(TypedDict, total=False): offset: int """Number of results to skip""" + + query: str + """Case-insensitive substring match against project name""" diff --git a/tests/api_resources/test_projects.py b/tests/api_resources/test_projects.py index 488c191..bb30145 100644 --- a/tests/api_resources/test_projects.py +++ b/tests/api_resources/test_projects.py @@ -158,6 +158,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None: project = client.projects.list( limit=100, offset=0, + query="query", ) assert_matches_type(SyncOffsetPagination[Project], project, path=["response"]) @@ -371,6 +372,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N project = await async_client.projects.list( limit=100, offset=0, + query="query", ) assert_matches_type(AsyncOffsetPagination[Project], project, path=["response"]) From c510a515b3c57846f0e681638167ba87b8ee15c3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 21:20:12 +0000 Subject: [PATCH 06/12] feat: Scope name uniqueness to project for profiles, session_pools, extensions, credentials --- .stats.yml | 4 ++-- src/kernel/resources/browser_pools.py | 8 ++++---- src/kernel/resources/credentials.py | 4 ++-- src/kernel/resources/extensions.py | 4 ++-- src/kernel/resources/profiles.py | 4 ++-- src/kernel/types/browser_pool.py | 2 +- src/kernel/types/browser_pool_create_params.py | 2 +- src/kernel/types/browser_pool_update_params.py | 2 +- src/kernel/types/credential.py | 2 +- src/kernel/types/credential_create_params.py | 2 +- src/kernel/types/extension_list_response.py | 2 +- src/kernel/types/extension_upload_params.py | 2 +- src/kernel/types/extension_upload_response.py | 2 +- src/kernel/types/profile_create_params.py | 2 +- 14 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.stats.yml b/.stats.yml index 229ced7..797624b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-86e061884d273a27064593a0de3a4ba366a12a1001181741addb4f781be18ec9.yml -openapi_spec_hash: e0a4ddf4a3302599376756127099488c +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-b51c72a040c8dfea9c0693de6631feabfffe42922d5feb60b4ac0ee5c83bb8f4.yml +openapi_spec_hash: 8b671cfe4debe8d9ad7c39ba5b0eaf6d config_hash: 08d55086449943a8fec212b870061a3f diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index 045737a..6c265cd 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -100,7 +100,7 @@ def create( kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live view. - name: Optional name for the browser pool. Must be unique within the organization. + name: Optional name for the browser pool. Must be unique within the project. profile: Profile selection for the browser session. Provide either id or name. If specified, the matching profile will be loaded into the browser session. @@ -243,7 +243,7 @@ def update( kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live view. - name: Optional name for the browser pool. Must be unique within the organization. + name: Optional name for the browser pool. Must be unique within the project. profile: Profile selection for the browser session. Provide either id or name. If specified, the matching profile will be loaded into the browser session. @@ -562,7 +562,7 @@ async def create( kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live view. - name: Optional name for the browser pool. Must be unique within the organization. + name: Optional name for the browser pool. Must be unique within the project. profile: Profile selection for the browser session. Provide either id or name. If specified, the matching profile will be loaded into the browser session. @@ -705,7 +705,7 @@ async def update( kiosk_mode: If true, launches the browser in kiosk mode to hide address bar and tabs in live view. - name: Optional name for the browser pool. Must be unique within the organization. + name: Optional name for the browser pool. Must be unique within the project. profile: Profile selection for the browser session. Provide either id or name. If specified, the matching profile will be loaded into the browser session. diff --git a/src/kernel/resources/credentials.py b/src/kernel/resources/credentials.py index 093fcc5..7d00faa 100644 --- a/src/kernel/resources/credentials.py +++ b/src/kernel/resources/credentials.py @@ -68,7 +68,7 @@ def create( Args: domain: Target domain this credential is for - name: Unique name for the credential within the organization + name: Unique name for the credential within the project values: Field name to value mapping (e.g., username, password) @@ -365,7 +365,7 @@ async def create( Args: domain: Target domain this credential is for - name: Unique name for the credential within the organization + name: Unique name for the credential within the project values: Field name to value mapping (e.g., username, password) diff --git a/src/kernel/resources/extensions.py b/src/kernel/resources/extensions.py index 9ea6e0b..d5cf0eb 100644 --- a/src/kernel/resources/extensions.py +++ b/src/kernel/resources/extensions.py @@ -211,7 +211,7 @@ def upload( Args: file: ZIP file containing the browser extension. - name: Optional unique name within the organization to reference this extension. + name: Optional unique name within the project to reference this extension. extra_headers: Send extra headers @@ -421,7 +421,7 @@ async def upload( Args: file: ZIP file containing the browser extension. - name: Optional unique name within the organization to reference this extension. + name: Optional unique name within the project to reference this extension. extra_headers: Send extra headers diff --git a/src/kernel/resources/profiles.py b/src/kernel/resources/profiles.py index c20ffb7..4083f12 100644 --- a/src/kernel/resources/profiles.py +++ b/src/kernel/resources/profiles.py @@ -68,7 +68,7 @@ def create( sessions. Args: - name: Optional name of the profile. Must be unique within the organization. + name: Optional name of the profile. Must be unique within the project. extra_headers: Send extra headers @@ -278,7 +278,7 @@ async def create( sessions. Args: - name: Optional name of the profile. Must be unique within the organization. + name: Optional name of the profile. Must be unique within the project. extra_headers: Send extra headers diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py index 8ca0dc4..807c73b 100644 --- a/src/kernel/types/browser_pool.py +++ b/src/kernel/types/browser_pool.py @@ -48,7 +48,7 @@ class BrowserPoolConfig(BaseModel): """ name: Optional[str] = None - """Optional name for the browser pool. Must be unique within the organization.""" + """Optional name for the browser pool. Must be unique within the project.""" profile: Optional[BrowserProfile] = None """Profile selection for the browser session. diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py index ecfb888..cab4e13 100644 --- a/src/kernel/types/browser_pool_create_params.py +++ b/src/kernel/types/browser_pool_create_params.py @@ -47,7 +47,7 @@ class BrowserPoolCreateParams(TypedDict, total=False): """ name: str - """Optional name for the browser pool. Must be unique within the organization.""" + """Optional name for the browser pool. Must be unique within the project.""" profile: BrowserProfile """Profile selection for the browser session. diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py index e34664a..7ddd131 100644 --- a/src/kernel/types/browser_pool_update_params.py +++ b/src/kernel/types/browser_pool_update_params.py @@ -53,7 +53,7 @@ class BrowserPoolUpdateParams(TypedDict, total=False): """ name: str - """Optional name for the browser pool. Must be unique within the organization.""" + """Optional name for the browser pool. Must be unique within the project.""" profile: BrowserProfile """Profile selection for the browser session. diff --git a/src/kernel/types/credential.py b/src/kernel/types/credential.py index bbf2af5..323617c 100644 --- a/src/kernel/types/credential.py +++ b/src/kernel/types/credential.py @@ -21,7 +21,7 @@ class Credential(BaseModel): """Target domain this credential is for""" name: str - """Unique name for the credential within the organization""" + """Unique name for the credential within the project""" updated_at: datetime """When the credential was last updated""" diff --git a/src/kernel/types/credential_create_params.py b/src/kernel/types/credential_create_params.py index 94964b9..9d30684 100644 --- a/src/kernel/types/credential_create_params.py +++ b/src/kernel/types/credential_create_params.py @@ -13,7 +13,7 @@ class CredentialCreateParams(TypedDict, total=False): """Target domain this credential is for""" name: Required[str] - """Unique name for the credential within the organization""" + """Unique name for the credential within the project""" values: Required[Dict[str, str]] """Field name to value mapping (e.g., username, password)""" diff --git a/src/kernel/types/extension_list_response.py b/src/kernel/types/extension_list_response.py index 79a5c99..bf9e544 100644 --- a/src/kernel/types/extension_list_response.py +++ b/src/kernel/types/extension_list_response.py @@ -27,7 +27,7 @@ class ExtensionListResponseItem(BaseModel): name: Optional[str] = None """Optional, easier-to-reference name for the extension. - Must be unique within the organization. + Must be unique within the project. """ diff --git a/src/kernel/types/extension_upload_params.py b/src/kernel/types/extension_upload_params.py index d36dde3..ab44d3e 100644 --- a/src/kernel/types/extension_upload_params.py +++ b/src/kernel/types/extension_upload_params.py @@ -14,4 +14,4 @@ class ExtensionUploadParams(TypedDict, total=False): """ZIP file containing the browser extension.""" name: str - """Optional unique name within the organization to reference this extension.""" + """Optional unique name within the project to reference this extension.""" diff --git a/src/kernel/types/extension_upload_response.py b/src/kernel/types/extension_upload_response.py index 1b3be22..068b25d 100644 --- a/src/kernel/types/extension_upload_response.py +++ b/src/kernel/types/extension_upload_response.py @@ -26,5 +26,5 @@ class ExtensionUploadResponse(BaseModel): name: Optional[str] = None """Optional, easier-to-reference name for the extension. - Must be unique within the organization. + Must be unique within the project. """ diff --git a/src/kernel/types/profile_create_params.py b/src/kernel/types/profile_create_params.py index 0b2b12a..bca1e17 100644 --- a/src/kernel/types/profile_create_params.py +++ b/src/kernel/types/profile_create_params.py @@ -9,4 +9,4 @@ class ProfileCreateParams(TypedDict, total=False): name: str - """Optional name of the profile. Must be unique within the organization.""" + """Optional name of the profile. Must be unique within the project.""" From fb5340dbf9ea49393d87b5760a2efaaf932c9442 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 9 May 2026 03:18:39 +0000 Subject: [PATCH 07/12] fix(client): add missing f-string prefix in file type error message --- src/kernel/_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kernel/_files.py b/src/kernel/_files.py index f30ac58..3fc9f62 100644 --- a/src/kernel/_files.py +++ b/src/kernel/_files.py @@ -99,7 +99,7 @@ async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles elif is_sequence_t(files): files = [(key, await _async_transform_file(file)) for key, file in files] else: - raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence") + raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") return files From e3f0b8db4b2d05140d112317315ba1e393f385cf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 20:42:37 +0000 Subject: [PATCH 08/12] feat: browser_pools: add start_url config (KERNEL-1217 PR 2) --- .stats.yml | 4 +-- src/kernel/resources/browser_pools.py | 32 +++++++++++++++++++ src/kernel/resources/browsers/browsers.py | 16 ++++++++++ src/kernel/types/browser_create_params.py | 9 ++++++ src/kernel/types/browser_create_response.py | 6 ++++ src/kernel/types/browser_list_response.py | 6 ++++ src/kernel/types/browser_pool.py | 9 ++++++ .../types/browser_pool_acquire_response.py | 6 ++++ .../types/browser_pool_create_params.py | 9 ++++++ .../types/browser_pool_update_params.py | 9 ++++++ src/kernel/types/browser_retrieve_response.py | 6 ++++ src/kernel/types/browser_update_response.py | 6 ++++ .../invocation_list_browsers_response.py | 6 ++++ tests/api_resources/test_browser_pools.py | 4 +++ tests/api_resources/test_browsers.py | 2 ++ 15 files changed, 128 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 797624b..e769974 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-b51c72a040c8dfea9c0693de6631feabfffe42922d5feb60b4ac0ee5c83bb8f4.yml -openapi_spec_hash: 8b671cfe4debe8d9ad7c39ba5b0eaf6d +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-7d2d29d7598105d50e5118e0bc5ac654361a92cb95555705dbd1d236848e1456.yml +openapi_spec_hash: 10002eae793e08f81932239bbc72b503 config_hash: 08d55086449943a8fec212b870061a3f diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index 6c265cd..10b2392 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -68,6 +68,7 @@ def create( name: str | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: str | Omit = omit, + start_url: str | Omit = omit, stealth: bool | Omit = omit, timeout_seconds: int | Omit = omit, viewport: BrowserViewport | Omit = omit, @@ -109,6 +110,12 @@ def create( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. + start_url: Optional URL to navigate to when a new browser is warmed into the pool. + Best-effort: failures to navigate do not fail pool fill. Only applied to + newly-warmed browsers — browsers reused via release/acquire keep whatever URL + the previous lease left them on. Accepts any URL Chromium can resolve, including + chrome:// pages. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -149,6 +156,7 @@ def create( "name": name, "profile": profile, "proxy_id": proxy_id, + "start_url": start_url, "stealth": stealth, "timeout_seconds": timeout_seconds, "viewport": viewport, @@ -208,6 +216,7 @@ def update( name: str | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: str | Omit = omit, + start_url: str | Omit = omit, stealth: bool | Omit = omit, timeout_seconds: int | Omit = omit, viewport: BrowserViewport | Omit = omit, @@ -252,6 +261,12 @@ def update( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. + start_url: Optional URL to navigate to when a new browser is warmed into the pool. + Best-effort: failures to navigate do not fail pool fill. Only applied to + newly-warmed browsers — browsers reused via release/acquire keep whatever URL + the previous lease left them on. Accepts any URL Chromium can resolve, including + chrome:// pages. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -295,6 +310,7 @@ def update( "name": name, "profile": profile, "proxy_id": proxy_id, + "start_url": start_url, "stealth": stealth, "timeout_seconds": timeout_seconds, "viewport": viewport, @@ -530,6 +546,7 @@ async def create( name: str | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: str | Omit = omit, + start_url: str | Omit = omit, stealth: bool | Omit = omit, timeout_seconds: int | Omit = omit, viewport: BrowserViewport | Omit = omit, @@ -571,6 +588,12 @@ async def create( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. + start_url: Optional URL to navigate to when a new browser is warmed into the pool. + Best-effort: failures to navigate do not fail pool fill. Only applied to + newly-warmed browsers — browsers reused via release/acquire keep whatever URL + the previous lease left them on. Accepts any URL Chromium can resolve, including + chrome:// pages. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -611,6 +634,7 @@ async def create( "name": name, "profile": profile, "proxy_id": proxy_id, + "start_url": start_url, "stealth": stealth, "timeout_seconds": timeout_seconds, "viewport": viewport, @@ -670,6 +694,7 @@ async def update( name: str | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: str | Omit = omit, + start_url: str | Omit = omit, stealth: bool | Omit = omit, timeout_seconds: int | Omit = omit, viewport: BrowserViewport | Omit = omit, @@ -714,6 +739,12 @@ async def update( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. + start_url: Optional URL to navigate to when a new browser is warmed into the pool. + Best-effort: failures to navigate do not fail pool fill. Only applied to + newly-warmed browsers — browsers reused via release/acquire keep whatever URL + the previous lease left them on. Accepts any URL Chromium can resolve, including + chrome:// pages. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -757,6 +788,7 @@ async def update( "name": name, "profile": profile, "proxy_id": proxy_id, + "start_url": start_url, "stealth": stealth, "timeout_seconds": timeout_seconds, "viewport": viewport, diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index f6a2372..d1dba08 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -160,6 +160,7 @@ def create( persistence: BrowserPersistenceParam | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: str | Omit = omit, + start_url: str | Omit = omit, stealth: bool | Omit = omit, timeout_seconds: int | Omit = omit, viewport: BrowserViewport | Omit = omit, @@ -196,6 +197,12 @@ def create( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. + start_url: Optional URL to navigate to immediately after the browser is created. + Best-effort: failures to navigate do not fail browser creation. Any pre-existing + tabs are reduced to a single tab which is then navigated. Accepts any URL + Chromium can resolve, including chrome:// pages. Ignored when reusing an + existing persistent session. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -238,6 +245,7 @@ def create( "persistence": persistence, "profile": profile, "proxy_id": proxy_id, + "start_url": start_url, "stealth": stealth, "timeout_seconds": timeout_seconds, "viewport": viewport, @@ -718,6 +726,7 @@ async def create( persistence: BrowserPersistenceParam | Omit = omit, profile: BrowserProfile | Omit = omit, proxy_id: str | Omit = omit, + start_url: str | Omit = omit, stealth: bool | Omit = omit, timeout_seconds: int | Omit = omit, viewport: BrowserViewport | Omit = omit, @@ -754,6 +763,12 @@ async def create( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy belonging to the caller's org. + start_url: Optional URL to navigate to immediately after the browser is created. + Best-effort: failures to navigate do not fail browser creation. Any pre-existing + tabs are reduced to a single tab which is then navigated. Accepts any URL + Chromium can resolve, including chrome:// pages. Ignored when reusing an + existing persistent session. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot mechanisms. @@ -796,6 +811,7 @@ async def create( "persistence": persistence, "profile": profile, "proxy_id": proxy_id, + "start_url": start_url, "stealth": stealth, "timeout_seconds": timeout_seconds, "viewport": viewport, diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 2827b1d..1a4493f 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -57,6 +57,15 @@ class BrowserCreateParams(TypedDict, total=False): Must reference a proxy belonging to the caller's org. """ + start_url: str + """Optional URL to navigate to immediately after the browser is created. + + Best-effort: failures to navigate do not fail browser creation. Any pre-existing + tabs are reduced to a single tab which is then navigated. Accepts any URL + Chromium can resolve, including chrome:// pages. Ignored when reusing an + existing persistent session. + """ + stealth: bool """ If true, launches the browser in stealth mode to reduce detection by anti-bot diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 9356bb0..e63a689 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -68,6 +68,12 @@ class BrowserCreateResponse(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" + start_url: Optional[str] = None + """URL the session was asked to navigate to on creation, if any. + + Recorded for debugging — navigation is best-effort and may have failed. + """ + usage: Optional[BrowserUsage] = None """Session usage metrics.""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index f3a88f2..de37e26 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -68,6 +68,12 @@ class BrowserListResponse(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" + start_url: Optional[str] = None + """URL the session was asked to navigate to on creation, if any. + + Recorded for debugging — navigation is best-effort and may have failed. + """ + usage: Optional[BrowserUsage] = None """Session usage metrics.""" diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py index 807c73b..a691d0e 100644 --- a/src/kernel/types/browser_pool.py +++ b/src/kernel/types/browser_pool.py @@ -63,6 +63,15 @@ class BrowserPoolConfig(BaseModel): Must reference a proxy belonging to the caller's org. """ + start_url: Optional[str] = None + """Optional URL to navigate to when a new browser is warmed into the pool. + + Best-effort: failures to navigate do not fail pool fill. Only applied to + newly-warmed browsers — browsers reused via release/acquire keep whatever URL + the previous lease left them on. Accepts any URL Chromium can resolve, including + chrome:// pages. + """ + stealth: Optional[bool] = None """ If true, launches the browser in stealth mode to reduce detection by anti-bot diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index 064c405..cff8286 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -68,6 +68,12 @@ class BrowserPoolAcquireResponse(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" + start_url: Optional[str] = None + """URL the session was asked to navigate to on creation, if any. + + Recorded for debugging — navigation is best-effort and may have failed. + """ + usage: Optional[BrowserUsage] = None """Session usage metrics.""" diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py index cab4e13..99d6c9c 100644 --- a/src/kernel/types/browser_pool_create_params.py +++ b/src/kernel/types/browser_pool_create_params.py @@ -62,6 +62,15 @@ class BrowserPoolCreateParams(TypedDict, total=False): Must reference a proxy belonging to the caller's org. """ + start_url: str + """Optional URL to navigate to when a new browser is warmed into the pool. + + Best-effort: failures to navigate do not fail pool fill. Only applied to + newly-warmed browsers — browsers reused via release/acquire keep whatever URL + the previous lease left them on. Accepts any URL Chromium can resolve, including + chrome:// pages. + """ + stealth: bool """ If true, launches the browser in stealth mode to reduce detection by anti-bot diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py index 7ddd131..5b069c4 100644 --- a/src/kernel/types/browser_pool_update_params.py +++ b/src/kernel/types/browser_pool_update_params.py @@ -68,6 +68,15 @@ class BrowserPoolUpdateParams(TypedDict, total=False): Must reference a proxy belonging to the caller's org. """ + start_url: str + """Optional URL to navigate to when a new browser is warmed into the pool. + + Best-effort: failures to navigate do not fail pool fill. Only applied to + newly-warmed browsers — browsers reused via release/acquire keep whatever URL + the previous lease left them on. Accepts any URL Chromium can resolve, including + chrome:// pages. + """ + stealth: bool """ If true, launches the browser in stealth mode to reduce detection by anti-bot diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 5b5a891..80a96d3 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -68,6 +68,12 @@ class BrowserRetrieveResponse(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" + start_url: Optional[str] = None + """URL the session was asked to navigate to on creation, if any. + + Recorded for debugging — navigation is best-effort and may have failed. + """ + usage: Optional[BrowserUsage] = None """Session usage metrics.""" diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index 188895a..4d1f61b 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -68,6 +68,12 @@ class BrowserUpdateResponse(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" + start_url: Optional[str] = None + """URL the session was asked to navigate to on creation, if any. + + Recorded for debugging — navigation is best-effort and may have failed. + """ + usage: Optional[BrowserUsage] = None """Session usage metrics.""" diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py index 23eda77..71c22a7 100644 --- a/src/kernel/types/invocation_list_browsers_response.py +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -68,6 +68,12 @@ class Browser(BaseModel): proxy_id: Optional[str] = None """ID of the proxy associated with this browser session, if any.""" + start_url: Optional[str] = None + """URL the session was asked to navigate to on creation, if any. + + Recorded for debugging — navigation is best-effort and may have failed. + """ + usage: Optional[BrowserUsage] = None """Session usage metrics.""" diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py index 70c7ad8..959eeef 100644 --- a/tests/api_resources/test_browser_pools.py +++ b/tests/api_resources/test_browser_pools.py @@ -51,6 +51,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: "save_changes": True, }, proxy_id="proxy_id", + start_url="https://example.com", stealth=True, timeout_seconds=60, viewport={ @@ -162,6 +163,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: "save_changes": True, }, proxy_id="proxy_id", + start_url="https://example.com", stealth=True, timeout_seconds=60, viewport={ @@ -473,6 +475,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> "save_changes": True, }, proxy_id="proxy_id", + start_url="https://example.com", stealth=True, timeout_seconds=60, viewport={ @@ -584,6 +587,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> "save_changes": True, }, proxy_id="proxy_id", + start_url="https://example.com", stealth=True, timeout_seconds=60, viewport={ diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 96742c7..94c5265 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -53,6 +53,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: "save_changes": True, }, proxy_id="proxy_id", + start_url="https://example.com", stealth=True, timeout_seconds=10, viewport={ @@ -478,6 +479,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> "save_changes": True, }, proxy_id="proxy_id", + start_url="https://example.com", stealth=True, timeout_seconds=10, viewport={ From b30bc1e0c3493f12a3499eb037f13561dfdcaedf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 03:10:40 +0000 Subject: [PATCH 09/12] feat(internal/types): support eagerly validating pydantic iterators --- src/kernel/_models.py | 80 +++++++++++++++++++++++++++++++++++++++++++ tests/test_models.py | 60 ++++++++++++++++++++++++++++++-- 2 files changed, 137 insertions(+), 3 deletions(-) diff --git a/src/kernel/_models.py b/src/kernel/_models.py index 29070e0..8c5ab26 100644 --- a/src/kernel/_models.py +++ b/src/kernel/_models.py @@ -25,7 +25,9 @@ ClassVar, Protocol, Required, + Annotated, ParamSpec, + TypeAlias, TypedDict, TypeGuard, final, @@ -79,7 +81,15 @@ from ._constants import RAW_RESPONSE_HEADER if TYPE_CHECKING: + from pydantic import GetCoreSchemaHandler, ValidatorFunctionWrapHandler + from pydantic_core import CoreSchema, core_schema from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema +else: + try: + from pydantic_core import CoreSchema, core_schema + except ImportError: + CoreSchema = None + core_schema = None __all__ = ["BaseModel", "GenericModel"] @@ -396,6 +406,76 @@ def model_dump_json( ) +class _EagerIterable(list[_T], Generic[_T]): + """ + Accepts any Iterable[T] input (including generators), consumes it + eagerly, and validates all items upfront. + + Validation preserves the original container type where possible + (e.g. a set[T] stays a set[T]). Serialization (model_dump / JSON) + always emits a list — round-tripping through model_dump() will not + restore the original container type. + """ + + @classmethod + def __get_pydantic_core_schema__( + cls, + source_type: Any, + handler: GetCoreSchemaHandler, + ) -> CoreSchema: + (item_type,) = get_args(source_type) or (Any,) + item_schema: CoreSchema = handler.generate_schema(item_type) + list_of_items_schema: CoreSchema = core_schema.list_schema(item_schema) + + return core_schema.no_info_wrap_validator_function( + cls._validate, + list_of_items_schema, + serialization=core_schema.plain_serializer_function_ser_schema( + cls._serialize, + info_arg=False, + ), + ) + + @staticmethod + def _validate(v: Iterable[_T], handler: "ValidatorFunctionWrapHandler") -> Any: + original_type: type[Any] = type(v) + + # Normalize to list so list_schema can validate each item + if isinstance(v, list): + items: list[_T] = v + else: + try: + items = list(v) + except TypeError as e: + raise TypeError("Value is not iterable") from e + + # Validate items against the inner schema + validated: list[_T] = handler(items) + + # Reconstruct original container type + if original_type is list: + return validated + # str(list) produces the list's repr, not a string built from items, + # so skip reconstruction for str and its subclasses. + if issubclass(original_type, str): + return validated + try: + return original_type(validated) + except (TypeError, ValueError): + # If the type cannot be reconstructed, just return the validated list + return validated + + @staticmethod + def _serialize(v: Iterable[_T]) -> list[_T]: + """Always serialize as a list so Pydantic's JSON encoder is happy.""" + if isinstance(v, list): + return v + return list(v) + + +EagerIterable: TypeAlias = Annotated[Iterable[_T], _EagerIterable] + + def _construct_field(value: object, field: FieldInfo, key: str) -> object: if value is None: return field_get_default(field) diff --git a/tests/test_models.py b/tests/test_models.py index 78f0fd3..c3922db 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,8 @@ import json -from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast +from typing import TYPE_CHECKING, Any, Dict, List, Union, Iterable, Optional, cast from datetime import datetime, timezone -from typing_extensions import Literal, Annotated, TypeAliasType +from collections import deque +from typing_extensions import Literal, Annotated, TypedDict, TypeAliasType import pytest import pydantic @@ -9,7 +10,7 @@ from kernel._utils import PropertyInfo from kernel._compat import PYDANTIC_V1, parse_obj, model_dump, model_json -from kernel._models import DISCRIMINATOR_CACHE, BaseModel, construct_type +from kernel._models import DISCRIMINATOR_CACHE, BaseModel, EagerIterable, construct_type class BasicModel(BaseModel): @@ -961,3 +962,56 @@ def __getattr__(self, attr: str) -> Item: ... assert model.a.prop == 1 assert isinstance(model.a, Item) assert model.other == "foo" + + +# NOTE: Workaround for Pydantic Iterable behavior. +# Iterable fields are replaced with a ValidatorIterator and may be consumed +# during serialization, which can cause subsequent dumps to return empty data. +# See: https://github.com/pydantic/pydantic/issues/9541 +@pytest.mark.parametrize( + "data, expected_validated", + [ + ([1, 2, 3], [1, 2, 3]), + ((1, 2, 3), (1, 2, 3)), + (set([1, 2, 3]), set([1, 2, 3])), + (iter([1, 2, 3]), [1, 2, 3]), + ([], []), + ((x for x in [1, 2, 3]), [1, 2, 3]), + (map(lambda x: x, [1, 2, 3]), [1, 2, 3]), + (frozenset([1, 2, 3]), frozenset([1, 2, 3])), + (deque([1, 2, 3]), deque([1, 2, 3])), + ], + ids=["list", "tuple", "set", "iterator", "empty", "generator", "map", "frozenset", "deque"], +) +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2") +def test_iterable_construction(data: Iterable[int], expected_validated: Iterable[int]) -> None: + class TypeWithIterable(TypedDict): + items: EagerIterable[int] + + class Model(BaseModel): + data: TypeWithIterable + + m = Model.model_validate({"data": {"items": data}}) + assert m.data["items"] == expected_validated + + # Verify repeated dumps don't lose data (the original bug) + assert m.model_dump()["data"]["items"] == list(expected_validated) + assert m.model_dump()["data"]["items"] == list(expected_validated) + + +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2") +def test_iterable_construction_str_falls_back_to_list() -> None: + # str is iterable (over chars), but str(list_of_chars) produces the list's repr + # rather than reconstructing a string from items. We special-case str to fall + # back to list instead of attempting reconstruction. + class TypeWithIterable(TypedDict): + items: EagerIterable[str] + + class Model(BaseModel): + data: TypeWithIterable + + m = Model.model_validate({"data": {"items": "hello"}}) + + # falls back to list of chars rather than calling str(["h", "e", "l", "l", "o"]) + assert m.data["items"] == ["h", "e", "l", "l", "o"] + assert m.model_dump()["data"]["items"] == ["h", "e", "l", "l", "o"] From fd4ffe40e037e95e93f46b7773d6a5e468c7b8fd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 19:36:25 +0000 Subject: [PATCH 10/12] feat: managed-auth: surface awaiting_external_action even when fallback actions exist --- .stats.yml | 4 ++-- .../types/auth/connection_follow_response.py | 17 ++++++++++++----- src/kernel/types/auth/managed_auth.py | 17 ++++++++++++----- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/.stats.yml b/.stats.yml index e769974..2085eb0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-7d2d29d7598105d50e5118e0bc5ac654361a92cb95555705dbd1d236848e1456.yml -openapi_spec_hash: 10002eae793e08f81932239bbc72b503 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-38c6ce8b0ce54e6c62517b9635acaf65369c26cdb1b55eb66290c13a8ab2b33d.yml +openapi_spec_hash: e6f6ed157b1e318d1a1db72fd1d27091 config_hash: 08d55086449943a8fec212b870061a3f diff --git a/src/kernel/types/auth/connection_follow_response.py b/src/kernel/types/auth/connection_follow_response.py index 7cabc1c..1cd1a14 100644 --- a/src/kernel/types/auth/connection_follow_response.py +++ b/src/kernel/types/auth/connection_follow_response.py @@ -119,7 +119,10 @@ class ManagedAuthStateEvent(BaseModel): """Time the state was reported.""" discovered_fields: Optional[List[ManagedAuthStateEventDiscoveredField]] = None - """Fields awaiting input (present when flow_step=AWAITING_INPUT).""" + """ + Fields awaiting input (present when flow_step=AWAITING_INPUT; may also be + present with AWAITING_EXTERNAL_ACTION as fallback actions). + """ error_code: Optional[str] = None """Machine-readable error code (present when flow_status=FAILED).""" @@ -144,12 +147,15 @@ class ManagedAuthStateEvent(BaseModel): mfa_options: Optional[List[ManagedAuthStateEventMfaOption]] = None """ - MFA method options (present when flow_step=AWAITING_INPUT and MFA selection - required). + MFA method options (present when flow_step=AWAITING_INPUT; may also be present + with AWAITING_EXTERNAL_ACTION as fallback actions). """ pending_sso_buttons: Optional[List[ManagedAuthStateEventPendingSSOButton]] = None - """SSO buttons available (present when flow_step=AWAITING_INPUT).""" + """ + SSO buttons available (present when flow_step=AWAITING_INPUT; may also be + present with AWAITING_EXTERNAL_ACTION as fallback actions). + """ post_login_url: Optional[str] = None """URL where the browser landed after successful login.""" @@ -157,7 +163,8 @@ class ManagedAuthStateEvent(BaseModel): sign_in_options: Optional[List[ManagedAuthStateEventSignInOption]] = None """ Non-MFA choices presented during the auth flow, such as account selection or org - pickers (present when flow_step=AWAITING_INPUT). + pickers (present when flow_step=AWAITING_INPUT; may also be present with + AWAITING_EXTERNAL_ACTION as fallback actions). """ website_error: Optional[str] = None diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py index 622c3bd..c44fd8d 100644 --- a/src/kernel/types/auth/managed_auth.py +++ b/src/kernel/types/auth/managed_auth.py @@ -185,7 +185,10 @@ class ManagedAuth(BaseModel): """ discovered_fields: Optional[List[DiscoveredField]] = None - """Fields awaiting input (present when flow_step=awaiting_input)""" + """ + Fields awaiting input (present when flow_step=awaiting_input; may also be + present with awaiting_external_action as fallback actions) + """ error_code: Optional[str] = None """Machine-readable error code (present when flow_status=failed)""" @@ -254,12 +257,15 @@ class ManagedAuth(BaseModel): mfa_options: Optional[List[MfaOption]] = None """ - MFA method options (present when flow_step=awaiting_input and MFA selection - required) + MFA method options (present when flow_step=awaiting_input; may also be present + with awaiting_external_action as fallback actions) """ pending_sso_buttons: Optional[List[PendingSSOButton]] = None - """SSO buttons available (present when flow_step=awaiting_input)""" + """ + SSO buttons available (present when flow_step=awaiting_input; may also be + present with awaiting_external_action as fallback actions) + """ post_login_url: Optional[str] = None """URL where the browser landed after successful login""" @@ -270,7 +276,8 @@ class ManagedAuth(BaseModel): sign_in_options: Optional[List[SignInOption]] = None """ Non-MFA choices presented during the auth flow, such as account selection or org - pickers (present when flow_step=awaiting_input). + pickers (present when flow_step=awaiting_input; may also be present with + awaiting_external_action as fallback actions). """ sso_provider: Optional[str] = None From 53fea1c26cd8de2b93944bb7625c401d05362b1c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 21:37:59 +0000 Subject: [PATCH 11/12] feat: Add opt-in record_session flag to managed auth --- .stats.yml | 4 +- src/kernel/resources/auth/connections.py | 42 ++++++++++++++++++- .../types/auth/connection_create_params.py | 6 +++ .../types/auth/connection_login_params.py | 6 +++ .../types/auth/connection_update_params.py | 3 ++ src/kernel/types/auth/managed_auth.py | 6 +++ tests/api_resources/auth/test_connections.py | 6 +++ 7 files changed, 69 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index 2085eb0..2939ac9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-38c6ce8b0ce54e6c62517b9635acaf65369c26cdb1b55eb66290c13a8ab2b33d.yml -openapi_spec_hash: e6f6ed157b1e318d1a1db72fd1d27091 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-64dac369ae935b0318cd611e1735d45359a663eb55cf43fbb8b09e9dd814d7a2.yml +openapi_spec_hash: cc0c6a4e716977df4b9eab5f4658d41b config_hash: 08d55086449943a8fec212b870061a3f diff --git a/src/kernel/resources/auth/connections.py b/src/kernel/resources/auth/connections.py index fd28bd1..4befc6e 100644 --- a/src/kernel/resources/auth/connections.py +++ b/src/kernel/resources/auth/connections.py @@ -66,6 +66,7 @@ def create( health_check_interval: int | Omit = omit, login_url: str | Omit = omit, proxy: connection_create_params.Proxy | Omit = omit, + record_session: bool | Omit = omit, save_credentials: 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. @@ -122,6 +123,9 @@ def create( proxy: Proxy selection. Provide either id or name. The proxy must belong to the caller's org. + record_session: Whether to record browser sessions for this connection by default. Useful for + debugging. Can be overridden per-login. Defaults to false. + save_credentials: Whether to save credentials after every successful login. Defaults to true. One-time codes (TOTP, SMS, etc.) are not saved. @@ -144,6 +148,7 @@ def create( "health_check_interval": health_check_interval, "login_url": login_url, "proxy": proxy, + "record_session": record_session, "save_credentials": save_credentials, }, connection_create_params.ConnectionCreateParams, @@ -198,6 +203,7 @@ def update( health_check_interval: int | Omit = omit, login_url: str | Omit = omit, proxy: connection_update_params.Proxy | Omit = omit, + record_session: bool | Omit = omit, save_credentials: 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. @@ -228,6 +234,8 @@ def update( proxy: Proxy selection. Provide either id or name. The proxy must belong to the caller's org. + record_session: Whether to record browser sessions for this connection by default + save_credentials: Whether to save credentials after every successful login extra_headers: Send extra headers @@ -249,6 +257,7 @@ def update( "health_check_interval": health_check_interval, "login_url": login_url, "proxy": proxy, + "record_session": record_session, "save_credentials": save_credentials, }, connection_update_params.ConnectionUpdateParams, @@ -398,6 +407,7 @@ def login( id: str, *, proxy: connection_login_params.Proxy | Omit = omit, + record_session: 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, @@ -415,6 +425,9 @@ def login( proxy: Proxy selection. Provide either id or name. The proxy must belong to the caller's org. + record_session: Override the connection's default for recording this login's browser session. + When omitted, the connection's record_session default is used. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -427,7 +440,13 @@ def login( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( path_template("/auth/connections/{id}/login", id=id), - body=maybe_transform({"proxy": proxy}, connection_login_params.ConnectionLoginParams), + body=maybe_transform( + { + "proxy": proxy, + "record_session": record_session, + }, + connection_login_params.ConnectionLoginParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -529,6 +548,7 @@ async def create( health_check_interval: int | Omit = omit, login_url: str | Omit = omit, proxy: connection_create_params.Proxy | Omit = omit, + record_session: bool | Omit = omit, save_credentials: 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. @@ -585,6 +605,9 @@ async def create( proxy: Proxy selection. Provide either id or name. The proxy must belong to the caller's org. + record_session: Whether to record browser sessions for this connection by default. Useful for + debugging. Can be overridden per-login. Defaults to false. + save_credentials: Whether to save credentials after every successful login. Defaults to true. One-time codes (TOTP, SMS, etc.) are not saved. @@ -607,6 +630,7 @@ async def create( "health_check_interval": health_check_interval, "login_url": login_url, "proxy": proxy, + "record_session": record_session, "save_credentials": save_credentials, }, connection_create_params.ConnectionCreateParams, @@ -661,6 +685,7 @@ async def update( health_check_interval: int | Omit = omit, login_url: str | Omit = omit, proxy: connection_update_params.Proxy | Omit = omit, + record_session: bool | Omit = omit, save_credentials: 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. @@ -691,6 +716,8 @@ async def update( proxy: Proxy selection. Provide either id or name. The proxy must belong to the caller's org. + record_session: Whether to record browser sessions for this connection by default + save_credentials: Whether to save credentials after every successful login extra_headers: Send extra headers @@ -712,6 +739,7 @@ async def update( "health_check_interval": health_check_interval, "login_url": login_url, "proxy": proxy, + "record_session": record_session, "save_credentials": save_credentials, }, connection_update_params.ConnectionUpdateParams, @@ -861,6 +889,7 @@ async def login( id: str, *, proxy: connection_login_params.Proxy | Omit = omit, + record_session: 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, @@ -878,6 +907,9 @@ async def login( proxy: Proxy selection. Provide either id or name. The proxy must belong to the caller's org. + record_session: Override the connection's default for recording this login's browser session. + When omitted, the connection's record_session default is used. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -890,7 +922,13 @@ async def login( raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( path_template("/auth/connections/{id}/login", id=id), - body=await async_maybe_transform({"proxy": proxy}, connection_login_params.ConnectionLoginParams), + body=await async_maybe_transform( + { + "proxy": proxy, + "record_session": record_session, + }, + connection_login_params.ConnectionLoginParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/kernel/types/auth/connection_create_params.py b/src/kernel/types/auth/connection_create_params.py index b021994..bdd2268 100644 --- a/src/kernel/types/auth/connection_create_params.py +++ b/src/kernel/types/auth/connection_create_params.py @@ -66,6 +66,12 @@ class ConnectionCreateParams(TypedDict, total=False): Provide either id or name. The proxy must belong to the caller's org. """ + record_session: bool + """Whether to record browser sessions for this connection by default. + + Useful for debugging. Can be overridden per-login. Defaults to false. + """ + save_credentials: bool """Whether to save credentials after every successful login. diff --git a/src/kernel/types/auth/connection_login_params.py b/src/kernel/types/auth/connection_login_params.py index 8ea6474..39756cb 100644 --- a/src/kernel/types/auth/connection_login_params.py +++ b/src/kernel/types/auth/connection_login_params.py @@ -14,6 +14,12 @@ class ConnectionLoginParams(TypedDict, total=False): Provide either id or name. The proxy must belong to the caller's org. """ + record_session: bool + """Override the connection's default for recording this login's browser session. + + When omitted, the connection's record_session default is used. + """ + class Proxy(TypedDict, total=False): """Proxy selection. diff --git a/src/kernel/types/auth/connection_update_params.py b/src/kernel/types/auth/connection_update_params.py index 77e738b..2383277 100644 --- a/src/kernel/types/auth/connection_update_params.py +++ b/src/kernel/types/auth/connection_update_params.py @@ -33,6 +33,9 @@ class ConnectionUpdateParams(TypedDict, total=False): Provide either id or name. The proxy must belong to the caller's org. """ + record_session: bool + """Whether to record browser sessions for this connection by default""" + save_credentials: bool """Whether to save credentials after every successful login""" diff --git a/src/kernel/types/auth/managed_auth.py b/src/kernel/types/auth/managed_auth.py index c44fd8d..09f89de 100644 --- a/src/kernel/types/auth/managed_auth.py +++ b/src/kernel/types/auth/managed_auth.py @@ -130,6 +130,12 @@ class ManagedAuth(BaseModel): profile_name: str """Name of the profile associated with this auth connection""" + record_session: bool + """ + Whether browser sessions for this connection are recorded by default for + debugging. Can be overridden per-login. + """ + save_credentials: bool """Whether credentials are saved after every successful login. diff --git a/tests/api_resources/auth/test_connections.py b/tests/api_resources/auth/test_connections.py index f5edd89..e3da167 100644 --- a/tests/api_resources/auth/test_connections.py +++ b/tests/api_resources/auth/test_connections.py @@ -50,6 +50,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: "id": "id", "name": "name", }, + record_session=False, save_credentials=True, ) assert_matches_type(ManagedAuth, connection, path=["response"]) @@ -150,6 +151,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: "id": "id", "name": "name", }, + record_session=False, save_credentials=True, ) assert_matches_type(ManagedAuth, connection, path=["response"]) @@ -327,6 +329,7 @@ def test_method_login_with_all_params(self, client: Kernel) -> None: "id": "id", "name": "name", }, + record_session=True, ) assert_matches_type(LoginResponse, connection, path=["response"]) @@ -456,6 +459,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> "id": "id", "name": "name", }, + record_session=False, save_credentials=True, ) assert_matches_type(ManagedAuth, connection, path=["response"]) @@ -556,6 +560,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> "id": "id", "name": "name", }, + record_session=False, save_credentials=True, ) assert_matches_type(ManagedAuth, connection, path=["response"]) @@ -733,6 +738,7 @@ async def test_method_login_with_all_params(self, async_client: AsyncKernel) -> "id": "id", "name": "name", }, + record_session=True, ) assert_matches_type(LoginResponse, connection, path=["response"]) From 5b7a3d2c15827f22da2e8b5dd55954db8b7a4fb2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 21:38:25 +0000 Subject: [PATCH 12/12] release: 0.53.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 24 ++++++++++++++++++++++++ pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fed4b17..c3e01e1 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.52.0" + ".": "0.53.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c0f9e33..a15797c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## 0.53.0 (2026-05-12) + +Full Changelog: [v0.52.0...v0.53.0](https://github.com/kernel/kernel-python-sdk/compare/v0.52.0...v0.53.0) + +### Features + +* Add 'switch' MFA option type for generic method-switcher links ([82ae224](https://github.com/kernel/kernel-python-sdk/commit/82ae224a85a22b16e48a26a8058cc992ef772cf2)) +* Add opt-in record_session flag to managed auth ([53fea1c](https://github.com/kernel/kernel-python-sdk/commit/53fea1c26cd8de2b93944bb7625c401d05362b1c)) +* **api:** server-side search on GET /projects ([cacb057](https://github.com/kernel/kernel-python-sdk/commit/cacb057a5d1d3bcb26b6df1f6fe83067f936d642)) +* browser_pools: add start_url config (KERNEL-1217 PR 2) ([e3f0b8d](https://github.com/kernel/kernel-python-sdk/commit/e3f0b8db4b2d05140d112317315ba1e393f385cf)) +* **internal/types:** support eagerly validating pydantic iterators ([b30bc1e](https://github.com/kernel/kernel-python-sdk/commit/b30bc1e0c3493f12a3499eb037f13561dfdcaedf)) +* managed-auth: surface awaiting_external_action even when fallback actions exist ([fd4ffe4](https://github.com/kernel/kernel-python-sdk/commit/fd4ffe40e037e95e93f46b7773d6a5e468c7b8fd)) +* Scope name uniqueness to project for profiles, session_pools, extensions, credentials ([c510a51](https://github.com/kernel/kernel-python-sdk/commit/c510a515b3c57846f0e681638167ba87b8ee15c3)) + + +### Bug Fixes + +* **client:** add missing f-string prefix in file type error message ([fb5340d](https://github.com/kernel/kernel-python-sdk/commit/fb5340dbf9ea49393d87b5760a2efaaf932c9442)) + + +### Chores + +* **internal:** reformat pyproject.toml ([27c799b](https://github.com/kernel/kernel-python-sdk/commit/27c799be452cb66af59b79d24b3d5147a85b70f7)) + ## 0.52.0 (2026-04-29) Full Changelog: [v0.51.0...v0.52.0](https://github.com/kernel/kernel-python-sdk/compare/v0.51.0...v0.52.0) diff --git a/pyproject.toml b/pyproject.toml index 5d43220..10375b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.52.0" +version = "0.53.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 1ce5f4b..173fc2c 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.52.0" # x-release-please-version +__version__ = "0.53.0" # x-release-please-version