From d980cbe052ea090a8ad2ed1dc51b72e7c6bc1b16 Mon Sep 17 00:00:00 2001 From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com> Date: Mon, 10 Aug 2026 20:23:42 +0000 Subject: [PATCH 1/9] ci: use one SDK release PR --- .github/workflows/release-please.yml | 76 ++++++++++++++++++++++++++-- .github/workflows/stlc-promote.yml | 71 +++++++++++++------------- release-please-config.json | 1 + 3 files changed, 110 insertions(+), 38 deletions(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 3f6bf798..4737ca42 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -1,12 +1,21 @@ name: Release Please + +# `next` accumulates validated SDK changes in one versioned PR to `main`. +# Merging that PR creates the GitHub release; the package publishing workflow +# runs from the release event. on: push: branches: + - next - main permissions: contents: read +concurrency: + group: release-please + cancel-in-progress: false + jobs: release-please: if: github.repository == 'kernel/kernel-python-sdk' @@ -26,7 +35,68 @@ jobs: permission-pull-requests: write permission-workflows: write - - uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1 - id: release + - name: Set up Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: '18.20.2' + + - name: Set up pnpm + uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 with: - token: ${{ steps.release-token.outputs.token }} + version: '9.11.0' + run_install: false + + - name: Build pinned release tooling + id: tooling + env: + RELEASE_PLEASE_DIR: ${{ runner.temp }}/release-please + RELEASE_PLEASE_SHA: a116e1e520e0f87824acf46a2e79c91d41e819d7 + run: | + set -euo pipefail + rm -rf "$RELEASE_PLEASE_DIR" + git init "$RELEASE_PLEASE_DIR" + git -C "$RELEASE_PLEASE_DIR" remote add origin https://github.com/stainless-api/release-please.git + git -C "$RELEASE_PLEASE_DIR" fetch --depth=1 origin "$RELEASE_PLEASE_SHA" + git -C "$RELEASE_PLEASE_DIR" checkout --detach FETCH_HEAD + pnpm --dir "$RELEASE_PLEASE_DIR" install --frozen-lockfile + pnpm --dir "$RELEASE_PLEASE_DIR" build + echo "cli=$RELEASE_PLEASE_DIR/build/src/bin/release-please.js" >> "$GITHUB_OUTPUT" + + - name: Open or update the release PR + if: github.ref_name == 'next' + env: + GH_TOKEN: ${{ steps.release-token.outputs.token }} + RELEASE_PLEASE: ${{ steps.tooling.outputs.cli }} + run: | + set -euo pipefail + node "$RELEASE_PLEASE" release-pr \ + --repo-url "$GITHUB_REPOSITORY" \ + --token "$GH_TOKEN" \ + --target-branch main \ + --changes-branch next + + - name: Remove the legacy promotion PR + if: github.ref_name == 'next' + env: + GH_TOKEN: ${{ steps.release-token.outputs.token }} + run: | + set -euo pipefail + legacy=$(gh pr list --repo "$GITHUB_REPOSITORY" --head stainless/release \ + --state open --json number --jq '.[].number') + for pr in $legacy; do + gh pr close "$pr" --repo "$GITHUB_REPOSITORY" \ + --comment "Superseded by the versioned release PR from next to main." + done + gh api -X DELETE "repos/$GITHUB_REPOSITORY/git/refs/heads/stainless/release" >/dev/null 2>&1 || true + + - name: Create the GitHub release + if: github.ref_name == 'main' + env: + GH_TOKEN: ${{ steps.release-token.outputs.token }} + RELEASE_PLEASE: ${{ steps.tooling.outputs.cli }} + run: | + set -euo pipefail + node "$RELEASE_PLEASE" github-release \ + --repo-url "$GITHUB_REPOSITORY" \ + --token "$GH_TOKEN" \ + --target-branch main diff --git a/.github/workflows/stlc-promote.yml b/.github/workflows/stlc-promote.yml index 5123a90a..d917658c 100644 --- a/.github/workflows/stlc-promote.yml +++ b/.github/workflows/stlc-promote.yml @@ -1,8 +1,9 @@ -name: Promote SDKs +name: Promote SDK changes -# Production requires pull requests, so staging is promoted through a merge- -# commit PR. Never squash or rebase this cross-repo PR: preserving the incoming -# commits keeps production and staging on one ancestry chain. +# Staging is the generator's integration history. Production `next` is the +# developer-facing queue for the next release. This workflow combines the +# latest released state with validated staging changes, then advances `next`. +# Release automation maintains the single versioned PR from `next` to `main`. on: push: branches: [main] @@ -35,49 +36,49 @@ jobs: repositories: kernel-python-sdk permission-contents: write permission-workflows: write - permission-pull-requests: write - - name: Fetch production main + - name: Fetch production branches + id: production env: GH_TOKEN: ${{ steps.production-token.outputs.token }} PRODUCTION_REPO: kernel/kernel-python-sdk run: | - git remote add production "https://x-access-token:${GH_TOKEN}@github.com/${PRODUCTION_REPO}.git" + set -euo pipefail + git remote add production \ + "https://x-access-token:${GH_TOKEN}@github.com/${PRODUCTION_REPO}.git" git fetch production main - - - name: Check whether production already has staging's content - id: diff - run: | - MERGED=$(git merge-tree --write-tree production/main origin/main) || MERGED=conflict - PRODUCTION_TREE=$(git rev-parse 'production/main^{tree}') - if [ "$MERGED" = "$PRODUCTION_TREE" ]; then - echo "Production already contains staging's content. Nothing to promote." - echo "synced=true" >> "$GITHUB_OUTPUT" + if git ls-remote --exit-code --heads production next >/dev/null 2>&1; then + git fetch production next + echo "has_next=true" >> "$GITHUB_OUTPUT" else - echo "synced=false" >> "$GITHUB_OUTPUT" + echo "has_next=false" >> "$GITHUB_OUTPUT" fi - - name: Push the production release branch - if: steps.diff.outputs.synced == 'false' + - name: Prepare the next release branch env: + APP_SLUG: ${{ steps.production-token.outputs.app-slug }} GH_TOKEN: ${{ steps.production-token.outputs.token }} - PRODUCTION_REPO: kernel/kernel-python-sdk - run: git push production origin/main:refs/heads/stainless/release --force + HAS_NEXT: ${{ steps.production.outputs.has_next }} + run: | + set -euo pipefail + bot_id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id) + git config user.name "${APP_SLUG}[bot]" + git config user.email "${bot_id}+${APP_SLUG}[bot]@users.noreply.github.com" + + git checkout -B stlc/promote-next origin/main + if ! git merge-base --is-ancestor production/main HEAD; then + git merge --no-edit production/main + fi - - name: Open or update the promote PR - if: steps.diff.outputs.synced == 'false' + if [ "$HAS_NEXT" = "true" ] && ! git merge-base --is-ancestor production/next HEAD; then + echo "::error title=Promotion blocked::production next has commits that are not present in staging or production main." + exit 1 + fi + + - name: Update the pending release env: GH_TOKEN: ${{ steps.production-token.outputs.token }} - PRODUCTION_REPO: kernel/kernel-python-sdk run: | - body=$(mktemp) - git log --oneline production/main..origin/main > "$body" - existing=$(gh pr list --repo "$PRODUCTION_REPO" --head stainless/release --state open --json number --jq 'if length == 0 then "" else .[0].number end') - if [ -z "$existing" ]; then - gh pr create --repo "$PRODUCTION_REPO" --base main --head stainless/release --title "Release SDK updates" --body-file "$body" - else - gh pr edit "$existing" --repo "$PRODUCTION_REPO" --title "Release SDK updates" --body-file "$body" - fi - if ! gh pr merge stainless/release --repo "$PRODUCTION_REPO" --merge --auto; then - echo "::warning title=Manual promotion required::Merge the promote PR with a merge commit." - fi + set -euo pipefail + git push production HEAD:refs/heads/next + echo "Updated production next; the versioned release PR will be opened or refreshed." diff --git a/release-please-config.json b/release-please-config.json index bdba3b86..6ffb6a57 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -10,6 +10,7 @@ "bump-minor-pre-major": true, "bump-patch-for-minor-pre-major": false, "pull-request-header": "Automated Release PR", + "pull-request-footer": "Merge this pull request with a merge commit. Merging creates the GitHub release and publishes the package.", "pull-request-title-pattern": "release: ${version}", "changelog-sections": [ { From 65c00dc5404e9c886352b6831e54c1326f2557e6 Mon Sep 17 00:00:00 2001 From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:08:51 +0000 Subject: [PATCH 2/9] Preserve pending SDK release ancestry --- .github/workflows/stlc-promote.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/stlc-promote.yml b/.github/workflows/stlc-promote.yml index d917658c..f625307d 100644 --- a/.github/workflows/stlc-promote.yml +++ b/.github/workflows/stlc-promote.yml @@ -65,14 +65,21 @@ jobs: git config user.name "${APP_SLUG}[bot]" git config user.email "${bot_id}+${APP_SLUG}[bot]@users.noreply.github.com" - git checkout -B stlc/promote-next origin/main + if [ "$HAS_NEXT" = "true" ]; then + git checkout -B stlc/promote-next production/next + else + git checkout -B stlc/promote-next production/main + fi + if ! git merge-base --is-ancestor production/main HEAD; then git merge --no-edit production/main fi + if ! git merge-base --is-ancestor origin/main HEAD; then + git merge --no-edit origin/main + fi - if [ "$HAS_NEXT" = "true" ] && ! git merge-base --is-ancestor production/next HEAD; then - echo "::error title=Promotion blocked::production next has commits that are not present in staging or production main." - exit 1 + if [ "$HAS_NEXT" = "true" ]; then + git merge-base --is-ancestor production/next HEAD fi - name: Update the pending release From 0bd6a7f0e857efb66257c428da50eeba24dee175 Mon Sep 17 00:00:00 2001 From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:34:49 +0000 Subject: [PATCH 3/9] Open recovery PRs for promotion conflicts --- .github/workflows/stlc-promote.yml | 44 ++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stlc-promote.yml b/.github/workflows/stlc-promote.yml index f625307d..e63aaf9e 100644 --- a/.github/workflows/stlc-promote.yml +++ b/.github/workflows/stlc-promote.yml @@ -35,6 +35,7 @@ jobs: owner: kernel repositories: kernel-python-sdk permission-contents: write + permission-pull-requests: write permission-workflows: write - name: Fetch production branches @@ -59,12 +60,49 @@ jobs: APP_SLUG: ${{ steps.production-token.outputs.app-slug }} GH_TOKEN: ${{ steps.production-token.outputs.token }} HAS_NEXT: ${{ steps.production.outputs.has_next }} + PRODUCTION_REPO: kernel/kernel-python-sdk run: | set -euo pipefail bot_id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id) git config user.name "${APP_SLUG}[bot]" git config user.email "${bot_id}+${APP_SLUG}[bot]@users.noreply.github.com" + open_conflict_pr() { + source_ref=$1 + source_name=$2 + advance_next=$3 + conflict_branch=stlc/promotion-conflict + + git merge --abort + existing=$(gh pr list --repo "$PRODUCTION_REPO" --base next \ + --head "$conflict_branch" --state open --json url --jq '.[0].url // ""') + if [ -n "$existing" ]; then + echo "::error title=SDK promotion blocked::Resolve the existing recovery PR: $existing" + exit 1 + fi + + if [ "$advance_next" = "true" ]; then + git push production HEAD:refs/heads/next + fi + git push production "$source_ref:refs/heads/$conflict_branch" --force + + body=$(mktemp) + printf '%s\n' \ + '## SDK promotion conflict' \ + '' \ + "The automated promotion could not merge $source_name into the pending next release." \ + '' \ + 'Resolve the conflicts on this branch, validate the SDK, mark this PR ready, and merge it with a merge commit.' \ + '' \ + 'After merging, rerun the staging Promote SDK changes workflow to include any newer generated changes.' \ + > "$body" + recovery_url=$(gh pr create --repo "$PRODUCTION_REPO" --draft \ + --base next --head "$conflict_branch" \ + --title 'chore: resolve SDK promotion conflict' --body-file "$body") + echo "::error title=SDK promotion conflict::Resolve the recovery PR: $recovery_url" + exit 1 + } + if [ "$HAS_NEXT" = "true" ]; then git checkout -B stlc/promote-next production/next else @@ -72,10 +110,12 @@ jobs: fi if ! git merge-base --is-ancestor production/main HEAD; then - git merge --no-edit production/main + git merge --no-edit production/main || \ + open_conflict_pr production/main 'production main' false fi if ! git merge-base --is-ancestor origin/main HEAD; then - git merge --no-edit origin/main + git merge --no-edit origin/main || \ + open_conflict_pr origin/main 'validated staging changes' true fi if [ "$HAS_NEXT" = "true" ]; then From e639638e52a2abcacc2bf1d0d81da09ccb023142 Mon Sep 17 00:00:00 2001 From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:59:45 +0000 Subject: [PATCH 4/9] Preserve conflict handler error checks --- .github/workflows/stlc-promote.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stlc-promote.yml b/.github/workflows/stlc-promote.yml index e63aaf9e..3bcd88c7 100644 --- a/.github/workflows/stlc-promote.yml +++ b/.github/workflows/stlc-promote.yml @@ -110,12 +110,14 @@ jobs: fi if ! git merge-base --is-ancestor production/main HEAD; then - git merge --no-edit production/main || \ + if ! git merge --no-edit production/main; then open_conflict_pr production/main 'production main' false + fi fi if ! git merge-base --is-ancestor origin/main HEAD; then - git merge --no-edit origin/main || \ + if ! git merge --no-edit origin/main; then open_conflict_pr origin/main 'validated staging changes' true + fi fi if [ "$HAS_NEXT" = "true" ]; then From d7c92107ee02cd222a605880ec0f30ef7dcca926 Mon Sep 17 00:00:00 2001 From: tnsardesai <18272584+tnsardesai@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:26:09 +0000 Subject: [PATCH 5/9] test: add region to the fake browser fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The API spec gains region as a required field on browser responses (kernel/kernel#3270); strict response validation rejects the fixture without it. Safe against the current spec — models allow extra keys. --- tests/test_browser_routing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_browser_routing.py b/tests/test_browser_routing.py index dbb8b895..84c5c541 100644 --- a/tests/test_browser_routing.py +++ b/tests/test_browser_routing.py @@ -32,6 +32,7 @@ def _fake_browser() -> dict[str, object]: "headless": True, "stealth": False, "timeout_seconds": 60, + "region": "us-east", } From 9ff1b445f8f2bb3f9bb24f613e22c8b940fc1d5a Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 23:49:40 +0000 Subject: [PATCH 6/9] feat: Expose plan-derived auth limits on GET /org/limits Stainless-Generated-From: f793e2f1d478198a0f75f553437b8b554b75368f --- src/kernel/resources/organization/limits.py | 12 ++---------- src/kernel/types/organization/org_limits.py | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/kernel/resources/organization/limits.py b/src/kernel/resources/organization/limits.py index d30bfbfa..d1bb6ecb 100644 --- a/src/kernel/resources/organization/limits.py +++ b/src/kernel/resources/organization/limits.py @@ -55,11 +55,7 @@ def retrieve( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> OrgLimits: - """ - Get the organization's concurrency limit — the maximum browsers running at once - across on-demand sessions and browser pool reservations — and the default - per-project concurrency cap applied to projects without an explicit override. - """ + """Get the organization's effective limits and managed auth usage.""" return self._get( "/org/limits", options=make_request_options( @@ -142,11 +138,7 @@ async def retrieve( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> OrgLimits: - """ - Get the organization's concurrency limit — the maximum browsers running at once - across on-demand sessions and browser pool reservations — and the default - per-project concurrency cap applied to projects without an explicit override. - """ + """Get the organization's effective limits and managed auth usage.""" return await self._get( "/org/limits", options=make_request_options( diff --git a/src/kernel/types/organization/org_limits.py b/src/kernel/types/organization/org_limits.py index 3d614996..bdc3cb4a 100644 --- a/src/kernel/types/organization/org_limits.py +++ b/src/kernel/types/organization/org_limits.py @@ -8,6 +8,27 @@ class OrgLimits(BaseModel): + auth_connections_used: int + """ + The organization's current non-deleted managed auth connections, counted + org-wide across every project. Compare against max_auth_connections to show + remaining capacity before a create is rejected with 403 insufficient_plan. + """ + + max_auth_connections: Optional[int] = None + """Maximum managed auth connections the organization's plan allows. + + Null means unlimited. Counted org-wide, so it cannot be multiplied across + projects. + """ + + min_health_check_interval_seconds: int + """ + Smallest health_check_interval the organization's plan accepts on a managed auth + connection. Requests below this are rejected with 400. Existing connections + stored below the floor are grandfathered until edited. + """ + default_project_max_concurrent_sessions: Optional[int] = None """ Default maximum concurrent browsers applied to every project that has no From fdb020d437b3b410b2d5758691f2818d5e744580 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 01:28:14 +0000 Subject: [PATCH 7/9] feat: Add region as a first-class API field with plan and flag gating Stainless-Generated-From: 91d0b1968ff1d553bd8eb0700df9b6c8d9167bce --- src/kernel/resources/browser_pools.py | 21 +++++++++++++++++++ src/kernel/resources/browsers/browsers.py | 20 ++++++++++++++++++ src/kernel/types/browser_create_params.py | 9 +++++++- src/kernel/types/browser_create_response.py | 4 ++++ src/kernel/types/browser_list_params.py | 3 +++ src/kernel/types/browser_list_response.py | 4 ++++ src/kernel/types/browser_pool.py | 4 ++++ .../types/browser_pool_acquire_response.py | 4 ++++ .../types/browser_pool_create_params.py | 9 +++++++- src/kernel/types/browser_pool_list_params.py | 5 ++++- src/kernel/types/browser_retrieve_response.py | 4 ++++ src/kernel/types/browser_update_response.py | 4 ++++ .../invocation_list_browsers_response.py | 4 ++++ tests/api_resources/test_browser_pools.py | 4 ++++ tests/api_resources/test_browsers.py | 4 ++++ 15 files changed, 100 insertions(+), 3 deletions(-) diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index d71fc272..18eafcfe 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Dict, Iterable, Optional +from typing_extensions import Literal import httpx @@ -70,6 +71,7 @@ def create( profile: browser_pool_create_params.Profile | Omit = omit, proxy_id: str | Omit = omit, refresh_on_profile_update: bool | Omit = omit, + region: Literal["us-east", "eu-west"] | Omit = omit, start_url: str | Omit = omit, stealth: bool | Omit = omit, telemetry: Optional[browser_pool_create_params.Telemetry] | Omit = omit, @@ -126,6 +128,10 @@ def create( pool browsers pick up the latest profile data. When a profile is provided during creation, this defaults to true. Requires a profile to be set on the pool. + region: Geographic region for the browser pool. It is fixed once the pool is created. + Region selection requires a Start-Up or Enterprise plan, defaults to us-east + when omitted on create. + 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 @@ -180,6 +186,7 @@ def create( "profile": profile, "proxy_id": proxy_id, "refresh_on_profile_update": refresh_on_profile_update, + "region": region, "start_url": start_url, "stealth": stealth, "telemetry": telemetry, @@ -386,6 +393,7 @@ def list( name: str | Omit = omit, offset: int | Omit = omit, query: str | Omit = omit, + region: Literal["us-east", "eu-west"] | 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, @@ -409,6 +417,8 @@ def list( query: Case-insensitive substring match against browser pool name. IDs match by exact value. + region: Filter pools by geographic region. Omit to list pools in all regions. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -431,6 +441,7 @@ def list( "name": name, "offset": offset, "query": query, + "region": region, }, browser_pool_list_params.BrowserPoolListParams, ), @@ -678,6 +689,7 @@ async def create( profile: browser_pool_create_params.Profile | Omit = omit, proxy_id: str | Omit = omit, refresh_on_profile_update: bool | Omit = omit, + region: Literal["us-east", "eu-west"] | Omit = omit, start_url: str | Omit = omit, stealth: bool | Omit = omit, telemetry: Optional[browser_pool_create_params.Telemetry] | Omit = omit, @@ -734,6 +746,10 @@ async def create( pool browsers pick up the latest profile data. When a profile is provided during creation, this defaults to true. Requires a profile to be set on the pool. + region: Geographic region for the browser pool. It is fixed once the pool is created. + Region selection requires a Start-Up or Enterprise plan, defaults to us-east + when omitted on create. + 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 @@ -788,6 +804,7 @@ async def create( "profile": profile, "proxy_id": proxy_id, "refresh_on_profile_update": refresh_on_profile_update, + "region": region, "start_url": start_url, "stealth": stealth, "telemetry": telemetry, @@ -994,6 +1011,7 @@ def list( name: str | Omit = omit, offset: int | Omit = omit, query: str | Omit = omit, + region: Literal["us-east", "eu-west"] | 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, @@ -1017,6 +1035,8 @@ def list( query: Case-insensitive substring match against browser pool name. IDs match by exact value. + region: Filter pools by geographic region. Omit to list pools in all regions. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -1039,6 +1059,7 @@ def list( "name": name, "offset": offset, "query": query, + "region": region, }, browser_pool_list_params.BrowserPoolListParams, ), diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 19f48988..35c9d612 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -175,6 +175,7 @@ def create( profile: BrowserProfile | Omit = omit, proxy: BrowserProxyConfigParam | Omit = omit, proxy_id: str | Omit = omit, + region: Literal["us-east", "eu-west"] | Omit = omit, start_url: str | Omit = omit, stealth: bool | Omit = omit, tags: TagsParam | Omit = omit, @@ -229,6 +230,10 @@ def create( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in the same project as the browser session. Deprecated in favor of proxy. + region: Geographic region for the browser session. It is fixed once the session is + created. Region selection requires a Start-Up or Enterprise plan, defaults to + us-east when omitted on create. + start_url: Optional URL to open when the browser session is created. Navigation is best-effort, so navigation failures do not prevent the session from being created. @@ -290,6 +295,7 @@ def create( "profile": profile, "proxy": proxy, "proxy_id": proxy_id, + "region": region, "start_url": start_url, "stealth": stealth, "tags": tags, @@ -440,6 +446,7 @@ def list( limit: int | Omit = omit, offset: int | Omit = omit, query: str | Omit = omit, + region: Literal["us-east", "eu-west"] | Omit = omit, status: Literal["active", "deleted", "all"] | Omit = omit, tags: Dict[str, str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -464,6 +471,8 @@ def list( query: Search browsers by name, session ID, profile name or ID, proxy ID, or pool name. + region: Filter sessions by geographic region. Omit to list sessions in all regions. + status: Filter sessions by status. "active" returns only active sessions (default), "deleted" returns only soft-deleted sessions, "all" returns both. @@ -493,6 +502,7 @@ def list( "limit": limit, "offset": offset, "query": query, + "region": region, "status": status, "tags": tags, }, @@ -775,6 +785,7 @@ async def create( profile: BrowserProfile | Omit = omit, proxy: BrowserProxyConfigParam | Omit = omit, proxy_id: str | Omit = omit, + region: Literal["us-east", "eu-west"] | Omit = omit, start_url: str | Omit = omit, stealth: bool | Omit = omit, tags: TagsParam | Omit = omit, @@ -829,6 +840,10 @@ async def create( proxy_id: Optional proxy to associate to the browser session. Must reference a proxy in the same project as the browser session. Deprecated in favor of proxy. + region: Geographic region for the browser session. It is fixed once the session is + created. Region selection requires a Start-Up or Enterprise plan, defaults to + us-east when omitted on create. + start_url: Optional URL to open when the browser session is created. Navigation is best-effort, so navigation failures do not prevent the session from being created. @@ -890,6 +905,7 @@ async def create( "profile": profile, "proxy": proxy, "proxy_id": proxy_id, + "region": region, "start_url": start_url, "stealth": stealth, "tags": tags, @@ -1040,6 +1056,7 @@ def list( limit: int | Omit = omit, offset: int | Omit = omit, query: str | Omit = omit, + region: Literal["us-east", "eu-west"] | Omit = omit, status: Literal["active", "deleted", "all"] | Omit = omit, tags: Dict[str, str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -1064,6 +1081,8 @@ def list( query: Search browsers by name, session ID, profile name or ID, proxy ID, or pool name. + region: Filter sessions by geographic region. Omit to list sessions in all regions. + status: Filter sessions by status. "active" returns only active sessions (default), "deleted" returns only soft-deleted sessions, "all" returns both. @@ -1093,6 +1112,7 @@ def list( "limit": limit, "offset": offset, "query": query, + "region": region, "status": status, "tags": tags, }, diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 262383c5..e30878ec 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Dict, Iterable, Optional -from typing_extensions import TypedDict +from typing_extensions import Literal, TypedDict from .tags_param import TagsParam from .browser_proxy_config_param import BrowserProxyConfigParam @@ -90,6 +90,13 @@ class BrowserCreateParams(TypedDict, total=False): favor of proxy. """ + region: Literal["us-east", "eu-west"] + """Geographic region for the browser session. + + It is fixed once the session is created. Region selection requires a Start-Up or + Enterprise plan, defaults to us-east when omitted on create. + """ + start_url: str """Optional URL to open when the browser session is created. diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 8618e7e5..429151df 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -2,6 +2,7 @@ from typing import Dict, Optional from datetime import datetime +from typing_extensions import Literal from .tags import Tags from .profile import Profile @@ -25,6 +26,9 @@ class BrowserCreateResponse(BaseModel): headless: bool """Whether the browser session is running in headless mode.""" + region: Literal["us-east", "eu-west"] + """Geographic region of the browser session. Fixed once the session is created.""" + session_id: str """Unique identifier for the browser session""" diff --git a/src/kernel/types/browser_list_params.py b/src/kernel/types/browser_list_params.py index 57de2521..8b829a8d 100644 --- a/src/kernel/types/browser_list_params.py +++ b/src/kernel/types/browser_list_params.py @@ -27,6 +27,9 @@ class BrowserListParams(TypedDict, total=False): Search browsers by name, session ID, profile name or ID, proxy ID, or pool name. """ + region: Literal["us-east", "eu-west"] + """Filter sessions by geographic region. Omit to list sessions in all regions.""" + status: Literal["active", "deleted", "all"] """Filter sessions by status. diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 8f4917a8..42d261a3 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -2,6 +2,7 @@ from typing import Dict, Optional from datetime import datetime +from typing_extensions import Literal from .tags import Tags from .profile import Profile @@ -25,6 +26,9 @@ class BrowserListResponse(BaseModel): headless: bool """Whether the browser session is running in headless mode.""" + region: Literal["us-east", "eu-west"] + """Geographic region of the browser session. Fixed once the session is created.""" + session_id: str """Unique identifier for the browser session""" diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py index 344ffd77..6f5a2052 100644 --- a/src/kernel/types/browser_pool.py +++ b/src/kernel/types/browser_pool.py @@ -2,6 +2,7 @@ from typing import Dict, List, Optional from datetime import datetime +from typing_extensions import Literal from .._models import BaseModel from .shared.browser_viewport import BrowserViewport @@ -166,6 +167,9 @@ class BrowserPool(BaseModel): (echoed as sent on create). """ + region: Literal["us-east", "eu-west"] + """Geographic region of the browser pool. Fixed once the pool is created.""" + name: Optional[str] = None """Browser pool name, if set""" diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index 7d849958..7096b9f6 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -2,6 +2,7 @@ from typing import Dict, Optional from datetime import datetime +from typing_extensions import Literal from .tags import Tags from .profile import Profile @@ -25,6 +26,9 @@ class BrowserPoolAcquireResponse(BaseModel): headless: bool """Whether the browser session is running in headless mode.""" + region: Literal["us-east", "eu-west"] + """Geographic region of the browser session. Fixed once the session is created.""" + session_id: str """Unique identifier 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 fb6aa63b..123fb172 100644 --- a/src/kernel/types/browser_pool_create_params.py +++ b/src/kernel/types/browser_pool_create_params.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Dict, Iterable, Optional -from typing_extensions import Required, TypedDict +from typing_extensions import Literal, Required, TypedDict from .shared_params.browser_viewport import BrowserViewport from .shared_params.browser_extension import BrowserExtension @@ -83,6 +83,13 @@ class BrowserPoolCreateParams(TypedDict, total=False): creation, this defaults to true. Requires a profile to be set on the pool. """ + region: Literal["us-east", "eu-west"] + """Geographic region for the browser pool. + + It is fixed once the pool is created. Region selection requires a Start-Up or + Enterprise plan, defaults to us-east when omitted on create. + """ + start_url: str """Optional URL to navigate to when a new browser is warmed into the pool. diff --git a/src/kernel/types/browser_pool_list_params.py b/src/kernel/types/browser_pool_list_params.py index 75fea5db..36f2f8ca 100644 --- a/src/kernel/types/browser_pool_list_params.py +++ b/src/kernel/types/browser_pool_list_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing_extensions import TypedDict +from typing_extensions import Literal, TypedDict __all__ = ["BrowserPoolListParams"] @@ -27,3 +27,6 @@ class BrowserPoolListParams(TypedDict, total=False): IDs match by exact value. """ + + region: Literal["us-east", "eu-west"] + """Filter pools by geographic region. Omit to list pools in all regions.""" diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 3afc6082..e7459a0c 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -2,6 +2,7 @@ from typing import Dict, Optional from datetime import datetime +from typing_extensions import Literal from .tags import Tags from .profile import Profile @@ -25,6 +26,9 @@ class BrowserRetrieveResponse(BaseModel): headless: bool """Whether the browser session is running in headless mode.""" + region: Literal["us-east", "eu-west"] + """Geographic region of the browser session. Fixed once the session is created.""" + session_id: str """Unique identifier for the browser session""" diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index d60fe900..4cfe18d4 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -2,6 +2,7 @@ from typing import Dict, Optional from datetime import datetime +from typing_extensions import Literal from .tags import Tags from .profile import Profile @@ -25,6 +26,9 @@ class BrowserUpdateResponse(BaseModel): headless: bool """Whether the browser session is running in headless mode.""" + region: Literal["us-east", "eu-west"] + """Geographic region of the browser session. Fixed once the session is created.""" + session_id: str """Unique identifier for the browser session""" diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py index 5cfe7e89..caedb6ea 100644 --- a/src/kernel/types/invocation_list_browsers_response.py +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -2,6 +2,7 @@ from typing import Dict, List, Optional from datetime import datetime +from typing_extensions import Literal from .tags import Tags from .profile import Profile @@ -25,6 +26,9 @@ class Browser(BaseModel): headless: bool """Whether the browser session is running in headless mode.""" + region: Literal["us-east", "eu-west"] + """Geographic region of the browser session. Fixed once the session is created.""" + session_id: str """Unique identifier for the browser session""" diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py index a85d5be7..8922c801 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: }, proxy_id="proxy_id", refresh_on_profile_update=True, + region="us-east", start_url="https://example.com", stealth=True, telemetry={ @@ -267,6 +268,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None: name="name", offset=0, query="query", + region="us-east", ) assert_matches_type(SyncOffsetPagination[BrowserPool], browser_pool, path=["response"]) @@ -557,6 +559,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> }, proxy_id="proxy_id", refresh_on_profile_update=True, + region="us-east", start_url="https://example.com", stealth=True, telemetry={ @@ -773,6 +776,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N name="name", offset=0, query="query", + region="us-east", ) assert_matches_type(AsyncOffsetPagination[BrowserPool], browser_pool, path=["response"]) diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 8b0a6182..3fd4c6dc 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -57,6 +57,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: "name": "x", }, proxy_id="proxy_id", + region="us-east", start_url="https://example.com", stealth=True, tags={ @@ -278,6 +279,7 @@ def test_method_list_with_all_params(self, client: Kernel) -> None: limit=1, offset=0, query="query", + region="us-east", status="active", tags={"foo": "string"}, ) @@ -512,6 +514,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> "name": "x", }, proxy_id="proxy_id", + region="us-east", start_url="https://example.com", stealth=True, tags={ @@ -733,6 +736,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> N limit=1, offset=0, query="query", + region="us-east", status="active", tags={"foo": "string"}, ) From b30c0e31182f09247bbf32823c256e27725fdbfc Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:04:42 +0000 Subject: [PATCH 8/9] feat: Add typed network config with private_hosts to browsers and pools Stainless-Generated-From: d10c7305e30fe0869a9027e670f3b6ca1121870c --- README.md | 4 +- api.md | 1 + src/kernel/resources/browser_pools.py | 25 ++++++++++++ src/kernel/resources/browsers/browsers.py | 9 +++++ src/kernel/types/__init__.py | 2 + src/kernel/types/browser_create_params.py | 7 ++++ src/kernel/types/browser_create_response.py | 7 ++++ src/kernel/types/browser_list_response.py | 7 ++++ src/kernel/types/browser_network_config.py | 37 ++++++++++++++++++ .../types/browser_network_config_param.py | 39 +++++++++++++++++++ src/kernel/types/browser_pool.py | 7 ++++ .../types/browser_pool_acquire_response.py | 7 ++++ .../types/browser_pool_create_params.py | 4 ++ .../types/browser_pool_update_params.py | 10 +++++ src/kernel/types/browser_retrieve_response.py | 7 ++++ src/kernel/types/browser_update_response.py | 7 ++++ .../invocation_list_browsers_response.py | 7 ++++ tests/api_resources/test_browser_pools.py | 4 ++ tests/api_resources/test_browsers.py | 2 + 19 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 src/kernel/types/browser_network_config.py create mode 100644 src/kernel/types/browser_network_config_param.py diff --git a/README.md b/README.md index 2e8ed560..35a3bc41 100644 --- a/README.md +++ b/README.md @@ -203,9 +203,9 @@ from kernel import Kernel client = Kernel() browser = client.browsers.create( - profile={}, + network={}, ) -print(browser.profile) +print(browser.network) ``` ## File uploads diff --git a/api.md b/api.md index 65ac4c06..b991db92 100644 --- a/api.md +++ b/api.md @@ -80,6 +80,7 @@ Types: ```python from kernel.types import ( + BrowserNetworkConfig, BrowserPoolRef, BrowserProxy, BrowserProxyConfig, diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index 18eafcfe..99bda961 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -29,6 +29,7 @@ from .._base_client import AsyncPaginator, make_request_options from ..types.tags_param import TagsParam from ..types.browser_pool import BrowserPool +from ..types.browser_network_config_param import BrowserNetworkConfigParam from ..types.browser_pool_acquire_response import BrowserPoolAcquireResponse from ..types.shared_params.browser_viewport import BrowserViewport from ..types.shared_params.browser_extension import BrowserExtension @@ -68,6 +69,7 @@ def create( headless: bool | Omit = omit, kiosk_mode: bool | Omit = omit, name: str | Omit = omit, + network: BrowserNetworkConfigParam | Omit = omit, profile: browser_pool_create_params.Profile | Omit = omit, proxy_id: str | Omit = omit, refresh_on_profile_update: bool | Omit = omit, @@ -115,6 +117,8 @@ def create( name: Optional name for the browser pool. Must be unique within the project. + network: Network configuration applied to browsers in this pool. + profile: Profile configuration for browsers in a pool. Provide either id or name. Profiles must be created beforehand. Unlike single browser sessions, pools load the profile read-only and never persist changes back to it, so save_changes is @@ -183,6 +187,7 @@ def create( "headless": headless, "kiosk_mode": kiosk_mode, "name": name, + "network": network, "profile": profile, "proxy_id": proxy_id, "refresh_on_profile_update": refresh_on_profile_update, @@ -245,6 +250,7 @@ def update( headless: bool | Omit = omit, kiosk_mode: bool | Omit = omit, name: str | Omit = omit, + network: BrowserNetworkConfigParam | Omit = omit, profile: browser_pool_update_params.Profile | Omit = omit, proxy_id: str | Omit = omit, refresh_on_profile_update: bool | Omit = omit, @@ -298,6 +304,12 @@ def update( name: If provided, replaces the pool name. Empty string is a no-op; the pool name cannot be cleared or reset to empty once assigned. + network: If provided, replaces the pool's network configuration. Omit to leave the + existing configuration unchanged; an empty object ({}) removes it, while + network: {private_hosts: []} sets an explicit empty list. Only applied to + browsers created in the pool after the update; browsers already in the pool keep + their configuration until discarded (see discard_all_idle). + profile: Profile configuration for browsers in a pool. Provide either id or name. Profiles must be created beforehand. Unlike single browser sessions, pools load the profile read-only and never persist changes back to it, so save_changes is @@ -368,6 +380,7 @@ def update( "headless": headless, "kiosk_mode": kiosk_mode, "name": name, + "network": network, "profile": profile, "proxy_id": proxy_id, "refresh_on_profile_update": refresh_on_profile_update, @@ -686,6 +699,7 @@ async def create( headless: bool | Omit = omit, kiosk_mode: bool | Omit = omit, name: str | Omit = omit, + network: BrowserNetworkConfigParam | Omit = omit, profile: browser_pool_create_params.Profile | Omit = omit, proxy_id: str | Omit = omit, refresh_on_profile_update: bool | Omit = omit, @@ -733,6 +747,8 @@ async def create( name: Optional name for the browser pool. Must be unique within the project. + network: Network configuration applied to browsers in this pool. + profile: Profile configuration for browsers in a pool. Provide either id or name. Profiles must be created beforehand. Unlike single browser sessions, pools load the profile read-only and never persist changes back to it, so save_changes is @@ -801,6 +817,7 @@ async def create( "headless": headless, "kiosk_mode": kiosk_mode, "name": name, + "network": network, "profile": profile, "proxy_id": proxy_id, "refresh_on_profile_update": refresh_on_profile_update, @@ -863,6 +880,7 @@ async def update( headless: bool | Omit = omit, kiosk_mode: bool | Omit = omit, name: str | Omit = omit, + network: BrowserNetworkConfigParam | Omit = omit, profile: browser_pool_update_params.Profile | Omit = omit, proxy_id: str | Omit = omit, refresh_on_profile_update: bool | Omit = omit, @@ -916,6 +934,12 @@ async def update( name: If provided, replaces the pool name. Empty string is a no-op; the pool name cannot be cleared or reset to empty once assigned. + network: If provided, replaces the pool's network configuration. Omit to leave the + existing configuration unchanged; an empty object ({}) removes it, while + network: {private_hosts: []} sets an explicit empty list. Only applied to + browsers created in the pool after the update; browsers already in the pool keep + their configuration until discarded (see discard_all_idle). + profile: Profile configuration for browsers in a pool. Provide either id or name. Profiles must be created beforehand. Unlike single browser sessions, pools load the profile read-only and never persist changes back to it, so save_changes is @@ -986,6 +1010,7 @@ async def update( "headless": headless, "kiosk_mode": kiosk_mode, "name": name, + "network": network, "profile": profile, "proxy_id": proxy_id, "refresh_on_profile_update": refresh_on_profile_update, diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 35c9d612..7e46a361 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -98,6 +98,7 @@ from ...types.browser_update_response import BrowserUpdateResponse from ...types.browser_retrieve_response import BrowserRetrieveResponse from ...types.browser_proxy_config_param import BrowserProxyConfigParam +from ...types.browser_network_config_param import BrowserNetworkConfigParam from ...types.shared_params.browser_profile import BrowserProfile from ...types.shared_params.browser_viewport import BrowserViewport from ...types.shared_params.browser_extension import BrowserExtension @@ -172,6 +173,7 @@ def create( invocation_id: str | Omit = omit, kiosk_mode: bool | Omit = omit, name: str | Omit = omit, + network: BrowserNetworkConfigParam | Omit = omit, profile: BrowserProfile | Omit = omit, proxy: BrowserProxyConfigParam | Omit = omit, proxy_id: str | Omit = omit, @@ -215,6 +217,8 @@ def create( the dashboard. Must be unique among active sessions within the project. Can be changed later via PATCH /browsers/{id_or_name}. + network: Network configuration for the browser session. Cannot be changed after creation. + profile: Profile selection for the browser session. Provide either id or name. If specified, the matching profile will be loaded into the browser session. Profiles must be created beforehand. @@ -292,6 +296,7 @@ def create( "invocation_id": invocation_id, "kiosk_mode": kiosk_mode, "name": name, + "network": network, "profile": profile, "proxy": proxy, "proxy_id": proxy_id, @@ -782,6 +787,7 @@ async def create( invocation_id: str | Omit = omit, kiosk_mode: bool | Omit = omit, name: str | Omit = omit, + network: BrowserNetworkConfigParam | Omit = omit, profile: BrowserProfile | Omit = omit, proxy: BrowserProxyConfigParam | Omit = omit, proxy_id: str | Omit = omit, @@ -825,6 +831,8 @@ async def create( the dashboard. Must be unique among active sessions within the project. Can be changed later via PATCH /browsers/{id_or_name}. + network: Network configuration for the browser session. Cannot be changed after creation. + profile: Profile selection for the browser session. Provide either id or name. If specified, the matching profile will be loaded into the browser session. Profiles must be created beforehand. @@ -902,6 +910,7 @@ async def create( "invocation_id": invocation_id, "kiosk_mode": kiosk_mode, "name": name, + "network": network, "profile": profile, "proxy": proxy, "proxy_id": proxy_id, diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index e86ac9c4..13c85213 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -58,6 +58,7 @@ from .project_update_params import ProjectUpdateParams as ProjectUpdateParams from .proxy_create_response import ProxyCreateResponse as ProxyCreateResponse from .proxy_update_response import ProxyUpdateResponse as ProxyUpdateResponse +from .browser_network_config import BrowserNetworkConfig as BrowserNetworkConfig from .credential_list_params import CredentialListParams as CredentialListParams from .deployment_list_params import DeploymentListParams as DeploymentListParams from .deployment_state_event import DeploymentStateEvent as DeploymentStateEvent @@ -96,6 +97,7 @@ from .invocation_update_response import InvocationUpdateResponse as InvocationUpdateResponse from .browser_pool_acquire_params import BrowserPoolAcquireParams as BrowserPoolAcquireParams from .browser_pool_release_params import BrowserPoolReleaseParams as BrowserPoolReleaseParams +from .browser_network_config_param import BrowserNetworkConfigParam as BrowserNetworkConfigParam from .deployment_retrieve_response import DeploymentRetrieveResponse as DeploymentRetrieveResponse from .invocation_retrieve_response import InvocationRetrieveResponse as InvocationRetrieveResponse from .audit_log_export_chunk_params import AuditLogExportChunkParams as AuditLogExportChunkParams diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index e30878ec..0c280602 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -7,6 +7,7 @@ from .tags_param import TagsParam from .browser_proxy_config_param import BrowserProxyConfigParam +from .browser_network_config_param import BrowserNetworkConfigParam from .shared_params.browser_profile import BrowserProfile from .shared_params.browser_viewport import BrowserViewport from .shared_params.browser_extension import BrowserExtension @@ -64,6 +65,12 @@ class BrowserCreateParams(TypedDict, total=False): changed later via PATCH /browsers/{id_or_name}. """ + network: BrowserNetworkConfigParam + """Network configuration for the browser session. + + Cannot be changed after creation. + """ + profile: BrowserProfile """Profile selection for the browser session. diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index 429151df..f8d625e9 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -10,6 +10,7 @@ from .browser_proxy import BrowserProxy from .browser_usage import BrowserUsage from .browser_pool_ref import BrowserPoolRef +from .browser_network_config import BrowserNetworkConfig from .shared.browser_viewport import BrowserViewport from .browsers.browser_telemetry_config import BrowserTelemetryConfig @@ -72,6 +73,12 @@ class BrowserCreateResponse(BaseModel): name: Optional[str] = None """Human-readable name of the browser session, if one was set at creation.""" + network: Optional[BrowserNetworkConfig] = None + """Network configuration the session was created with, if any. + + Omitted when the session has no network configuration. + """ + pool: Optional[BrowserPoolRef] = None """Browser pool this session was acquired from, if any.""" diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 42d261a3..108bbef2 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -10,6 +10,7 @@ from .browser_proxy import BrowserProxy from .browser_usage import BrowserUsage from .browser_pool_ref import BrowserPoolRef +from .browser_network_config import BrowserNetworkConfig from .shared.browser_viewport import BrowserViewport from .browsers.browser_telemetry_config import BrowserTelemetryConfig @@ -72,6 +73,12 @@ class BrowserListResponse(BaseModel): name: Optional[str] = None """Human-readable name of the browser session, if one was set at creation.""" + network: Optional[BrowserNetworkConfig] = None + """Network configuration the session was created with, if any. + + Omitted when the session has no network configuration. + """ + pool: Optional[BrowserPoolRef] = None """Browser pool this session was acquired from, if any.""" diff --git a/src/kernel/types/browser_network_config.py b/src/kernel/types/browser_network_config.py new file mode 100644 index 00000000..e1941816 --- /dev/null +++ b/src/kernel/types/browser_network_config.py @@ -0,0 +1,37 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel + +__all__ = ["BrowserNetworkConfig"] + + +class BrowserNetworkConfig(BaseModel): + """Network configuration for a browser session or browser pool.""" + + private_hosts: Optional[List[str]] = None + """ + Destinations the browser reaches directly through the session's own network + instead of through Kernel-managed egress — for private hosts reachable over a + VPN or tunnel the session has joined (e.g. a Tailscale tailnet). By default, + private IP ranges already route directly: RFC1918 (10.0.0.0/8, 172.16.0.0/12, + 192.168.0.0/16), CGNAT/Tailscale (100.64.0.0/10), and IPv6 ULA (fc00::/7). An + explicitly supplied list replaces those defaults with exactly the entries given, + and an empty list ([]) disables them so all traffic uses Kernel-managed egress; + omit private_hosts to keep the defaults. Entries are hostname patterns + ("_.example.ts.net", "preview.internal") or IP/CIDR literals ("100.64.0.0/10", + "10.1.30.63"). IP and CIDR entries only match URLs written with a literal IP + address; they never match hostnames that resolve into the range, so private DNS + names need a hostname entry even when they resolve inside the default ranges. + CIDRs must be in canonical masked form (host bits zero), and only the private + ranges listed above are accepted; public, loopback, link-local, and unspecified + ranges are rejected. Exact IPv6 addresses must be bracketed ("[fd00::1]"); IPv6 + CIDR ranges are unbracketed ("fd00::/8"). Wildcards are limited to one leading + "_." over a suffix with at least two labels that is not a public suffix (so + "_.co.uk" or "_.ts.net" are rejected, while "\\**.example.ts.net" is accepted). + Hostname and IP entries may carry a port; CIDR ranges may not. Hostname entries + are not resolved during validation, so callers must ensure they identify private + destinations. Not related to a proxy's bypass_hosts, which selects between + upstream-proxy and Kernel-managed direct egress and cannot reach into a VPN. + """ diff --git a/src/kernel/types/browser_network_config_param.py b/src/kernel/types/browser_network_config_param.py new file mode 100644 index 00000000..49f7ea64 --- /dev/null +++ b/src/kernel/types/browser_network_config_param.py @@ -0,0 +1,39 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +from .._types import SequenceNotStr + +__all__ = ["BrowserNetworkConfigParam"] + + +class BrowserNetworkConfigParam(TypedDict, total=False): + """Network configuration for a browser session or browser pool.""" + + private_hosts: SequenceNotStr[str] + """ + Destinations the browser reaches directly through the session's own network + instead of through Kernel-managed egress — for private hosts reachable over a + VPN or tunnel the session has joined (e.g. a Tailscale tailnet). By default, + private IP ranges already route directly: RFC1918 (10.0.0.0/8, 172.16.0.0/12, + 192.168.0.0/16), CGNAT/Tailscale (100.64.0.0/10), and IPv6 ULA (fc00::/7). An + explicitly supplied list replaces those defaults with exactly the entries given, + and an empty list ([]) disables them so all traffic uses Kernel-managed egress; + omit private_hosts to keep the defaults. Entries are hostname patterns + ("_.example.ts.net", "preview.internal") or IP/CIDR literals ("100.64.0.0/10", + "10.1.30.63"). IP and CIDR entries only match URLs written with a literal IP + address; they never match hostnames that resolve into the range, so private DNS + names need a hostname entry even when they resolve inside the default ranges. + CIDRs must be in canonical masked form (host bits zero), and only the private + ranges listed above are accepted; public, loopback, link-local, and unspecified + ranges are rejected. Exact IPv6 addresses must be bracketed ("[fd00::1]"); IPv6 + CIDR ranges are unbracketed ("fd00::/8"). Wildcards are limited to one leading + "_." over a suffix with at least two labels that is not a public suffix (so + "_.co.uk" or "_.ts.net" are rejected, while "\\**.example.ts.net" is accepted). + Hostname and IP entries may carry a port; CIDR ranges may not. Hostname entries + are not resolved during validation, so callers must ensure they identify private + destinations. Not related to a proxy's bypass_hosts, which selects between + upstream-proxy and Kernel-managed direct egress and cannot reach into a VPN. + """ diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py index 6f5a2052..4504fc5d 100644 --- a/src/kernel/types/browser_pool.py +++ b/src/kernel/types/browser_pool.py @@ -5,6 +5,7 @@ from typing_extensions import Literal from .._models import BaseModel +from .browser_network_config import BrowserNetworkConfig from .shared.browser_viewport import BrowserViewport from .shared.browser_extension import BrowserExtension from .browsers.browser_telemetry_config import BrowserTelemetryConfig @@ -75,6 +76,12 @@ class BrowserPoolConfig(BaseModel): name: Optional[str] = None """Optional name for the browser pool. Must be unique within the project.""" + network: Optional[BrowserNetworkConfig] = None + """Network configuration applied to browsers in this pool, if any. + + Omitted when the pool has no network configuration. + """ + profile: Optional[BrowserPoolConfigProfile] = None """Profile configuration for browsers in a pool. diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index 7096b9f6..720dc569 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -10,6 +10,7 @@ from .browser_proxy import BrowserProxy from .browser_usage import BrowserUsage from .browser_pool_ref import BrowserPoolRef +from .browser_network_config import BrowserNetworkConfig from .shared.browser_viewport import BrowserViewport from .browsers.browser_telemetry_config import BrowserTelemetryConfig @@ -72,6 +73,12 @@ class BrowserPoolAcquireResponse(BaseModel): name: Optional[str] = None """Human-readable name of the browser session, if one was set at creation.""" + network: Optional[BrowserNetworkConfig] = None + """Network configuration the session was created with, if any. + + Omitted when the session has no network configuration. + """ + pool: Optional[BrowserPoolRef] = None """Browser pool this session was acquired from, if any.""" diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py index 123fb172..474e4243 100644 --- a/src/kernel/types/browser_pool_create_params.py +++ b/src/kernel/types/browser_pool_create_params.py @@ -5,6 +5,7 @@ from typing import Dict, Iterable, Optional from typing_extensions import Literal, Required, TypedDict +from .browser_network_config_param import BrowserNetworkConfigParam from .shared_params.browser_viewport import BrowserViewport from .shared_params.browser_extension import BrowserExtension from .browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam @@ -61,6 +62,9 @@ class BrowserPoolCreateParams(TypedDict, total=False): name: str """Optional name for the browser pool. Must be unique within the project.""" + network: BrowserNetworkConfigParam + """Network configuration applied to browsers in this pool.""" + profile: Profile """Profile configuration for browsers in a pool. diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py index 069181c3..36693003 100644 --- a/src/kernel/types/browser_pool_update_params.py +++ b/src/kernel/types/browser_pool_update_params.py @@ -5,6 +5,7 @@ from typing import Dict, Iterable, Optional from typing_extensions import TypedDict +from .browser_network_config_param import BrowserNetworkConfigParam from .shared_params.browser_viewport import BrowserViewport from .shared_params.browser_extension import BrowserExtension from .browsers.browser_telemetry_categories_config_param import BrowserTelemetryCategoriesConfigParam @@ -67,6 +68,15 @@ class BrowserPoolUpdateParams(TypedDict, total=False): assigned. """ + network: BrowserNetworkConfigParam + """If provided, replaces the pool's network configuration. + + Omit to leave the existing configuration unchanged; an empty object ({}) removes + it, while network: {private_hosts: []} sets an explicit empty list. Only applied + to browsers created in the pool after the update; browsers already in the pool + keep their configuration until discarded (see discard_all_idle). + """ + profile: Profile """Profile configuration for browsers in a pool. diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index e7459a0c..8e39a607 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -10,6 +10,7 @@ from .browser_proxy import BrowserProxy from .browser_usage import BrowserUsage from .browser_pool_ref import BrowserPoolRef +from .browser_network_config import BrowserNetworkConfig from .shared.browser_viewport import BrowserViewport from .browsers.browser_telemetry_config import BrowserTelemetryConfig @@ -72,6 +73,12 @@ class BrowserRetrieveResponse(BaseModel): name: Optional[str] = None """Human-readable name of the browser session, if one was set at creation.""" + network: Optional[BrowserNetworkConfig] = None + """Network configuration the session was created with, if any. + + Omitted when the session has no network configuration. + """ + pool: Optional[BrowserPoolRef] = None """Browser pool this session was acquired from, if any.""" diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index 4cfe18d4..c04f43f8 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -10,6 +10,7 @@ from .browser_proxy import BrowserProxy from .browser_usage import BrowserUsage from .browser_pool_ref import BrowserPoolRef +from .browser_network_config import BrowserNetworkConfig from .shared.browser_viewport import BrowserViewport from .browsers.browser_telemetry_config import BrowserTelemetryConfig @@ -72,6 +73,12 @@ class BrowserUpdateResponse(BaseModel): name: Optional[str] = None """Human-readable name of the browser session, if one was set at creation.""" + network: Optional[BrowserNetworkConfig] = None + """Network configuration the session was created with, if any. + + Omitted when the session has no network configuration. + """ + pool: Optional[BrowserPoolRef] = None """Browser pool this session was acquired from, if any.""" diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py index caedb6ea..bcf8dd40 100644 --- a/src/kernel/types/invocation_list_browsers_response.py +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -10,6 +10,7 @@ from .browser_proxy import BrowserProxy from .browser_usage import BrowserUsage from .browser_pool_ref import BrowserPoolRef +from .browser_network_config import BrowserNetworkConfig from .shared.browser_viewport import BrowserViewport from .browsers.browser_telemetry_config import BrowserTelemetryConfig @@ -72,6 +73,12 @@ class Browser(BaseModel): name: Optional[str] = None """Human-readable name of the browser session, if one was set at creation.""" + network: Optional[BrowserNetworkConfig] = None + """Network configuration the session was created with, if any. + + Omitted when the session has no network configuration. + """ + pool: Optional[BrowserPoolRef] = None """Browser pool this session was acquired from, if any.""" diff --git a/tests/api_resources/test_browser_pools.py b/tests/api_resources/test_browser_pools.py index 8922c801..e411df27 100644 --- a/tests/api_resources/test_browser_pools.py +++ b/tests/api_resources/test_browser_pools.py @@ -45,6 +45,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: headless=False, kiosk_mode=True, name="my-pool", + network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]}, profile={ "id": "id", "name": "name", @@ -179,6 +180,7 @@ def test_method_update_with_all_params(self, client: Kernel) -> None: headless=False, kiosk_mode=True, name="my-pool", + network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]}, profile={ "id": "id", "name": "name", @@ -553,6 +555,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> headless=False, kiosk_mode=True, name="my-pool", + network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]}, profile={ "id": "id", "name": "name", @@ -687,6 +690,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> headless=False, kiosk_mode=True, name="my-pool", + network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]}, profile={ "id": "id", "name": "name", diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 3fd4c6dc..ae017186 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -46,6 +46,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: invocation_id="rr33xuugxj9h0bkf1rdt2bet", kiosk_mode=True, name="checkout-flow-1", + network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]}, profile={ "id": "id", "name": "name", @@ -503,6 +504,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> invocation_id="rr33xuugxj9h0bkf1rdt2bet", kiosk_mode=True, name="checkout-flow-1", + network={"private_hosts": ["*.example.ts.net", "100.64.0.0/10"]}, profile={ "id": "id", "name": "name", From f202d649655977511bf180a998c7aac8fa940914 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:09:00 +0000 Subject: [PATCH 9/9] release: 0.89.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 d80a91e2..6af24e3d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.88.0" + ".": "0.89.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 9137ac6f..92fb5212 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [0.89.0](https://github.com/kernel/kernel-python-sdk/compare/v0.88.0...v0.89.0) (2026-08-12) + + +### Features + +* Add region as a first-class API field with plan and flag gating ([fdb020d](https://github.com/kernel/kernel-python-sdk/commit/fdb020d437b3b410b2d5758691f2818d5e744580)) +* Add typed network config with private_hosts to browsers and pools ([b30c0e3](https://github.com/kernel/kernel-python-sdk/commit/b30c0e31182f09247bbf32823c256e27725fdbfc)) +* Expose plan-derived auth limits on GET /org/limits ([9ff1b44](https://github.com/kernel/kernel-python-sdk/commit/9ff1b445f8f2bb3f9bb24f613e22c8b940fc1d5a)) + ## [0.88.0](https://github.com/kernel/kernel-python-sdk/compare/v0.87.0...v0.88.0) (2026-08-10) ### Features diff --git a/pyproject.toml b/pyproject.toml index db2cb8e0..57df3c54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.88.0" +version = "0.89.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 892c18cb..e4082c56 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.88.0" # x-release-please-version +__version__ = "0.89.0" # x-release-please-version