diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index a9d0cc1..56ee145 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.69.0"
+ ".": "0.70.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index 4424fe7..db9991c 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 120
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-cde481b2f320ce48f83db84ae96226b0e7568146c9387c4fefebf286ecb0dd0a.yml
-openapi_spec_hash: 6bd86d767290fcd7e2a6aae26dff5417
-config_hash: 03c7e57f268c750e2415831662e95969
+configured_endpoints: 121
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-bccc42bb10d1afe703f0374d11f33e775522069d69a13bb2345cc566a49ba4f3.yml
+openapi_spec_hash: 9f00975c0e741ed84011413674313be0
+config_hash: 3a50aee540dce69a53bb8942f5086f5e
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b73a808..088dc0c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## 0.70.0 (2026-06-23)
+
+Full Changelog: [v0.69.0...v0.70.0](https://github.com/kernel/kernel-go-sdk/compare/v0.69.0...v0.70.0)
+
+### Features
+
+* Align browser-pool timeout/viewport/fill-rate contract with implementation; reject save_changes on update ([86fd2d4](https://github.com/kernel/kernel-go-sdk/commit/86fd2d44854ed0bfebdc83f848d2546fc64b84b4))
+* **api:** add GET /extensions/{id_or_name}/metadata ([95a4d55](https://github.com/kernel/kernel-go-sdk/commit/95a4d55245360ff26318260f11ca393955e9dde0))
+
## 0.69.0 (2026-06-18)
Full Changelog: [v0.68.0...v0.69.0](https://github.com/kernel/kernel-go-sdk/compare/v0.68.0...v0.69.0)
diff --git a/README.md b/README.md
index 2a3c9f8..acd9c09 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@ Or to pin the version:
```sh
-go get -u 'github.com/kernel/kernel-go-sdk@v0.69.0'
+go get -u 'github.com/kernel/kernel-go-sdk@v0.70.0'
```
diff --git a/api.md b/api.md
index 0c51245..2334965 100644
--- a/api.md
+++ b/api.md
@@ -317,6 +317,7 @@ Methods:
Response Types:
- kernel.ExtensionListResponse
+- kernel.ExtensionGetResponse
- kernel.ExtensionUploadResponse
Methods:
@@ -325,6 +326,7 @@ Methods:
- client.Extensions.Delete(ctx context.Context, idOrName string) error
- client.Extensions.Download(ctx context.Context, idOrName string) (\*http.Response, error)
- client.Extensions.DownloadFromChromeStore(ctx context.Context, query kernel.ExtensionDownloadFromChromeStoreParams) (\*http.Response, error)
+- client.Extensions.Get(ctx context.Context, idOrName string) (\*kernel.ExtensionGetResponse, error)
- client.Extensions.Upload(ctx context.Context, body kernel.ExtensionUploadParams) (\*kernel.ExtensionUploadResponse, error)
# BrowserPools
diff --git a/browserpool.go b/browserpool.go
index 3b65f0e..60dff73 100644
--- a/browserpool.go
+++ b/browserpool.go
@@ -198,7 +198,9 @@ type BrowserPoolBrowserPoolConfig struct {
ChromePolicy map[string]any `json:"chrome_policy"`
// List of browser extensions to load into the session. Provide each by id or name.
Extensions []shared.BrowserExtension `json:"extensions"`
- // Percentage of the pool to fill per minute. Defaults to 10%.
+ // Percentage of the pool to fill per minute. Defaults to 10. The cap is 25 for
+ // most organizations but can be raised per-organization, so only the lower bound
+ // is enforced here.
FillRatePerMinute int64 `json:"fill_rate_per_minute"`
// If true, launches the browser using a headless image. Defaults to false.
Headless bool `json:"headless"`
@@ -224,7 +226,7 @@ type BrowserPoolBrowserPoolConfig struct {
// mechanisms.
Stealth bool `json:"stealth"`
// Default idle timeout in seconds for browsers acquired from this pool before they
- // are destroyed. Defaults to 600 seconds if not specified
+ // are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours).
TimeoutSeconds int64 `json:"timeout_seconds"`
// Initial browser window size in pixels with optional refresh rate. If omitted,
// image defaults apply (1920x1080@25). For GPU images, the default is
@@ -370,7 +372,9 @@ type BrowserPoolNewParams struct {
// your organization's pooled sessions limit (the sum of all pool sizes cannot
// exceed your limit).
Size int64 `json:"size" api:"required"`
- // Percentage of the pool to fill per minute. Defaults to 10%.
+ // Percentage of the pool to fill per minute. Defaults to 10. The cap is 25 for
+ // most organizations but can be raised per-organization, so only the lower bound
+ // is enforced here.
FillRatePerMinute param.Opt[int64] `json:"fill_rate_per_minute,omitzero"`
// If true, launches the browser using a headless image. Defaults to false.
Headless param.Opt[bool] `json:"headless,omitzero"`
@@ -392,7 +396,7 @@ type BrowserPoolNewParams struct {
// mechanisms.
Stealth param.Opt[bool] `json:"stealth,omitzero"`
// Default idle timeout in seconds for browsers acquired from this pool before they
- // are destroyed. Defaults to 600 seconds if not specified
+ // are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours).
TimeoutSeconds param.Opt[int64] `json:"timeout_seconds,omitzero"`
// Custom Chrome enterprise policy overrides applied to all browsers in this pool.
// Keys are Chrome enterprise policy names; values must match their expected types.
@@ -433,7 +437,9 @@ type BrowserPoolUpdateParams struct {
// Whether to discard all idle browsers and rebuild the pool immediately. Defaults
// to false.
DiscardAllIdle param.Opt[bool] `json:"discard_all_idle,omitzero"`
- // Percentage of the pool to fill per minute. Defaults to 10%.
+ // Percentage of the pool to fill per minute. Defaults to 10. The cap is 25 for
+ // most organizations but can be raised per-organization, so only the lower bound
+ // is enforced here.
FillRatePerMinute param.Opt[int64] `json:"fill_rate_per_minute,omitzero"`
// If true, launches the browser using a headless image. Defaults to false.
Headless param.Opt[bool] `json:"headless,omitzero"`
@@ -459,7 +465,7 @@ type BrowserPoolUpdateParams struct {
// mechanisms.
Stealth param.Opt[bool] `json:"stealth,omitzero"`
// Default idle timeout in seconds for browsers acquired from this pool before they
- // are destroyed. Defaults to 600 seconds if not specified
+ // are destroyed. Defaults to 600 seconds. Minimum 10, maximum 259200 (72 hours).
TimeoutSeconds param.Opt[int64] `json:"timeout_seconds,omitzero"`
// Custom Chrome enterprise policy overrides applied to all browsers in this pool.
// Keys are Chrome enterprise policy names; values must match their expected types.
diff --git a/browserpool_test.go b/browserpool_test.go
index 4f12f7c..abdd8ec 100644
--- a/browserpool_test.go
+++ b/browserpool_test.go
@@ -48,7 +48,7 @@ func TestBrowserPoolNewWithOptionalParams(t *testing.T) {
ProxyID: kernel.String("proxy_id"),
StartURL: kernel.String("https://example.com"),
Stealth: kernel.Bool(true),
- TimeoutSeconds: kernel.Int(60),
+ TimeoutSeconds: kernel.Int(10),
Viewport: shared.BrowserViewportParam{
Height: 800,
Width: 1280,
@@ -125,7 +125,7 @@ func TestBrowserPoolUpdateWithOptionalParams(t *testing.T) {
Size: kernel.Int(10),
StartURL: kernel.String("https://example.com"),
Stealth: kernel.Bool(true),
- TimeoutSeconds: kernel.Int(60),
+ TimeoutSeconds: kernel.Int(10),
Viewport: shared.BrowserViewportParam{
Height: 800,
Width: 1280,
diff --git a/extension.go b/extension.go
index d34878a..9839b63 100644
--- a/extension.go
+++ b/extension.go
@@ -104,6 +104,19 @@ func (r *ExtensionService) DownloadFromChromeStore(ctx context.Context, query Ex
return res, err
}
+// Get an extension's metadata (name, size, timestamps) by ID or name, without
+// downloading the archive.
+func (r *ExtensionService) Get(ctx context.Context, idOrName string, opts ...option.RequestOption) (res *ExtensionGetResponse, err error) {
+ opts = slices.Concat(r.Options, opts)
+ if idOrName == "" {
+ err = errors.New("missing required id_or_name parameter")
+ return nil, err
+ }
+ path := fmt.Sprintf("extensions/%s/metadata", idOrName)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+ return res, err
+}
+
// Upload a zip file containing an unpacked browser extension. Optionally provide a
// unique name for later reference.
func (r *ExtensionService) Upload(ctx context.Context, body ExtensionUploadParams, opts ...option.RequestOption) (res *ExtensionUploadResponse, err error) {
@@ -144,6 +157,37 @@ func (r *ExtensionListResponse) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
+// A browser extension uploaded to Kernel.
+type ExtensionGetResponse struct {
+ // Unique identifier for the extension
+ ID string `json:"id" api:"required"`
+ // Timestamp when the extension was created
+ CreatedAt time.Time `json:"created_at" api:"required" format:"date-time"`
+ // Size of the extension archive in bytes
+ SizeBytes int64 `json:"size_bytes" api:"required"`
+ // Timestamp when the extension was last used
+ LastUsedAt time.Time `json:"last_used_at" api:"nullable" format:"date-time"`
+ // Optional, easier-to-reference name for the extension. Must be unique within the
+ // project.
+ Name string `json:"name" api:"nullable"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ ID respjson.Field
+ CreatedAt respjson.Field
+ SizeBytes respjson.Field
+ LastUsedAt respjson.Field
+ Name respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r ExtensionGetResponse) RawJSON() string { return r.JSON.raw }
+func (r *ExtensionGetResponse) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
// A browser extension uploaded to Kernel.
type ExtensionUploadResponse struct {
// Unique identifier for the extension
diff --git a/extension_test.go b/extension_test.go
index 8884e17..1944009 100644
--- a/extension_test.go
+++ b/extension_test.go
@@ -138,6 +138,29 @@ func TestExtensionDownloadFromChromeStoreWithOptionalParams(t *testing.T) {
}
}
+func TestExtensionGet(t *testing.T) {
+ t.Skip("Mock server tests are disabled")
+ baseURL := "http://localhost:4010"
+ if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
+ baseURL = envURL
+ }
+ if !testutil.CheckTestServer(t, baseURL) {
+ return
+ }
+ client := kernel.NewClient(
+ option.WithBaseURL(baseURL),
+ option.WithAPIKey("My API Key"),
+ )
+ _, err := client.Extensions.Get(context.TODO(), "id_or_name")
+ if err != nil {
+ var apierr *kernel.Error
+ if errors.As(err, &apierr) {
+ t.Log(string(apierr.DumpRequest(true)))
+ }
+ t.Fatalf("err should be nil: %s", err.Error())
+ }
+}
+
func TestExtensionUploadWithOptionalParams(t *testing.T) {
t.Skip("Mock server tests are disabled")
baseURL := "http://localhost:4010"
diff --git a/internal/version.go b/internal/version.go
index 0b83351..d62062c 100644
--- a/internal/version.go
+++ b/internal/version.go
@@ -2,4 +2,4 @@
package internal
-const PackageVersion = "0.69.0" // x-release-please-version
+const PackageVersion = "0.70.0" // x-release-please-version
diff --git a/shared/shared.go b/shared/shared.go
index 09471ec..366ff9e 100644
--- a/shared/shared.go
+++ b/shared/shared.go
@@ -168,12 +168,12 @@ func (r *BrowserProfileParam) UnmarshalJSON(data []byte) error {
// based on the resolution (higher resolutions use lower refresh rates to keep
// bandwidth reasonable).
type BrowserViewport struct {
- // Browser window height in pixels.
+ // Browser window height in pixels. Any positive integer is accepted.
Height int64 `json:"height" api:"required"`
- // Browser window width in pixels.
+ // Browser window width in pixels. Any positive integer is accepted.
Width int64 `json:"width" api:"required"`
- // Display refresh rate in Hz. If omitted, automatically determined from width and
- // height.
+ // Display refresh rate in Hz. Any positive integer is accepted; if omitted,
+ // automatically determined from width and height.
RefreshRate int64 `json:"refresh_rate"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
@@ -215,12 +215,12 @@ func (r BrowserViewport) ToParam() BrowserViewportParam {
//
// The properties Height, Width are required.
type BrowserViewportParam struct {
- // Browser window height in pixels.
+ // Browser window height in pixels. Any positive integer is accepted.
Height int64 `json:"height" api:"required"`
- // Browser window width in pixels.
+ // Browser window width in pixels. Any positive integer is accepted.
Width int64 `json:"width" api:"required"`
- // Display refresh rate in Hz. If omitted, automatically determined from width and
- // height.
+ // Display refresh rate in Hz. Any positive integer is accepted; if omitted,
+ // automatically determined from width and height.
RefreshRate param.Opt[int64] `json:"refresh_rate,omitzero"`
paramObj
}