diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2969a75..820b5c1 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.111.0" + ".": "0.112.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index e8ac4eb..ec91ee3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1 +1 @@ -configured_endpoints: 169 +configured_endpoints: 172 diff --git a/CHANGELOG.md b/CHANGELOG.md index c3ee939..4517336 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [0.112.0](https://github.com/kernel/kernel-go-sdk/compare/v0.111.0...v0.112.0) (2026-09-24) + + +### Features + +* Add profiles to browser pool acquire ([b50814f](https://github.com/kernel/kernel-go-sdk/commit/b50814fd0cd8cff8a37e56b48142eef0b3653b7c)) +* Correct monitor_disconnected description on computed state ([0584ea1](https://github.com/kernel/kernel-go-sdk/commit/0584ea1d2238bfb06f1995ef4cab305917941efa)) +* Expose Search rollout access in org entitlements ([8e055e1](https://github.com/kernel/kernel-go-sdk/commit/8e055e16e1db7872b2bb298901c760aba435030d)) +* Filter archived telemetry events by type ([a9ee57c](https://github.com/kernel/kernel-go-sdk/commit/a9ee57caab330fac08e26e24896274baf2e64b43)) +* Proxy custom WebMCP tools in the control plane ([47c1310](https://github.com/kernel/kernel-go-sdk/commit/47c13103242727ad8633062d359aee2ad7d6cb32)) +* Publish iframe and worker browser target types ([1ef7b7c](https://github.com/kernel/kernel-go-sdk/commit/1ef7b7c34dc3b987d6c515c82ea399c424f0c6e6)) +* Route browser destinations through selected proxies ([ffc38db](https://github.com/kernel/kernel-go-sdk/commit/ffc38db98feff227e904bd3c84e11cc715c010a8)) + ## [0.111.0](https://github.com/kernel/kernel-go-sdk/compare/v0.110.0...v0.111.0) (2026-09-22) diff --git a/README.md b/README.md index ea27be0..26ea6c9 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.111.0' +go get -u 'github.com/kernel/kernel-go-sdk@v0.112.0' ``` diff --git a/api.md b/api.md index aef4897..6c87a60 100644 --- a/api.md +++ b/api.md @@ -333,17 +333,37 @@ Params Types: Response Types: +- kernel.CustomToolSource - kernel.InvocationResult - kernel.Tool - kernel.ToolAnnotations - kernel.ToolFrame +- kernel.ToolMetadata - kernel.ToolSource - kernel.ToolsResponse Methods: - client.Browsers.Webmcp.InvokeTool(ctx context.Context, idOrName string, body kernel.BrowserWebmcpInvokeToolParams) (\*kernel.InvocationResult, error) -- client.Browsers.Webmcp.ListTools(ctx context.Context, idOrName string) (\*kernel.ToolsResponse, error) +- client.Browsers.Webmcp.ListTools(ctx context.Context, idOrName string, query kernel.BrowserWebmcpListToolsParams) (\*kernel.ToolsResponse, error) + +### CustomTools + +Params Types: + +- kernel.AddRequestParam + +Response Types: + +- kernel.CustomToolsResponse +- kernel.Definition +- kernel.Match + +Methods: + +- client.Browsers.Webmcp.CustomTools.List(ctx context.Context, idOrName string) (\*kernel.CustomToolsResponse, error) +- client.Browsers.Webmcp.CustomTools.Add(ctx context.Context, idOrName string, body kernel.BrowserWebmcpCustomToolAddParams) (\*kernel.CustomToolsResponse, error) +- client.Browsers.Webmcp.CustomTools.Remove(ctx context.Context, id string, body kernel.BrowserWebmcpCustomToolRemoveParams) error # Profiles diff --git a/browser.go b/browser.go index 341f0e6..0aff2de 100644 --- a/browser.go +++ b/browser.go @@ -237,9 +237,25 @@ type BrowserNetworkConfig struct { // 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. PrivateHosts []string `json:"private_hosts"` + // Per-destination proxy routes for a browser session. After setup, a destination + // hostname is matched against every route's hosts, regardless of port; route order + // does not matter. An exact hostname beats a wildcard, and a longer wildcard + // suffix beats a shorter one (for a.b.example.com: "a.b.example.com" > + // "_.b.example.com" > "_.example.com"). A host pattern may appear in only one + // route. "\*.example.com" matches subdomains only, not example.com. A matched + // request selects the route's proxy instead of the session's top-level proxy + // (including mode: direct); the route proxy's own bypass_hosts still apply. If the + // route proxy becomes unavailable, matched requests fail closed without falling + // back. Requests that match no route use the session's default egress from the + // top-level proxy field (or the browser default when proxy is omitted: stealth + // proxy or direct egress). Routes take effect once the session is created; + // start_url and other traffic during browser setup use the top-level proxy. + // Setting routes requires proxy v3. Not supported on browser pools. + ProxyRoutes []BrowserNetworkConfigProxyRoute `json:"proxy_routes"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { PrivateHosts respjson.Field + ProxyRoutes respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` @@ -260,6 +276,48 @@ func (r BrowserNetworkConfig) ToParam() BrowserNetworkConfigParam { return param.Override[BrowserNetworkConfigParam](json.RawMessage(r.RawJSON())) } +type BrowserNetworkConfigProxyRoute struct { + // Exact hostnames or leading \*. wildcard patterns (subdomains only); patterns + // cannot include ports, and matching ignores the destination port. Hosts not + // matched by any route use the session's top-level proxy (or the browser default + // when proxy is omitted). + Hosts []string `json:"hosts" api:"required"` + // Select an active non-direct proxy by ID or name. Responses always use ID. + Proxy BrowserNetworkConfigProxyRouteProxy `json:"proxy" api:"required"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Hosts respjson.Field + Proxy respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r BrowserNetworkConfigProxyRoute) RawJSON() string { return r.JSON.raw } +func (r *BrowserNetworkConfigProxyRoute) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Select an active non-direct proxy by ID or name. Responses always use ID. +type BrowserNetworkConfigProxyRouteProxy struct { + ID string `json:"id"` + Name string `json:"name"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + Name respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r BrowserNetworkConfigProxyRouteProxy) RawJSON() string { return r.JSON.raw } +func (r *BrowserNetworkConfigProxyRouteProxy) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + // Network configuration for a browser session or browser pool. type BrowserNetworkConfigParam struct { // Destinations the browser reaches directly through the session's own network @@ -285,6 +343,21 @@ type BrowserNetworkConfigParam struct { // 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. PrivateHosts []string `json:"private_hosts,omitzero"` + // Per-destination proxy routes for a browser session. After setup, a destination + // hostname is matched against every route's hosts, regardless of port; route order + // does not matter. An exact hostname beats a wildcard, and a longer wildcard + // suffix beats a shorter one (for a.b.example.com: "a.b.example.com" > + // "_.b.example.com" > "_.example.com"). A host pattern may appear in only one + // route. "\*.example.com" matches subdomains only, not example.com. A matched + // request selects the route's proxy instead of the session's top-level proxy + // (including mode: direct); the route proxy's own bypass_hosts still apply. If the + // route proxy becomes unavailable, matched requests fail closed without falling + // back. Requests that match no route use the session's default egress from the + // top-level proxy field (or the browser default when proxy is omitted: stealth + // proxy or direct egress). Routes take effect once the session is created; + // start_url and other traffic during browser setup use the top-level proxy. + // Setting routes requires proxy v3. Not supported on browser pools. + ProxyRoutes []BrowserNetworkConfigProxyRouteParam `json:"proxy_routes,omitzero"` paramObj } @@ -296,6 +369,41 @@ func (r *BrowserNetworkConfigParam) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +// The properties Hosts, Proxy are required. +type BrowserNetworkConfigProxyRouteParam struct { + // Exact hostnames or leading \*. wildcard patterns (subdomains only); patterns + // cannot include ports, and matching ignores the destination port. Hosts not + // matched by any route use the session's top-level proxy (or the browser default + // when proxy is omitted). + Hosts []string `json:"hosts,omitzero" api:"required"` + // Select an active non-direct proxy by ID or name. Responses always use ID. + Proxy BrowserNetworkConfigProxyRouteProxyParam `json:"proxy,omitzero" api:"required"` + paramObj +} + +func (r BrowserNetworkConfigProxyRouteParam) MarshalJSON() (data []byte, err error) { + type shadow BrowserNetworkConfigProxyRouteParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *BrowserNetworkConfigProxyRouteParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Select an active non-direct proxy by ID or name. Responses always use ID. +type BrowserNetworkConfigProxyRouteProxyParam struct { + ID param.Opt[string] `json:"id,omitzero"` + Name param.Opt[string] `json:"name,omitzero"` + paramObj +} + +func (r BrowserNetworkConfigProxyRouteProxyParam) MarshalJSON() (data []byte, err error) { + type shadow BrowserNetworkConfigProxyRouteProxyParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *BrowserNetworkConfigProxyRouteProxyParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + // Browser pool this session was acquired from, if any. type BrowserPoolRef struct { // Browser pool ID diff --git a/browser_test.go b/browser_test.go index f69f671..a826fcd 100644 --- a/browser_test.go +++ b/browser_test.go @@ -45,6 +45,13 @@ func TestBrowserNewWithOptionalParams(t *testing.T) { Name: kernel.String("checkout-flow-1"), Network: kernel.BrowserNetworkConfigParam{ PrivateHosts: []string{"*.example.ts.net", "100.64.0.0/10"}, + ProxyRoutes: []kernel.BrowserNetworkConfigProxyRouteParam{{ + Hosts: []string{"string"}, + Proxy: kernel.BrowserNetworkConfigProxyRouteProxyParam{ + ID: kernel.String("x"), + Name: kernel.String("x"), + }, + }}, }, Profile: shared.BrowserProfileParam{ ID: kernel.String("id"), diff --git a/browserpool.go b/browserpool.go index e3352dd..88212f1 100644 --- a/browserpool.go +++ b/browserpool.go @@ -994,6 +994,10 @@ type BrowserPoolAcquireParams struct { // is reset to the pool's baseline, so the override does not carry over to the next // lease. Telemetry BrowserPoolAcquireParamsTelemetry `json:"telemetry,omitzero"` + // 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. + Profile shared.BrowserProfileParam `json:"profile,omitzero"` // Optional user-defined key-value tags for the acquired browser session, used to // find and group sessions later. Applies to this lease only and are cleared when // the browser is released back to the pool. Up to 50 pairs. @@ -1112,7 +1116,8 @@ type BrowserPoolReleaseParams struct { // Defaults to true. A reused browser keeps the configuration it was created with, // so it does not pick up pool configuration changes made while it was in use. // Release with `reuse: false`, or flush the pool afterward, to rebuild it with the - // current configuration. + // current configuration. Browsers loaded with an acquire-time profile are always + // destroyed and replaced, even when reuse is true. Reuse param.Opt[bool] `json:"reuse,omitzero"` paramObj } diff --git a/browserpool_test.go b/browserpool_test.go index 1be6e77..96ba15f 100644 --- a/browserpool_test.go +++ b/browserpool_test.go @@ -42,6 +42,13 @@ func TestBrowserPoolNewWithOptionalParams(t *testing.T) { Name: kernel.String("my-pool"), Network: kernel.BrowserNetworkConfigParam{ PrivateHosts: []string{"*.example.ts.net", "100.64.0.0/10"}, + ProxyRoutes: []kernel.BrowserNetworkConfigProxyRouteParam{{ + Hosts: []string{"string"}, + Proxy: kernel.BrowserNetworkConfigProxyRouteProxyParam{ + ID: kernel.String("x"), + Name: kernel.String("x"), + }, + }}, }, Profile: kernel.BrowserPoolNewParamsProfile{ ID: kernel.String("id"), @@ -169,6 +176,13 @@ func TestBrowserPoolUpdateWithOptionalParams(t *testing.T) { Name: kernel.String("my-pool"), Network: kernel.BrowserNetworkConfigParam{ PrivateHosts: []string{"*.example.ts.net", "100.64.0.0/10"}, + ProxyRoutes: []kernel.BrowserNetworkConfigProxyRouteParam{{ + Hosts: []string{"string"}, + Proxy: kernel.BrowserNetworkConfigProxyRouteProxyParam{ + ID: kernel.String("x"), + Name: kernel.String("x"), + }, + }}, }, Profile: kernel.BrowserPoolUpdateParamsProfile{ ID: kernel.String("id"), @@ -320,7 +334,12 @@ func TestBrowserPoolAcquireWithOptionalParams(t *testing.T) { kernel.BrowserPoolAcquireParams{ AcquireTimeoutSeconds: kernel.Int(0), Name: kernel.String("checkout-flow-1"), - StartURL: kernel.String("https://example.com"), + Profile: shared.BrowserProfileParam{ + ID: kernel.String("id"), + Name: kernel.String("name"), + SaveChanges: kernel.Bool(true), + }, + StartURL: kernel.String("https://example.com"), Tags: kernel.Tags{ "team": "backend", "env": "staging", diff --git a/browsertelemetry.go b/browsertelemetry.go index 5e53597..54d9cce 100644 --- a/browsertelemetry.go +++ b/browsertelemetry.go @@ -46,8 +46,9 @@ func NewBrowserTelemetryService(opts ...option.RequestOption) (r BrowserTelemetr // Reads a page of telemetry events for the browser session. To page through // results, pass the X-Next-Offset value from the previous response as offset and -// repeat while X-Has-More is true. Returns an empty list when telemetry data is -// unavailable. +// repeat while X-Has-More is true. The category and type filters apply within each +// page, so a filtered page may be empty while X-Has-More is true. Returns an empty +// list when telemetry data is unavailable. func (r *BrowserTelemetryService) Events(ctx context.Context, idOrName string, query BrowserTelemetryEventsParams, opts ...option.RequestOption) (res *pagination.OffsetPagination[BrowserTelemetryEventsResponse], err error) { var raw *http.Response opts = slices.Concat(r.Options, opts) @@ -71,8 +72,9 @@ func (r *BrowserTelemetryService) Events(ctx context.Context, idOrName string, q // Reads a page of telemetry events for the browser session. To page through // results, pass the X-Next-Offset value from the previous response as offset and -// repeat while X-Has-More is true. Returns an empty list when telemetry data is -// unavailable. +// repeat while X-Has-More is true. The category and type filters apply within each +// page, so a filtered page may be empty while X-Has-More is true. Returns an empty +// list when telemetry data is unavailable. func (r *BrowserTelemetryService) EventsAutoPaging(ctx context.Context, idOrName string, query BrowserTelemetryEventsParams, opts ...option.RequestOption) *pagination.OffsetPaginationAutoPager[BrowserTelemetryEventsResponse] { return pagination.NewOffsetPaginationAutoPager(r.Events(ctx, idOrName, query, opts...)) } @@ -3572,7 +3574,8 @@ type BrowserEventContext struct { TargetID string `json:"target_id"` // CDP target type of the page that produced the event. // - // Any of "page", "background_page", "service_worker", "shared_worker", "other". + // Any of "page", "iframe", "worker", "background_page", "service_worker", + // "shared_worker", "other". TargetType BrowserEventContextTargetType `json:"target_type"` // URL relevant to this event — page URL for navigation and page events, request // URL for network events. @@ -3602,6 +3605,8 @@ type BrowserEventContextTargetType string const ( BrowserEventContextTargetTypePage BrowserEventContextTargetType = "page" + BrowserEventContextTargetTypeIframe BrowserEventContextTargetType = "iframe" + BrowserEventContextTargetTypeWorker BrowserEventContextTargetType = "worker" BrowserEventContextTargetTypeBackgroundPage BrowserEventContextTargetType = "background_page" BrowserEventContextTargetTypeServiceWorker BrowserEventContextTargetType = "service_worker" BrowserEventContextTargetTypeSharedWorker BrowserEventContextTargetType = "shared_worker" @@ -3945,8 +3950,12 @@ func (r *BrowserLiveViewDisconnectEventData) UnmarshalJSON(data []byte) error { } // The CDP connection to Chrome was lost. Telemetry events may be dropped until -// monitor_reconnected arrives. Treat any in-progress computed state (network_idle, -// page_layout_settled) as unreliable until then. +// monitor_reconnected arrives. In-progress computed state is discarded rather than +// paused, so computed events still pending for the current navigation +// (network_idle, page_layout_settled, page_navigation_settled) never fire. +// monitor_reconnected does not restore them. After reattachment a fresh state +// machine starts, so computed events can resume before the next navigation and +// carry empty navigation context until one occurs. type BrowserMonitorDisconnectedEvent struct { Category constant.Monitor `json:"category" default:"monitor"` // Provenance metadata identifying which producer emitted the event. @@ -4488,7 +4497,8 @@ type BrowserPageCrashedEventData struct { TargetID string `json:"target_id" api:"required"` // CDP target type of the page that produced the event. // - // Any of "page", "background_page", "service_worker", "shared_worker", "other". + // Any of "page", "iframe", "worker", "background_page", "service_worker", + // "shared_worker", "other". TargetType string `json:"target_type" api:"required"` // URL the page was on when its renderer process crashed. URL string `json:"url" api:"required"` @@ -4880,7 +4890,8 @@ type BrowserPageNavigationEventData struct { TargetID string `json:"target_id"` // CDP target type of the page that produced the event. // - // Any of "page", "background_page", "service_worker", "shared_worker", "other". + // Any of "page", "iframe", "worker", "background_page", "service_worker", + // "shared_worker", "other". TargetType string `json:"target_type"` // URL navigated to. URL string `json:"url"` @@ -4980,7 +4991,8 @@ type BrowserPageTabOpenedEventData struct { TargetID string `json:"target_id"` // CDP target type of the page that produced the event. // - // Any of "page", "background_page", "service_worker", "shared_worker", "other". + // Any of "page", "iframe", "worker", "background_page", "service_worker", + // "shared_worker", "other". TargetType string `json:"target_type"` // Initial page title of the new tab. Title string `json:"title"` @@ -5109,17 +5121,18 @@ func (r *BrowserProxyErrorEvent) UnmarshalJSON(data []byte) error { type BrowserProxyErrorEventData struct { // Proxy-layer error code: the X-Kernel-Proxy-Error response header value from a // branded 5xx error page served by the metro egress host-proxy. Values mirror what - // the proxy emits: destination_blocked, provider_blacklisted, - // provider_unreachable, provider_rejected, origin_tls_timeout, - // origin_response_incomplete, proxy_unavailable, restricted_route_unavailable, - // upstream_timeout, upstream_dns_failure, upstream_connect_failed. A header value - // the browser image does not recognize is reported as unknown, with the header - // value in raw_code. + // the proxy emits: destination_blocked, destination_route_unavailable, + // provider_blacklisted, provider_unreachable, provider_rejected, + // origin_tls_timeout, origin_response_incomplete, proxy_unavailable, + // restricted_route_unavailable, upstream_timeout, upstream_dns_failure, + // upstream_connect_failed. A header value the browser image does not recognize is + // reported as unknown, with the header value in raw_code. // - // Any of "destination_blocked", "provider_blacklisted", "provider_unreachable", - // "provider_rejected", "origin_tls_timeout", "origin_response_incomplete", - // "proxy_unavailable", "restricted_route_unavailable", "upstream_timeout", - // "upstream_dns_failure", "upstream_connect_failed", "unknown". + // Any of "destination_blocked", "destination_route_unavailable", + // "provider_blacklisted", "provider_unreachable", "provider_rejected", + // "origin_tls_timeout", "origin_response_incomplete", "proxy_unavailable", + // "restricted_route_unavailable", "upstream_timeout", "upstream_dns_failure", + // "upstream_connect_failed", "unknown". Code string `json:"code" api:"required"` // CDP request identifier matching the originating request. RequestID string `json:"request_id" api:"required"` @@ -6811,9 +6824,7 @@ type BrowserTelemetryEventsParams struct { // Read direction. asc (default) reads oldest first, starting from since or the // offset cursor. desc reads newest first: each request returns one page of up to // limit records ending at the offset cursor (or until, or the newest archived - // event); combining desc with since is rejected with a 400. In either direction - // the category filter applies within the page, so a filtered page may be empty - // while X-Has-More is true. + // event); combining desc with since is rejected with a 400. Order param.Opt[string] `query:"order,omitzero" json:"-"` // Start of the window: an RFC-3339 timestamp, or a duration like 5m meaning that // long ago. Defaults to 5m. Ignored when offset is set. @@ -6827,6 +6838,10 @@ type BrowserTelemetryEventsParams struct { // Any of "console", "network", "page", "interaction", "control", "platform", // "connection", "system", "screenshot", "captcha", "monitor". Category []string `query:"category,omitzero" json:"-"` + // Restrict results to these event types, such as page_crashed or + // captcha_challenge_result. Repeat the parameter for multiple values. Combines + // with category: when both are set an event must match both. + Type []string `query:"type,omitzero" json:"-"` paramObj } diff --git a/browsertelemetry_test.go b/browsertelemetry_test.go index 1593b38..15c4797 100644 --- a/browsertelemetry_test.go +++ b/browsertelemetry_test.go @@ -35,6 +35,7 @@ func TestBrowserTelemetryEventsWithOptionalParams(t *testing.T) { Offset: kernel.Int(0), Order: kernel.String("order"), Since: kernel.String("since"), + Type: []string{"string"}, Until: kernel.String("until"), }, ) diff --git a/browserwebmcp.go b/browserwebmcp.go index d1507d8..5cbdad1 100644 --- a/browserwebmcp.go +++ b/browserwebmcp.go @@ -7,9 +7,11 @@ import ( "errors" "fmt" "net/http" + "net/url" "slices" "github.com/kernel/kernel-go-sdk/internal/apijson" + "github.com/kernel/kernel-go-sdk/internal/apiquery" shimjson "github.com/kernel/kernel-go-sdk/internal/encoding/json" "github.com/kernel/kernel-go-sdk/internal/requestconfig" "github.com/kernel/kernel-go-sdk/option" @@ -27,6 +29,8 @@ import ( // the [NewBrowserWebmcpService] method instead. type BrowserWebmcpService struct { Options []option.RequestOption + // Discover and invoke native page tools across the browser instance. + CustomTools BrowserWebmcpCustomToolService } // NewBrowserWebmcpService generates a new service that applies the given options @@ -35,6 +39,7 @@ type BrowserWebmcpService struct { func NewBrowserWebmcpService(opts ...option.RequestOption) (r BrowserWebmcpService) { r = BrowserWebmcpService{} r.Options = opts + r.CustomTools = NewBrowserWebmcpCustomToolService(opts...) return } @@ -45,7 +50,8 @@ func NewBrowserWebmcpService(opts ...option.RequestOption) (r BrowserWebmcpServi // then submit through Playwright or computer interaction without invoking the tool // again. If the tab or embedded frame disappears, or the request times out after // invocation begins, the response reports outcome_unknown and the tool is not -// retried. +// retried. CDP-backed custom tool outputs above 240 KiB return an error rather +// than a truncated result. func (r *BrowserWebmcpService) InvokeTool(ctx context.Context, idOrName string, body BrowserWebmcpInvokeToolParams, opts ...option.RequestOption) (res *InvocationResult, err error) { opts = slices.Concat(r.Options, opts) if idOrName == "" { @@ -57,21 +63,41 @@ func (r *BrowserWebmcpService) InvokeTool(ctx context.Context, idOrName string, return res, err } -// Returns a snapshot of native WebMCP tools available across every open tab and -// embedded frame in the browser. Each tool includes an opaque tool_ref for -// invoking that exact live registration. Tools disappear when their document -// closes or navigates away. -func (r *BrowserWebmcpService) ListTools(ctx context.Context, idOrName string, opts ...option.RequestOption) (res *ToolsResponse, err error) { +// Returns a snapshot of native and custom WebMCP tools available across every open +// tab and embedded frame in the browser. Each tool includes an opaque tool_ref for +// invoking that exact live registration, nested tool metadata, and source +// information. Custom tools include their generated ID, namespace, and CDP +// target_id in source. Tools disappear when their document closes or navigates +// away. Use exclude_custom to return only page-provided tools. +func (r *BrowserWebmcpService) ListTools(ctx context.Context, idOrName string, query BrowserWebmcpListToolsParams, opts ...option.RequestOption) (res *ToolsResponse, 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("browsers/%s/webmcp/tools", idOrName) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) return res, err } +type CustomToolSource struct { + ID string `json:"id" api:"required"` + Namespace string `json:"namespace" api:"required"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + Namespace respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r CustomToolSource) RawJSON() string { return r.JSON.raw } +func (r *CustomToolSource) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + type InvocationResult struct { InvocationID string `json:"invocation_id" api:"required"` // awaiting_submission means a non-autosubmit declarative form was populated but @@ -133,24 +159,21 @@ func (r *InvokeRequestParam) UnmarshalJSON(data []byte) error { } type Tool struct { - Description string `json:"description" api:"required"` - InputSchema map[string]any `json:"input_schema" api:"required"` - Name string `json:"name" api:"required"` - Source ToolSource `json:"source" api:"required"` + Source ToolSource `json:"source" api:"required"` + // Tool metadata follows the + // [MCP Tool definition](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool) + // and the + // [WebMCP RegisteredTool definition](https://webmachinelearning.github.io/webmcp/#dictdef-registeredtool). + // outputSchema is optional for page and custom tools. + Tool ToolMetadata `json:"tool" api:"required"` // Opaque reference for invoking this exact live registration. It becomes invalid // when its document or browser process is replaced. ToolRef string `json:"tool_ref" api:"required"` - // Page-provided behavioral hints. These values are untrusted and are not enforced - // by Kernel. - Annotations ToolAnnotations `json:"annotations"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { - Description respjson.Field - InputSchema respjson.Field - Name respjson.Field Source respjson.Field + Tool respjson.Field ToolRef respjson.Field - Annotations respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` @@ -162,21 +185,30 @@ func (r *Tool) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } -// Page-provided behavioral hints. These values are untrusted and are not enforced -// by Kernel. +// Tool-provided behavioral hints from the +// [MCP tool specification](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool) +// and the +// [WebMCP ToolAnnotations definition](https://webmachinelearning.github.io/webmcp/#dictdef-toolannotations). +// These hints are untrusted and are not enforced by Kernel. type ToolAnnotations struct { - Autosubmit bool `json:"autosubmit" api:"required"` - Consequential bool `json:"consequential" api:"required"` - ReadOnly bool `json:"read_only" api:"required"` - UntrustedContent bool `json:"untrusted_content" api:"required"` + Autosubmit bool `json:"autosubmit"` + ConsequentialHint bool `json:"consequentialHint"` + DestructiveHint bool `json:"destructiveHint"` + IdempotentHint bool `json:"idempotentHint"` + OpenWorldHint bool `json:"openWorldHint"` + ReadOnlyHint bool `json:"readOnlyHint"` + UntrustedContentHint bool `json:"untrustedContentHint"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { - Autosubmit respjson.Field - Consequential respjson.Field - ReadOnly respjson.Field - UntrustedContent respjson.Field - ExtraFields map[string]respjson.Field - raw string + Autosubmit respjson.Field + ConsequentialHint respjson.Field + DestructiveHint respjson.Field + IdempotentHint respjson.Field + OpenWorldHint respjson.Field + ReadOnlyHint respjson.Field + UntrustedContentHint respjson.Field + ExtraFields map[string]respjson.Field + raw string } `json:"-"` } @@ -207,6 +239,42 @@ func (r *ToolFrame) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +// Tool metadata follows the +// [MCP Tool definition](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool) +// and the +// [WebMCP RegisteredTool definition](https://webmachinelearning.github.io/webmcp/#dictdef-registeredtool). +// outputSchema is optional for page and custom tools. +type ToolMetadata struct { + Description string `json:"description" api:"required"` + InputSchema map[string]any `json:"inputSchema" api:"required"` + Name string `json:"name" api:"required"` + // Tool-provided behavioral hints from the + // [MCP tool specification](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool) + // and the + // [WebMCP ToolAnnotations definition](https://webmachinelearning.github.io/webmcp/#dictdef-toolannotations). + // These hints are untrusted and are not enforced by Kernel. + Annotations ToolAnnotations `json:"annotations"` + OutputSchema map[string]any `json:"outputSchema"` + Title string `json:"title"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Description respjson.Field + InputSchema respjson.Field + Name respjson.Field + Annotations respjson.Field + OutputSchema respjson.Field + Title respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ToolMetadata) RawJSON() string { return r.JSON.raw } +func (r *ToolMetadata) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + type ToolSource struct { // Embedded frame that registered the tool, or null when the top-level page // registered it. @@ -220,7 +288,11 @@ type ToolSource struct { TabID int64 `json:"tab_id" api:"required"` // Monotonically increasing identifier for the browser window during the current // browser process. - WindowID int64 `json:"window_id" api:"required"` + WindowID int64 `json:"window_id" api:"required"` + Custom CustomToolSource `json:"custom"` + // CDP target ID for a custom tool's registration tab; omitted for page-provided + // tools. + TargetID string `json:"target_id"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { Frame respjson.Field @@ -228,6 +300,8 @@ type ToolSource struct { PageURL respjson.Field TabID respjson.Field WindowID respjson.Field + Custom respjson.Field + TargetID respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` @@ -266,3 +340,18 @@ func (r BrowserWebmcpInvokeToolParams) MarshalJSON() (data []byte, err error) { func (r *BrowserWebmcpInvokeToolParams) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } + +type BrowserWebmcpListToolsParams struct { + // Exclude custom tools when true. + ExcludeCustom param.Opt[bool] `query:"exclude_custom,omitzero" json:"-"` + paramObj +} + +// URLQuery serializes [BrowserWebmcpListToolsParams]'s query parameters as +// `url.Values`. +func (r BrowserWebmcpListToolsParams) URLQuery() (v url.Values, err error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} diff --git a/browserwebmcp_test.go b/browserwebmcp_test.go index da4fad1..d57f3f2 100644 --- a/browserwebmcp_test.go +++ b/browserwebmcp_test.go @@ -48,7 +48,7 @@ func TestBrowserWebmcpInvokeToolWithOptionalParams(t *testing.T) { } } -func TestBrowserWebmcpListTools(t *testing.T) { +func TestBrowserWebmcpListToolsWithOptionalParams(t *testing.T) { t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { @@ -61,7 +61,13 @@ func TestBrowserWebmcpListTools(t *testing.T) { option.WithBaseURL(baseURL), option.WithAPIKey("My API Key"), ) - _, err := client.Browsers.Webmcp.ListTools(context.TODO(), "htzv5orfit78e1m2biiifpbv") + _, err := client.Browsers.Webmcp.ListTools( + context.TODO(), + "htzv5orfit78e1m2biiifpbv", + kernel.BrowserWebmcpListToolsParams{ + ExcludeCustom: kernel.Bool(true), + }, + ) if err != nil { var apierr *kernel.Error if errors.As(err, &apierr) { diff --git a/browserwebmcpcustomtool.go b/browserwebmcpcustomtool.go new file mode 100644 index 0000000..ceafcd4 --- /dev/null +++ b/browserwebmcpcustomtool.go @@ -0,0 +1,193 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package kernel + +import ( + "context" + "errors" + "fmt" + "net/http" + "slices" + + "github.com/kernel/kernel-go-sdk/internal/apijson" + shimjson "github.com/kernel/kernel-go-sdk/internal/encoding/json" + "github.com/kernel/kernel-go-sdk/internal/requestconfig" + "github.com/kernel/kernel-go-sdk/option" + "github.com/kernel/kernel-go-sdk/packages/param" + "github.com/kernel/kernel-go-sdk/packages/respjson" +) + +// Discover and invoke native page tools across the browser instance. +// +// BrowserWebmcpCustomToolService contains methods and other services that help +// with interacting with the kernel API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewBrowserWebmcpCustomToolService] method instead. +type BrowserWebmcpCustomToolService struct { + Options []option.RequestOption +} + +// NewBrowserWebmcpCustomToolService generates a new service that applies the given +// options to each request. These options are applied after the parent client's +// options (if there is one), and before any request-specific options. +func NewBrowserWebmcpCustomToolService(opts ...option.RequestOption) (r BrowserWebmcpCustomToolService) { + r = BrowserWebmcpCustomToolService{} + r.Options = opts + return +} + +// Returns every registered custom tool with its generated ID, namespace, matcher, +// and MCP tool metadata. +func (r *BrowserWebmcpCustomToolService) List(ctx context.Context, idOrName string, opts ...option.RequestOption) (res *CustomToolsResponse, 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("browsers/%s/webmcp/custom-tools", idOrName) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return res, err +} + +// Add a namespaced batch of custom tools. A custom tool can be page-backed or +// CDP-backed. Page-backed tools execute in the page via JavaScript. CDP-backed +// tools execute via CDP and can use all browser REPL tools (see `/repl`). The +// source must evaluate to a non-empty array of definitions with URL matchers, tool +// metadata (including an optional output schema), and execute functions. The batch +// is added atomically. Matchers apply to top-level documents and nested frames, +// including out-of-process iframes; each matching tool is exposed once on the +// tab's top-level document and appears in the WebMCP tool snapshot. +// +// To update one tool, list the tools, delete its ID, and add its replacement. Set +// force_overwrite_namespace to replace every existing tool in the namespace +// atomically; omitted or false adds tools without replacing existing ones. +// Existing invocations continue. +func (r *BrowserWebmcpCustomToolService) Add(ctx context.Context, idOrName string, body BrowserWebmcpCustomToolAddParams, opts ...option.RequestOption) (res *CustomToolsResponse, 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("browsers/%s/webmcp/custom-tools", idOrName) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return res, err +} + +// Removes one custom tool by generated ID. An invocation already in progress is +// not canceled. +func (r *BrowserWebmcpCustomToolService) Remove(ctx context.Context, id string, body BrowserWebmcpCustomToolRemoveParams, opts ...option.RequestOption) (err error) { + opts = slices.Concat(r.Options, opts) + opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...) + if body.IDOrName == "" { + err = errors.New("missing required id_or_name parameter") + return err + } + if id == "" { + err = errors.New("missing required id parameter") + return err + } + path := fmt.Sprintf("browsers/%s/webmcp/custom-tools/%s", body.IDOrName, id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...) + return err +} + +// The properties Namespace, Source are required. +type AddRequestParam struct { + Namespace string `json:"namespace" api:"required"` + // JavaScript expression that evaluates to a non-empty array of custom tool + // definitions. Limited to 8,000,000 bytes when UTF-8 encoded, so multi-byte + // characters reduce the allowed character count. + Source string `json:"source" api:"required"` + // Atomically replace all existing tools in this namespace with this batch when + // true. + ForceOverwriteNamespace param.Opt[bool] `json:"force_overwrite_namespace,omitzero"` + paramObj +} + +func (r AddRequestParam) MarshalJSON() (data []byte, err error) { + type shadow AddRequestParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *AddRequestParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type CustomToolsResponse struct { + Tools []Definition `json:"tools" api:"required"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Tools respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r CustomToolsResponse) RawJSON() string { return r.JSON.raw } +func (r *CustomToolsResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type Definition struct { + ID string `json:"id" api:"required"` + Kind string `json:"kind" api:"required"` + Match Match `json:"match" api:"required"` + Namespace string `json:"namespace" api:"required"` + // Tool metadata follows the + // [MCP Tool definition](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool) + // and the + // [WebMCP RegisteredTool definition](https://webmachinelearning.github.io/webmcp/#dictdef-registeredtool). + // outputSchema is optional for page and custom tools. + Tool ToolMetadata `json:"tool" api:"required"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + Kind respjson.Field + Match respjson.Field + Namespace respjson.Field + Tool respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r Definition) RawJSON() string { return r.JSON.raw } +func (r *Definition) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type Match struct { + URLPatterns []string `json:"url_patterns" api:"required"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + URLPatterns respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r Match) RawJSON() string { return r.JSON.raw } +func (r *Match) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type BrowserWebmcpCustomToolAddParams struct { + AddRequest AddRequestParam + paramObj +} + +func (r BrowserWebmcpCustomToolAddParams) MarshalJSON() (data []byte, err error) { + return shimjson.Marshal(r.AddRequest) +} +func (r *BrowserWebmcpCustomToolAddParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type BrowserWebmcpCustomToolRemoveParams struct { + IDOrName string `path:"id_or_name" api:"required" json:"-"` + paramObj +} diff --git a/browserwebmcpcustomtool_test.go b/browserwebmcpcustomtool_test.go new file mode 100644 index 0000000..239a7b2 --- /dev/null +++ b/browserwebmcpcustomtool_test.go @@ -0,0 +1,99 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package kernel_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/kernel/kernel-go-sdk" + "github.com/kernel/kernel-go-sdk/internal/testutil" + "github.com/kernel/kernel-go-sdk/option" +) + +func TestBrowserWebmcpCustomToolList(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.Browsers.Webmcp.CustomTools.List(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 TestBrowserWebmcpCustomToolAddWithOptionalParams(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.Browsers.Webmcp.CustomTools.Add( + context.TODO(), + "id_or_name", + kernel.BrowserWebmcpCustomToolAddParams{ + AddRequest: kernel.AddRequestParam{ + Namespace: "namespace", + Source: "source", + ForceOverwriteNamespace: kernel.Bool(true), + }, + }, + ) + 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 TestBrowserWebmcpCustomToolRemove(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.Browsers.Webmcp.CustomTools.Remove( + context.TODO(), + "ct_n10b9798ad53ecc4y69z31e1", + kernel.BrowserWebmcpCustomToolRemoveParams{ + IDOrName: "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()) + } +} diff --git a/internal/version.go b/internal/version.go index d1b8bd7..44c772b 100644 --- a/internal/version.go +++ b/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.111.0" // x-release-please-version +const PackageVersion = "0.112.0" // x-release-please-version diff --git a/organizationentitlement.go b/organizationentitlement.go index 1f41056..201ba62 100644 --- a/organizationentitlement.go +++ b/organizationentitlement.go @@ -82,6 +82,9 @@ type OrgEntitlementsFeatures struct { ManagedProxies OrgEntitlementsFeaturesManagedProxies `json:"managed_proxies" api:"required"` Profiles OrgEntitlementsFeaturesProfiles `json:"profiles" api:"required"` ProxyBypassHosts OrgEntitlementsFeaturesProxyBypassHosts `json:"proxy_bypass_hosts" api:"required"` + // Whether the Search API is enabled for the organization by its rollout feature + // flag. + Search OrgEntitlementsFeaturesSearch `json:"search" api:"required"` // Whether the organization can access vaults, using the same access check as vault // API routes. Vaults OrgEntitlementsFeaturesVaults `json:"vaults" api:"required"` @@ -99,6 +102,7 @@ type OrgEntitlementsFeatures struct { ManagedProxies respjson.Field Profiles respjson.Field ProxyBypassHosts respjson.Field + Search respjson.Field Vaults respjson.Field ExtraFields map[string]respjson.Field raw string @@ -336,6 +340,25 @@ func (r *OrgEntitlementsFeaturesProxyBypassHosts) UnmarshalJSON(data []byte) err return apijson.UnmarshalRoot(data, r) } +// Whether the Search API is enabled for the organization by its rollout feature +// flag. +type OrgEntitlementsFeaturesSearch struct { + // Whether the organization is entitled to use this feature. + Enabled bool `json:"enabled" api:"required"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Enabled respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r OrgEntitlementsFeaturesSearch) RawJSON() string { return r.JSON.raw } +func (r *OrgEntitlementsFeaturesSearch) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + // Whether the organization can access vaults, using the same access check as vault // API routes. type OrgEntitlementsFeaturesVaults struct {