Conversation
|
Firetiger deploy monitoring skipped This PR didn't match the auto-monitor filter configured on your GitHub connection:
Reason: PR modifies client libraries and session handling, not API endpoints (packages/api/cmd/api/) or Temporal workflows (packages/api/lib/temporal) as specified in the filter. To monitor this PR anyway, reply with |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed:
HTTPClient()never extracts user's custom HTTP client- Changed
option.WithHTTPClientto returnrequestconfig.PreRequestOptionFuncsoPreRequestOptionsnow applies it andBrowserSessionClient.HTTPClient()receives the caller’s custom client.
- Changed
Or push these changes by commenting:
@cursor push 3b2a924f02
Preview (3b2a924f02)
diff --git a/option/requestoption.go b/option/requestoption.go
--- a/option/requestoption.go
+++ b/option/requestoption.go
@@ -56,7 +56,7 @@
// For custom uses cases, it is recommended to provide an [*http.Client] with a custom
// [http.RoundTripper] as its transport, rather than directly implementing [HTTPClient].
func WithHTTPClient(client HTTPClient) RequestOption {
- return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
+ return requestconfig.PreRequestOptionFunc(func(r *requestconfig.RequestConfig) error {
if client == nil {
return fmt.Errorf("requestoption: custom http client cannot be nil")
}You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: SDK-level dependency on code-gen-only
golang.org/x/toolsforces Go version bump- I moved
internal/genbrowsersessionservicesto its own Go module and updated generation entrypoints sogolang.org/x/toolsno longer affects the SDK module, allowing the rootgo.modto stay at Go 1.22 without tool-only deps.
- I moved
Or push these changes by commenting:
@cursor push bac079904b
Preview (bac079904b)
diff --git a/browser_session.go b/browser_session.go
--- a/browser_session.go
+++ b/browser_session.go
@@ -1,6 +1,6 @@
package kernel
-//go:generate go run ./internal/genbrowsersessionservices -output browser_session_services_gen.go
+//go:generate sh -c "cd internal/genbrowsersessionservices && go run . -dir ../.. -output browser_session_services_gen.go"
import (
"fmt"
diff --git a/go.mod b/go.mod
--- a/go.mod
+++ b/go.mod
@@ -1,16 +1,13 @@
module github.com/kernel/kernel-go-sdk
-go 1.23.0
+go 1.22
require (
github.com/tidwall/gjson v1.18.0
github.com/tidwall/sjson v1.2.5
- golang.org/x/tools v0.31.0
)
require (
github.com/tidwall/match v1.1.1 // indirect
- github.com/tidwall/pretty v1.2.1 // indirect
- golang.org/x/mod v0.24.0 // indirect
- golang.org/x/sync v0.12.0 // indirect
+ github.com/tidwall/pretty v1.2.0 // indirect
)
diff --git a/go.sum b/go.sum
--- a/go.sum
+++ b/go.sum
@@ -1,18 +1,9 @@
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
-github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
-github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
-golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
-golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
-golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
-golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
-golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
-golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
diff --git a/internal/genbrowsersessionservices/go.mod b/internal/genbrowsersessionservices/go.mod
new file mode 100644
--- /dev/null
+++ b/internal/genbrowsersessionservices/go.mod
@@ -1,0 +1,10 @@
+module github.com/kernel/kernel-go-sdk/internal/genbrowsersessionservices
+
+go 1.23.0
+
+require golang.org/x/tools v0.31.0
+
+require (
+ golang.org/x/mod v0.24.0 // indirect
+ golang.org/x/sync v0.12.0 // indirect
+)
diff --git a/internal/genbrowsersessionservices/go.sum b/internal/genbrowsersessionservices/go.sum
new file mode 100644
--- /dev/null
+++ b/internal/genbrowsersessionservices/go.sum
@@ -1,0 +1,8 @@
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
+golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
+golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
+golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
+golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
diff --git a/scripts/generate-browser-session b/scripts/generate-browser-session
--- a/scripts/generate-browser-session
+++ b/scripts/generate-browser-session
@@ -4,4 +4,7 @@
cd "$(dirname "$0")/.."
-go run ./internal/genbrowsersessionservices -output browser_session_services_gen.go
+(
+ cd internal/genbrowsersessionservices
+ go run . -dir ../.. -output browser_session_services_gen.go
+)You can send follow-ups to the cloud agent here.
Bind browser subresource calls to a browser session's base_url and expose raw HTTP through a standard http.Client so metro-routed access feels like normal Go networking. Made-with: Cursor
Use the browser session base_url directly for path rewriting, preserve custom HTTP clients in HTTPClient(), and add an env-gated integration test for browser-scoped routing. Made-with: Cursor
Avoid depending on base_url path details in the integration test, keep the JWT helper package-private, and make round-tripper conformance explicit while preserving browser-scoped routing behavior. Made-with: Cursor
Keep the raw round-tripper constructor package-private, remove defensive middleware branches that imply unsupported empty inputs, and retain the browser-scoped integration coverage without baking in base_url path details. Made-with: Cursor
Replace the handwritten browser-scoped Go service façade with deterministic generated bindings derived from the generated browser service graph, and enforce regeneration in lint. Made-with: Cursor
1720a28 to
b6a77bc
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Middleware path rewrite ignores URL RawPath field
- After rewriting req.URL.Path, the middleware now clears req.URL.RawPath so escaped-path requests consistently use the updated path, and a regression test covers this case.
Or push these changes by commenting:
@cursor push 4794dcee9b
Preview (4794dcee9b)
diff --git a/lib/browserscope/middleware.go b/lib/browserscope/middleware.go
--- a/lib/browserscope/middleware.go
+++ b/lib/browserscope/middleware.go
@@ -33,6 +33,7 @@
rest = "/" + rest
}
req.URL.Path = prefix + rest
+ req.URL.RawPath = ""
}
}
diff --git a/lib/browserscope/middleware_test.go b/lib/browserscope/middleware_test.go
--- a/lib/browserscope/middleware_test.go
+++ b/lib/browserscope/middleware_test.go
@@ -55,3 +55,27 @@
func TestBrowserSessionMiddlewareType(t *testing.T) {
var _ option.Middleware = BrowserSessionMiddleware("a", "b")
}
+
+func TestBrowserSessionMiddlewareClearsRawPathOnRewrite(t *testing.T) {
+ mw := BrowserSessionMiddleware("sess1", "")
+ var final *http.Request
+ next := func(req *http.Request) (*http.Response, error) {
+ final = req
+ return nil, nil
+ }
+
+ u, err := url.Parse("https://host/browser/kernel/browsers/sess1/process/%20exec")
+ if err != nil {
+ t.Fatal(err)
+ }
+ req := &http.Request{URL: u}
+
+ _, _ = mw(req, next)
+
+ if final.URL.RawPath != "" {
+ t.Fatalf("raw path should be cleared after rewrite: got %q", final.URL.RawPath)
+ }
+ if got := final.URL.EscapedPath(); got != "/browser/kernel/process/%20exec" {
+ t.Fatalf("escaped path rewrite: got %q", got)
+ }
+}You can send follow-ups to the cloud agent here.
Show the browser-scoped HTTPClient flow explicitly so the /curl/raw-backed public API is discoverable from a runnable Go example. Made-with: Cursor
Route browser subresources and raw HTTP through the shared browser route cache so the SDK no longer needs the generated browser-scoped client surface. Made-with: Cursor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 4 total unresolved issues (including 2 from previous reviews).
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Error from
PreRequestOptionssilently swallowed inHTTPClientBrowserService.HTTPClientnow returnsnil, errwhenrequestconfig.PreRequestOptionsfails, and a regression test verifies the error is propagated.
- ✅ Fixed: NewClient mutates shared option causing cross-client cache corruption
NewClientnow clones eachbrowserRoutingOptionbefore injecting the per-client cache so reused options no longer share mutable cache pointers, with a test confirming per-client cache isolation.
Or push these changes by commenting:
@cursor push 2f05d40ea0
Preview (2f05d40ea0)
diff --git a/browser.go b/browser.go
--- a/browser.go
+++ b/browser.go
@@ -174,7 +174,7 @@
}
cfg, err := requestconfig.PreRequestOptions(opts...)
if err != nil {
- return browserscope.HTTPClient(route.BaseURL, route.JWT, nil), nil
+ return nil, err
}
return browserscope.HTTPClient(route.BaseURL, route.JWT, cfg.HTTPClient), nil
}
diff --git a/browser_routing_test.go b/browser_routing_test.go
--- a/browser_routing_test.go
+++ b/browser_routing_test.go
@@ -111,3 +111,17 @@
t.Fatalf("expected control-plane path, got %q", got)
}
}
+
+func TestBrowserRoutingOptionReuseKeepsPerClientRouteCache(t *testing.T) {
+ routingOpt := WithBrowserRouting(BrowserRoutingConfig{Enabled: true, DirectToVMSubresources: []string{"process"}})
+
+ client1 := NewClient(routingOpt)
+ client2 := NewClient(routingOpt)
+
+ if got := browserRouteCacheFromOptions(client1.Options); got != client1.BrowserRouteCache {
+ t.Fatalf("expected client1 options to resolve client1 cache, got %p want %p", got, client1.BrowserRouteCache)
+ }
+ if got := browserRouteCacheFromOptions(client2.Options); got != client2.BrowserRouteCache {
+ t.Fatalf("expected client2 options to resolve client2 cache, got %p want %p", got, client2.BrowserRouteCache)
+ }
+}
diff --git a/browser_session_httpclient_test.go b/browser_session_httpclient_test.go
--- a/browser_session_httpclient_test.go
+++ b/browser_session_httpclient_test.go
@@ -1,11 +1,13 @@
package kernel
import (
+ "errors"
"io"
"net/http"
"net/http/httptest"
"testing"
+ "github.com/kernel/kernel-go-sdk/internal/requestconfig"
"github.com/kernel/kernel-go-sdk/lib/browserscope"
"github.com/kernel/kernel-go-sdk/option"
)
@@ -75,3 +77,30 @@
t.Fatal("expected cached route lookup failure")
}
}
+
+func TestBrowserSessionHTTPClientPropagatesPreRequestOptionError(t *testing.T) {
+ c := NewClient(
+ option.WithBaseURL("https://api.example/"),
+ option.WithAPIKey("sk"),
+ )
+
+ primeBrowserRouteCache(c.Options, browserscope.Ref{
+ SessionID: "sid",
+ BaseURL: "https://browser-session.test/browser/kernel",
+ CdpWsURL: "wss://x/browser/cdp?jwt=j1",
+ })
+
+ wantErr := errors.New("pre-request failed")
+ client, err := c.Browsers.HTTPClient(
+ "sid",
+ requestconfig.PreRequestOptionFunc(func(*requestconfig.RequestConfig) error {
+ return wantErr
+ }),
+ )
+ if !errors.Is(err, wantErr) {
+ t.Fatalf("expected error %q, got %v", wantErr, err)
+ }
+ if client != nil {
+ t.Fatal("expected nil client when pre-request options fail")
+ }
+}
diff --git a/client.go b/client.go
--- a/client.go
+++ b/client.go
@@ -65,9 +65,11 @@
func NewClient(opts ...option.RequestOption) (r Client) {
opts = append(DefaultClientOptions(), opts...)
cache := browserscope.NewRouteCache()
- for _, opt := range opts {
+ for i, opt := range opts {
if routing, ok := opt.(*browserRoutingOption); ok {
- routing.cache = cache
+ routingWithCache := *routing
+ routingWithCache.cache = cache
+ opts[i] = &routingWithCache
}
}
opts = append(opts, withBrowserRouteCache(cache))You can send follow-ups to the cloud agent here.
Drop the extra cache priming helper, remove metro wording, and rename the example so the go diff stays focused on direct-to-VM routing. Made-with: Cursor
Rename the browser routing allowlist field to Subresources so the direct-to-VM configuration is shorter and easier to read. Made-with: Cursor
Rename the handwritten routing helpers to browserrouting, fix the shared-cache and RawPath issues, and revert the generated/module churn that should not stay in the PR. Made-with: Cursor
Drop the superseded lib/browserscope files now that the renamed browserrouting package owns the direct-to-VM helpers. Made-with: Cursor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Config error silently swallowed, dropping custom HTTP client
- The browser HTTP client constructor now returns the
NewRequestConfigerror instead of silently falling back to a default client.
- The browser HTTP client constructor now returns the
Or push these changes by commenting:
@cursor push 8a33f5475d
Preview (8a33f5475d)
diff --git a/browser_http_client.go b/browser_http_client.go
--- a/browser_http_client.go
+++ b/browser_http_client.go
@@ -27,7 +27,7 @@
cfg, err := requestconfig.NewRequestConfig(context.Background(), http.MethodGet, "https://example.com", nil, nil, opts...)
if err != nil {
- return browserrouting.NewHTTPClient(route.BaseURL, route.JWT, nil), nil
+ return nil, err
}
return browserrouting.NewHTTPClient(route.BaseURL, route.JWT, cfg.HTTPClient), nilYou can send follow-ups to the cloud agent here.
Stop exposing browser routing rollout controls on the client constructor and derive direct-to-VM subresource routing from KERNEL_BROWSER_ROUTING_SUBRESOURCES instead, defaulting to curl while keeping raw HTTP helpers cache-backed.
Return request config errors from Browsers.HTTPClient instead of silently falling back to the default client, so invalid options do not drop custom HTTP behavior without notice. Add a regression test for the failure path. Made-with: Cursor
Centralize browser route cache warm-up and eviction in the shared routing middleware so browser service methods can stay generic while direct-to-VM routing still learns browser base URLs from API responses. Made-with: Cursor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Missing default case causes incorrect path scanning
- I added a default branch in parseBrowserMetadataPath to immediately return false for subresource paths after the first browsers segment.
- ✅ Fixed: Duplicated dead post-response code in middleware
- I removed the duplicated post-response block by unifying response handling into a single shared section after optional direct-VM rewrite logic.
Or push these changes by commenting:
@cursor push 8ec9b4a5bd
Preview (8ec9b4a5bd)
diff --git a/lib/browserrouting/route_cache.go b/lib/browserrouting/route_cache.go
--- a/lib/browserrouting/route_cache.go
+++ b/lib/browserrouting/route_cache.go
@@ -79,42 +79,29 @@
return func(req *http.Request, next option.MiddlewareNext) (*http.Response, error) {
cacheSessionID, cacheablePath := parseBrowserMetadataPath(req.URL.Path)
sessionID, subresource, suffix, ok := parseDirectVMPath(req.URL.Path)
- if !ok {
- res, err := next(req)
- if err != nil {
- return res, err
- }
- if req.Method == http.MethodDelete && cacheSessionID != "" && isSuccessfulResponse(res) {
- cache.Delete(cacheSessionID)
- }
- if cacheablePath {
- if err := sniffAndPopulateCache(res, cache); err != nil {
- return nil, err
- }
- }
- return res, nil
- }
- if _, ok := allowed[subresource]; ok {
- route, ok := cache.Load(sessionID)
- if ok {
- base, err := url.Parse(route.BaseURL)
- if err != nil {
- return nil, err
- }
- req.Header.Del("Authorization")
- if route.JWT != "" {
- q := req.URL.Query()
- if q.Get("jwt") == "" {
- q.Set("jwt", route.JWT)
- req.URL.RawQuery = q.Encode()
+ if ok {
+ if _, ok := allowed[subresource]; ok {
+ route, ok := cache.Load(sessionID)
+ if ok {
+ base, err := url.Parse(route.BaseURL)
+ if err != nil {
+ return nil, err
}
+ req.Header.Del("Authorization")
+ if route.JWT != "" {
+ q := req.URL.Query()
+ if q.Get("jwt") == "" {
+ q.Set("jwt", route.JWT)
+ req.URL.RawQuery = q.Encode()
+ }
+ }
+
+ req.URL.Scheme = base.Scheme
+ req.URL.Host = base.Host
+ req.Host = base.Host
+ req.URL.Path = joinURLPath(base.Path, subresource, suffix)
+ req.URL.RawPath = ""
}
-
- req.URL.Scheme = base.Scheme
- req.URL.Host = base.Host
- req.Host = base.Host
- req.URL.Path = joinURLPath(base.Path, subresource, suffix)
- req.URL.RawPath = ""
}
}
@@ -148,6 +135,8 @@
return "", false
}
return parts[i+1], true
+ default:
+ return "", false
}
}
return "", falseYou can send follow-ups to the cloud agent here.
Reject subresource paths when detecting browser metadata responses and unify the post-response cache sync path so routing middleware does not carry dead duplicate logic. Made-with: Cursor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Cache delete then sniff ordering re-adds deleted route
- Reordered
finalizeResponseto sniff/populate first and perform successful DELETE eviction afterward so deleted sessions cannot be re-cached from response bodies.
- Reordered
Or push these changes by commenting:
@cursor push dbaa41f099
Preview (dbaa41f099)
diff --git a/lib/browserrouting/route_cache.go b/lib/browserrouting/route_cache.go
--- a/lib/browserrouting/route_cache.go
+++ b/lib/browserrouting/route_cache.go
@@ -135,14 +135,14 @@
}
func finalizeResponse(req *http.Request, res *http.Response, cache *RouteCache, cacheSessionID string, cacheablePath bool) (*http.Response, error) {
- if req.Method == http.MethodDelete && cacheSessionID != "" && isSuccessfulResponse(res) {
- cache.Delete(cacheSessionID)
- }
if cacheablePath {
if err := sniffAndPopulateCache(res, cache); err != nil {
return nil, err
}
}
+ if req.Method == http.MethodDelete && cacheSessionID != "" && isSuccessfulResponse(res) {
+ cache.Delete(cacheSessionID)
+ }
return res, nil
}You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit b293866. Configure here.
Process cache sniffing before successful browser delete eviction so delete responses that include browser metadata cannot reinsert stale route entries. Add a regression test for JSON delete responses. Made-with: Cursor
Sayan-
left a comment
There was a problem hiding this comment.
great stuff - thanks for iterating on the shape!
Warm the direct VM route cache from browser pool acquire responses and evict released sessions by sniffing the pool release request body in middleware. Made-with: Cursor
Sayan-
left a comment
There was a problem hiding this comment.
great stuff - thanks for iterating on the shape!


Summary
KERNEL_BROWSER_ROUTING_SUBRESOURCEScurlwhen the env var is unset, and treat an explicit empty string as fully disabling browser subresource routingBrowserRouteCache, which still powersBrowsers.HTTPClient(sessionID)and cache-backed direct/curl/rawrequestsNewClient()so the SDK can grow toward automatic routing without exposing rollout knobs in the public APIRollout behavior
curlsubresources directly to the browser VM"": disable browser subresource routing entirelyBrowsers.HTTPClient(sessionID)still always goes direct to the browser VM because it uses the cached browser route and/curl/rawTest plan
go test ./...KERNEL_API_KEY=... KERNEL_BASE_URL=https://api.onkernel.com go run ./examples/browser-routingNote
Medium Risk
Adds a new request-rewriting middleware and cached route handling that changes how certain browser subresource calls are routed and how auth headers/query params are rewritten, which could impact request correctness. Behavior is gated by
KERNEL_BROWSER_ROUTING_SUBRESOURCESbut defaults to routingcurldirectly.Overview
Enables direct-to-VM routing for selected browser subresource endpoints by introducing a shared
BrowserRouteCacheonClientand aDirectVMRoutingMiddlewarethat rewrites allowlisted/browsers/{session}/...requests to the sessionbase_url, stripsAuthorization, and injectsjwtwhen available.Adds
Browsers.HTTPClient(sessionID)to return anhttp.Clientthat always tunnels egress through the browser VM’s internal/curl/rawusing cached route data, plus env-driven rollout viaKERNEL_BROWSER_ROUTING_SUBRESOURCES(unset defaults tocurl, empty disables routing).Includes new
lib/browserroutingimplementation (route cache, websocket-jwt extraction, raw curl round-tripper), an example program, and unit/integration tests covering cache warm/evict, allowlist behavior, and request/response preservation.Reviewed by Cursor Bugbot for commit 4f754d1. Bugbot is set up for automated code reviews on this repo. Configure here.