Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ef994b4
feat: add browser-scoped session client
rgarcia Apr 13, 2026
64f7f81
fix: align browser-scoped routing with base_url
rgarcia Apr 13, 2026
3e3e33f
fix: tighten browser-scoped helper surface
rgarcia Apr 13, 2026
0ac61ef
refactor: narrow browser-scoped helper exports
rgarcia Apr 13, 2026
b6a77bc
feat: generate browser-scoped service bindings
rgarcia Apr 13, 2026
92dc96e
docs: add browser-scoped raw http example
rgarcia Apr 21, 2026
3452e53
refactor: remove browser session wrapper layer
rgarcia Apr 22, 2026
6bdf25f
refactor: simplify direct-to-vm route caching
rgarcia Apr 22, 2026
909c377
refactor: rename browser routing subresources config
rgarcia Apr 22, 2026
77bda33
fix: clean up go browser routing follow-ups
rgarcia Apr 22, 2026
d594f39
fix: remove old go browser scope package
rgarcia Apr 22, 2026
c0430e3
chore(internal): more robust bootstrap script
stainless-app[bot] Apr 23, 2026
681e57f
refactor: move go browser routing rollout to env
rgarcia Apr 24, 2026
1ec1358
fix: propagate browser HTTP client config errors
rgarcia Apr 24, 2026
e23defc
feat: Expose browser_session_id on managed auth connection
stainless-app[bot] Apr 24, 2026
62078d3
refactor: move browser route cache sync into middleware
rgarcia Apr 24, 2026
b293866
fix: tighten browser route metadata parsing
rgarcia Apr 24, 2026
0e44ff3
fix: make browser route deletion win over sniffing
rgarcia Apr 24, 2026
4f754d1
fix: keep browser pool routing in sync with cache
rgarcia Apr 24, 2026
a13c8f6
Merge pull request #95 from kernel/raf/browser-scoped-client
rgarcia Apr 24, 2026
01bdd6a
release: 0.51.0
stainless-app[bot] Apr 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.50.0"
".": "0.51.0"
}
4 changes: 2 additions & 2 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 112
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-686a9addd4f9356ca26ff3ff04e1a11466d77a412859829075566394922b715d.yml
openapi_spec_hash: 7a9e9c2023400d44bcbfb87b7ec07708
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-e14974fd90680e5745b35d8718a1ccce2181f6d17a6e0a1fd35fc5bca88795ae.yml
openapi_spec_hash: 1b3aa75f0ab48b122d514047f9c82873
config_hash: 08d55086449943a8fec212b870061a3f
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,47 @@
# Changelog

## 0.51.0 (2026-04-24)

Full Changelog: [v0.50.0...v0.51.0](https://github.com/kernel/kernel-go-sdk/compare/v0.50.0...v0.51.0)

### Features

* add browser-scoped session client ([ef994b4](https://github.com/kernel/kernel-go-sdk/commit/ef994b4d6189ee0cdb3ad6e002816e0d77c7db48))
* Expose browser_session_id on managed auth connection ([e23defc](https://github.com/kernel/kernel-go-sdk/commit/e23defc4abad6e49ae854ead1d1a015a2ecad097))
* generate browser-scoped service bindings ([b6a77bc](https://github.com/kernel/kernel-go-sdk/commit/b6a77bc656e6480e1bee0932fcc3b5beb40bcb10))


### Bug Fixes

* align browser-scoped routing with base_url ([64f7f81](https://github.com/kernel/kernel-go-sdk/commit/64f7f811a2f3d7c657ad56a1f34a8bea0206e585))
* clean up go browser routing follow-ups ([77bda33](https://github.com/kernel/kernel-go-sdk/commit/77bda33323c48a60ab652bf42a39cc2bc1f909a0))
* keep browser pool routing in sync with cache ([4f754d1](https://github.com/kernel/kernel-go-sdk/commit/4f754d13f38e5b545de8d84cb26de28f6f2f892a))
* make browser route deletion win over sniffing ([0e44ff3](https://github.com/kernel/kernel-go-sdk/commit/0e44ff363c53b1225d48313be6376b2cda7b057f))
* propagate browser HTTP client config errors ([1ec1358](https://github.com/kernel/kernel-go-sdk/commit/1ec1358150d20982fc582c634e68f42322d1fa4c))
* remove old go browser scope package ([d594f39](https://github.com/kernel/kernel-go-sdk/commit/d594f39a352e2ad5639807f222bb16535cce72c6))
* tighten browser route metadata parsing ([b293866](https://github.com/kernel/kernel-go-sdk/commit/b293866537bddb183ad7e8d014c0e2c298d715a9))
* tighten browser-scoped helper surface ([3e3e33f](https://github.com/kernel/kernel-go-sdk/commit/3e3e33f031d8460662da8a368467a3dcc6a0ec3f))


### Chores

* **internal:** more robust bootstrap script ([c0430e3](https://github.com/kernel/kernel-go-sdk/commit/c0430e3e2952f10648ed0181edf85a69230e9279))


### Documentation

* add browser-scoped raw http example ([92dc96e](https://github.com/kernel/kernel-go-sdk/commit/92dc96e99e14eeb9eb6881115f330369bb3c7542))


### Refactors

* move browser route cache sync into middleware ([62078d3](https://github.com/kernel/kernel-go-sdk/commit/62078d3213dbe4ede9600968b48d5c8e7aa118e4))
* move go browser routing rollout to env ([681e57f](https://github.com/kernel/kernel-go-sdk/commit/681e57f34b3199a4d720fb517da08b3bbd6bc0fb))
* narrow browser-scoped helper exports ([0ac61ef](https://github.com/kernel/kernel-go-sdk/commit/0ac61ef0532e6682eed740765975775767e28a10))
* remove browser session wrapper layer ([3452e53](https://github.com/kernel/kernel-go-sdk/commit/3452e537737576fdb645a4e5de0d9147e50cb33b))
* rename browser routing subresources config ([909c377](https://github.com/kernel/kernel-go-sdk/commit/909c377e21adddbaede526ba2a356352652e947b))
* simplify direct-to-vm route caching ([6bdf25f](https://github.com/kernel/kernel-go-sdk/commit/6bdf25f058d2e0bb552bd47ba4478222d97aa254))

## 0.50.0 (2026-04-20)

Full Changelog: [v0.49.0...v0.50.0](https://github.com/kernel/kernel-go-sdk/compare/v0.49.0...v0.50.0)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Or to pin the version:
<!-- x-release-please-start-version -->

```sh
go get -u 'github.com/kernel/kernel-go-sdk@v0.50.0'
go get -u 'github.com/kernel/kernel-go-sdk@v0.51.0'
```

<!-- x-release-please-end -->
Expand Down
5 changes: 5 additions & 0 deletions authconnection.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,10 @@ type ManagedAuth struct {
// - OneLogin: \*.onelogin.com
// - Ping Identity: _.pingone.com, _.pingidentity.com
AllowedDomains []string `json:"allowed_domains"`
// ID of the underlying browser session driving the current flow (present when flow
// in progress). Use this to inspect or terminate the browser session via the
// `/browsers` API.
BrowserSessionID string `json:"browser_session_id" api:"nullable"`
// Whether automatic re-authentication is possible (has credential, selectors, and
// login_url)
CanReauth bool `json:"can_reauth"`
Expand Down Expand Up @@ -320,6 +324,7 @@ type ManagedAuth struct {
SaveCredentials respjson.Field
Status respjson.Field
AllowedDomains respjson.Field
BrowserSessionID respjson.Field
CanReauth respjson.Field
CanReauthReason respjson.Field
Credential respjson.Field
Expand Down
34 changes: 34 additions & 0 deletions browser_http_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package kernel

import (
"context"
"fmt"
"net/http"
"slices"

"github.com/kernel/kernel-go-sdk/internal/requestconfig"
"github.com/kernel/kernel-go-sdk/lib/browserrouting"
"github.com/kernel/kernel-go-sdk/option"
)

// HTTPClient returns an [http.Client] that performs HTTP requests through the
// browser VM's internal /curl/raw path using cached browser route data.
func (r *BrowserService) HTTPClient(id string, opts ...option.RequestOption) (*http.Client, error) {
opts = slices.Concat(r.Options, opts)
cache := browserRouteCacheFromOptions(opts)
if cache == nil {
return nil, fmt.Errorf("kernel: browser route cache is not configured")
}

route, ok := cache.Load(id)
if !ok {
return nil, fmt.Errorf("kernel: browser route cache does not contain session %s", id)
}

cfg, err := requestconfig.NewRequestConfig(context.Background(), http.MethodGet, "https://example.com", nil, nil, opts...)
if err != nil {
return nil, err
}

return browserrouting.NewHTTPClient(route.BaseURL, route.JWT, cfg.HTTPClient), nil
}
97 changes: 97 additions & 0 deletions browser_routing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package kernel

import (
"os"
"strings"

"github.com/kernel/kernel-go-sdk/internal/requestconfig"
"github.com/kernel/kernel-go-sdk/lib/browserrouting"
"github.com/kernel/kernel-go-sdk/option"
)

const browserRoutingSubresourcesEnv = "KERNEL_BROWSER_ROUTING_SUBRESOURCES"

type browserRoutingOption struct {
cache *browserrouting.RouteCache
subresources []string
}

type browserRouteCacheOption struct {
cache *browserrouting.RouteCache
}

func withBrowserRoutingSubresources(cache *browserrouting.RouteCache, subresources []string) option.RequestOption {
return &browserRoutingOption{cache: cache, subresources: subresources}
}

func (o *browserRoutingOption) Apply(r *requestconfig.RequestConfig) error {
r.Middlewares = append(r.Middlewares, browserrouting.DirectVMRoutingMiddleware(o.cache, o.subresources))
return nil
}

func (o *browserRoutingOption) browserRouteCache() *browserrouting.RouteCache {
return o.cache
}

func (o *browserRouteCacheOption) Apply(*requestconfig.RequestConfig) error {
return nil
}

func (o *browserRouteCacheOption) browserRouteCache() *browserrouting.RouteCache {
return o.cache
}

func withBrowserRouteCache(cache *browserrouting.RouteCache) option.RequestOption {
return &browserRouteCacheOption{cache: cache}
}

func browserRouteCacheFromOptions(opts []option.RequestOption) *browserrouting.RouteCache {
for _, opt := range opts {
if carrier, ok := opt.(interface{ browserRouteCache() *browserrouting.RouteCache }); ok {
if cache := carrier.browserRouteCache(); cache != nil {
return cache
}
}
}
return nil
}

func storeBrowserRouteCache(opts []option.RequestOption, refs ...browserrouting.Ref) {
cache := browserRouteCacheFromOptions(opts)
for _, ref := range refs {
route, ok := browserRouteFromRef(ref)
if cache != nil && ok {
cache.Store(route)
}
}
}

func browserRouteFromRef(ref browserrouting.Ref) (browserrouting.Route, bool) {
norm, err := ref.Normalize()
if err != nil {
return browserrouting.Route{}, false
}
return browserrouting.Route{
SessionID: norm.SessionID,
BaseURL: norm.BaseURL,
JWT: norm.JWT,
}, true
}

func browserRoutingSubresourcesFromEnv() []string {
raw, ok := os.LookupEnv(browserRoutingSubresourcesEnv)
if !ok {
return []string{"curl"}
}
if strings.TrimSpace(raw) == "" {
return []string{}
}

subresources := make([]string, 0)
for _, part := range strings.Split(raw, ",") {
if trimmed := strings.TrimSpace(part); trimmed != "" {
subresources = append(subresources, trimmed)
}
}
return subresources
}
150 changes: 150 additions & 0 deletions browser_routing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package kernel

import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"testing"

"github.com/kernel/kernel-go-sdk/option"
)

func TestBrowserRoutingWarmsCacheAndRoutesAllowlistedSubresources(t *testing.T) {
t.Setenv(browserRoutingSubresourcesEnv, "process")

var calls []struct {
Path string
Auth string
}
var srv *httptest.Server
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls = append(calls, struct {
Path string
Auth string
}{Path: r.URL.Path + "?" + r.URL.RawQuery, Auth: r.Header.Get("Authorization")})

w.Header().Set("Content-Type", "application/json")
switch r.URL.Path {
case "/browsers":
_ = json.NewEncoder(w).Encode(map[string]any{
"session_id": "sess-1",
"base_url": srv.URL + "/browser/kernel",
"cdp_ws_url": "wss://browser-session.test/browser/cdp?jwt=token-abc",
})
default:
_ = json.NewEncoder(w).Encode(map[string]any{
"duration_ms": 1,
"exit_code": 0,
"stderr_b64": "",
"stdout_b64": "",
})
}
}))
defer srv.Close()

client := NewClient(
option.WithBaseURL(srv.URL),
option.WithAPIKey("sk_test"),
option.WithHTTPClient(srv.Client()),
)

if _, err := client.Browsers.New(context.Background(), BrowserNewParams{}); err != nil {
t.Fatal(err)
}
if _, err := client.Browsers.Process.Exec(context.Background(), "sess-1", BrowserProcessExecParams{Command: "echo"}); err != nil {
t.Fatal(err)
}

if route, ok := client.BrowserRouteCache.Load("sess-1"); !ok || route.JWT != "token-abc" {
t.Fatalf("expected warmed browser route cache, got %#v ok=%v", route, ok)
}
if len(calls) != 2 {
t.Fatalf("expected 2 calls, got %d", len(calls))
}
if calls[1].Path != "/browser/kernel/process/exec?jwt=token-abc" {
t.Fatalf("expected direct VM path, got %q", calls[1].Path)
}
if calls[1].Auth != "" {
t.Fatalf("expected authorization header removed, got %q", calls[1].Auth)
}
}

func TestBrowserRoutingSkipsSubresourcesOutsideConfiguredAllowlist(t *testing.T) {
t.Setenv(browserRoutingSubresourcesEnv, "computer")

var paths []string
var srv *httptest.Server
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
paths = append(paths, r.URL.Path)
w.Header().Set("Content-Type", "application/json")
switch r.URL.Path {
case "/browsers":
_ = json.NewEncoder(w).Encode(map[string]any{
"session_id": "sess-1",
"base_url": srv.URL + "/browser/kernel",
"cdp_ws_url": "wss://browser-session.test/browser/cdp?jwt=token-abc",
})
default:
_ = json.NewEncoder(w).Encode(map[string]any{
"duration_ms": 1,
"exit_code": 0,
"stderr_b64": "",
"stdout_b64": "",
})
}
}))
defer srv.Close()

client := NewClient(
option.WithBaseURL(srv.URL),
option.WithAPIKey("sk_test"),
option.WithHTTPClient(srv.Client()),
)

if _, err := client.Browsers.New(context.Background(), BrowserNewParams{}); err != nil {
t.Fatal(err)
}
if _, err := client.Browsers.Process.Exec(context.Background(), "sess-1", BrowserProcessExecParams{Command: "echo"}); err != nil {
t.Fatal(err)
}

if got := paths[len(paths)-1]; got != "/browsers/sess-1/process/exec" {
t.Fatalf("expected control-plane path, got %q", got)
}
}

func TestBrowserRoutingSubresourcesFromEnvDefaultsToCurl(t *testing.T) {
original, ok := os.LookupEnv(browserRoutingSubresourcesEnv)
if err := os.Unsetenv(browserRoutingSubresourcesEnv); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if !ok {
_ = os.Unsetenv(browserRoutingSubresourcesEnv)
return
}
_ = os.Setenv(browserRoutingSubresourcesEnv, original)
})
if got := browserRoutingSubresourcesFromEnv(); len(got) != 1 || got[0] != "curl" {
t.Fatalf("expected default subresources [curl], got %#v", got)
}

t.Setenv(browserRoutingSubresourcesEnv, "")
if got := browserRoutingSubresourcesFromEnv(); len(got) != 0 {
t.Fatalf("expected empty env to disable routing, got %#v", got)
}

t.Setenv(browserRoutingSubresourcesEnv, "curl, process")
got := browserRoutingSubresourcesFromEnv()
want := []string{"curl", "process"}
if len(got) != len(want) {
t.Fatalf("expected %v, got %#v", want, got)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("expected %v, got %#v", want, got)
}
}
}
Loading
Loading