From 5cec28fbd98f48acb617fa9ced1d830810edd156 Mon Sep 17 00:00:00 2001 From: hiroTamada <88675973+hiroTamada@users.noreply.github.com> Date: Thu, 24 Sep 2026 19:39:29 +0000 Subject: [PATCH 1/2] Add browser proxy routes to create command --- README.md | 1 + cmd/browsers.go | 84 ++++++++++++++++++++++++++- cmd/browsers_test.go | 111 ++++++++++++++++++++++++++++++++++++ cmd/browsers_webmcp.go | 10 ++-- cmd/browsers_webmcp_test.go | 8 +-- go.mod | 2 +- go.sum | 4 +- 7 files changed, 206 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 21fbba85..757df6cb 100644 --- a/README.md +++ b/README.md @@ -255,6 +255,7 @@ kernel search --request '{"query":"browser automation","include_domains":["examp - `--kiosk` - Launch browser in kiosk mode - `--region us-east|eu-west|ap-southeast` - Geographic region for the session. Fixed once the session is created; requires a Start-Up or Enterprise plan and defaults to `us-east`. - `--private-host ` - Destination the browser reaches directly through the session's own network instead of Kernel-managed egress, for private hosts on a VPN or tunnel the session joins (repeatable or comma-separated, max 32). Accepts hostname patterns (`*.example.ts.net`), IPs (`10.1.30.63`, `[fd00::1]`), and private CIDRs (`100.64.0.0/10`). Replaces the default private ranges (RFC1918, `100.64.0.0/10`, `fc00::/7`); omit to keep them. Fixed once the session is created. Unrelated to a proxy's `--bypass-host`, which only chooses between upstream proxy and Kernel-managed direct egress. + - `--proxy-route '[,...]='` - Route matching browser requests through a selected proxy (repeatable, max 10 routes with 1–50 hosts each). Example: `--proxy-route 'api.ipify.org,*.ipify.org=name:my-dc-proxy'`. The proxy is an ID by default; use `id:` or `name:` explicitly. Exact hostnames beat wildcards; longer wildcard suffixes beat shorter ones. `*.example.com` matches subdomains, not `example.com`. Matching ignores case and ports. Unmatched hosts use `--proxy-*` or default egress, while `--start-url` uses the top-level proxy during setup. Routes are create-only and are not available on pool sessions. - `--start-url ` - Initial page to open on launch - `--proxy-id ` / `--proxy-name ` - Use that proxy for the session regardless of stealth (mutually exclusive with each other and with `--proxy-mode`) - `--proxy-mode direct|default` - Egress mode instead of a selected proxy: `direct` for no proxy regardless of stealth, `default` for the stealth-derived default (Kernel's stealth proxy with `--stealth`, direct egress otherwise). Omit all proxy flags to get the default. diff --git a/cmd/browsers.go b/cmd/browsers.go index d8bcfd90..6314e514 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -234,6 +234,70 @@ func buildNetworkParam(privateHosts []string) (kernel.BrowserNetworkConfigParam, return network, nil } +const ( + maxProxyRoutes = 10 + maxProxyRouteHosts = 50 +) + +// parseProxyRoutes converts each --proxy-route HOST[,HOST...]=ID|id:ID|name:NAME +// into the SDK's create-only network parameter. Host semantics are validated by the API. +func parseProxyRoutes(values []string) ([]kernel.BrowserNetworkConfigProxyRouteParam, error) { + if len(values) > maxProxyRoutes { + return nil, fmt.Errorf("too many --proxy-route entries: %d (maximum %d)", len(values), maxProxyRoutes) + } + routes := make([]kernel.BrowserNetworkConfigProxyRouteParam, 0, len(values)) + for _, value := range values { + hostList, selector, ok := strings.Cut(value, "=") + if !ok || strings.TrimSpace(selector) == "" || strings.Contains(selector, "=") { + return nil, fmt.Errorf("invalid --proxy-route %q: expected HOST[,HOST...]=PROXY", value) + } + hosts := strings.Split(hostList, ",") + if len(hosts) > maxProxyRouteHosts { + return nil, fmt.Errorf("too many hosts in --proxy-route: %d (maximum %d)", len(hosts), maxProxyRouteHosts) + } + for i, host := range hosts { + hosts[i] = strings.TrimSpace(host) + if hosts[i] == "" { + return nil, fmt.Errorf("invalid --proxy-route %q: hosts must be non-empty", value) + } + } + selector = strings.TrimSpace(selector) + proxy := kernel.BrowserNetworkConfigProxyRouteProxyParam{} + switch { + case strings.HasPrefix(selector, "name:"): + proxy.Name = kernel.Opt(strings.TrimSpace(strings.TrimPrefix(selector, "name:"))) + if proxy.Name.Value == "" { + return nil, fmt.Errorf("invalid --proxy-route %q: proxy name must be non-empty", value) + } + case strings.HasPrefix(selector, "id:"): + selector = strings.TrimSpace(strings.TrimPrefix(selector, "id:")) + if selector == "" { + return nil, fmt.Errorf("invalid --proxy-route %q: proxy ID must be non-empty", value) + } + proxy.ID = kernel.Opt(selector) + default: + proxy.ID = kernel.Opt(selector) + } + routes = append(routes, kernel.BrowserNetworkConfigProxyRouteParam{Hosts: hosts, Proxy: proxy}) + } + return routes, nil +} + +func formatProxyRoutes(network kernel.BrowserNetworkConfig) string { + if len(network.ProxyRoutes) == 0 { + return "-" + } + routes := make([]string, 0, len(network.ProxyRoutes)) + for _, route := range network.ProxyRoutes { + proxy := route.Proxy.ID + if proxy == "" { + proxy = route.Proxy.Name + } + routes = append(routes, strings.Join(route.Hosts, ", ")+" = "+proxy) + } + return strings.Join(routes, "; ") +} + // formatPrivateHosts renders a network configuration for table output. A missing // private_hosts list means the API's default private ranges apply; an explicit // empty list means nothing routes around Kernel-managed egress. @@ -394,6 +458,7 @@ type BrowsersCreateInput struct { ProxyMode string Region string PrivateHosts []string + ProxyRoutes []string StartURL string Extensions []string Vaults []string @@ -660,7 +725,12 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error { if err != nil { return err } - if len(network.PrivateHosts) > 0 { + routes, err := parseProxyRoutes(in.ProxyRoutes) + if err != nil { + return err + } + network.ProxyRoutes = routes + if len(network.PrivateHosts) > 0 || len(network.ProxyRoutes) > 0 { params.Network = network } @@ -732,7 +802,9 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error { return util.PrintPrettyJSON(browser) } - printBrowserSessionResult(browser.SessionID, browser.CdpWsURL, browser.BrowserLiveViewURL, browser.Profile, browser.ProfileSaveChanges, browser.StartURL, browser.Name, browser.Tags) + tableData := buildBrowserTableData(browser.SessionID, browser.CdpWsURL, browser.BrowserLiveViewURL, browser.Profile, browser.ProfileSaveChanges, browser.StartURL, browser.Name, browser.Tags) + tableData = append(tableData, []string{"Private Hosts", formatPrivateHosts(browser.Network)}, []string{"Proxy Routes", formatProxyRoutes(browser.Network)}) + PrintTableNoPad(tableData, true) if len(browser.Vaults) > 0 { rows := pterm.TableData{{"Attached vault ID", "Name"}} for _, vault := range browser.Vaults { @@ -874,6 +946,7 @@ func (b BrowsersCmd) Get(ctx context.Context, in BrowsersGetInput) error { tableData = append(tableData, []string{"Proxy", proxy}) } tableData = append(tableData, []string{"Private Hosts", formatPrivateHosts(browser.Network)}) + tableData = append(tableData, []string{"Proxy Routes", formatProxyRoutes(browser.Network)}) if vaults := formatVaultReferences(browser.Vaults); vaults != "" { tableData = append(tableData, []string{"Vaults", vaults}) } @@ -3172,6 +3245,7 @@ unrestricted code execution inside the browser VM and is not sandboxed.`, browsersCreateCmd.Flags().String("proxy-mode", "", "Proxy egress mode instead of a selected proxy: 'direct' for no proxy regardless of stealth, or 'default' for the browser default (Kernel's stealth proxy when --stealth is set, direct egress otherwise)") browsersCreateCmd.Flags().String("region", "", "Geographic region for the session: 'us-east', 'eu-west', or 'ap-southeast'. Fixed once the session is created; requires a Start-Up or Enterprise plan and defaults to us-east") browsersCreateCmd.Flags().StringSlice("private-host", nil, "Destinations the browser reaches directly through its own network instead of Kernel-managed egress, for private hosts on a VPN or tunnel the session joins (repeat or comma-separated, max 32). Accepts hostname patterns ('*.example.ts.net'), IPs ('10.1.30.63', '[fd00::1]'), and private CIDRs ('100.64.0.0/10'). Replaces the default private ranges (RFC1918, 100.64.0.0/10, fc00::/7); omit to keep them. Fixed once the session is created") + browsersCreateCmd.Flags().StringArray("proxy-route", nil, "Route HOST[,HOST...]=PROXY through a proxy (repeatable, max 10 routes and 50 hosts per route). PROXY is an ID by default; use id:ID or name:NAME explicitly. Exact hosts beat wildcards (longer suffixes win); *.example.com excludes example.com. Unmatched hosts use --proxy-* or default egress; start_url uses the top-level proxy. Create-only") browsersCreateCmd.Flags().String("start-url", "", "Initial page to open on launch") browsersCreateCmd.Flags().StringSlice("extension", []string{}, "Extension IDs or names to load (repeatable; may be passed multiple times or comma-separated)") browsersCreateCmd.Flags().String("viewport", "", "Browser viewport size (e.g., 1920x1080@25). Supported: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1024x768@60, 1200x800@60, 1280x800@60") @@ -3305,6 +3379,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { proxyMode, _ := cmd.Flags().GetString("proxy-mode") region, _ := cmd.Flags().GetString("region") privateHosts, _ := cmd.Flags().GetStringSlice("private-host") + proxyRoutes, _ := cmd.Flags().GetStringArray("proxy-route") startURL, _ := cmd.Flags().GetString("start-url") extensions, _ := cmd.Flags().GetStringSlice("extension") vaults, _ := cmd.Flags().GetStringArray("vault") @@ -3331,6 +3406,10 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { } } + if (poolID != "" || poolName != "") && cmd.Flags().Changed("proxy-route") { + return fmt.Errorf("--proxy-route cannot be used with --pool-id or --pool-name; routes require a new browser") + } + if poolID != "" && poolName != "" { pterm.Error.Println("must specify at most one of --pool-id or --pool-name") return nil @@ -3446,6 +3525,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { ProxyMode: proxyMode, Region: region, PrivateHosts: privateHosts, + ProxyRoutes: proxyRoutes, StartURL: startURL, Extensions: extensions, Vaults: vaults, diff --git a/cmd/browsers_test.go b/cmd/browsers_test.go index 0919cc24..0a245e10 100644 --- a/cmd/browsers_test.go +++ b/cmd/browsers_test.go @@ -610,6 +610,117 @@ func TestBrowsersCreate_WithPrivateHosts(t *testing.T) { })) } +func TestParseProxyRoutes(t *testing.T) { + routes, err := parseProxyRoutes([]string{" api.ipify.org , *.ipify.org =name:my-dc-proxy", "other.example=id:proxy-123", "fallback.example=proxy-456"}) + require.NoError(t, err) + require.Len(t, routes, 3) + assert.Equal(t, []string{"api.ipify.org", "*.ipify.org"}, routes[0].Hosts) + assert.Equal(t, "my-dc-proxy", routes[0].Proxy.Name.Value) + assert.False(t, routes[0].Proxy.ID.Valid()) + assert.Equal(t, "proxy-123", routes[1].Proxy.ID.Value) + assert.Equal(t, "proxy-456", routes[2].Proxy.ID.Value) + assert.False(t, routes[2].Proxy.Name.Valid()) + + for _, value := range []string{"", "host", "=id:proxy", "host=", "host= id: ", "host=name: ", "host=proxy=other", "host,,other=proxy", "host,=proxy", ",host=proxy"} { + t.Run(value, func(t *testing.T) { + _, err := parseProxyRoutes([]string{value}) + assert.Error(t, err) + }) + } + _, err = parseProxyRoutes(make([]string, maxProxyRoutes+1)) + assert.ErrorContains(t, err, "maximum 10") + atLimit := make([]string, maxProxyRoutes) + for i := range atLimit { + atLimit[i] = fmt.Sprintf("host-%d=proxy", i) + } + _, err = parseProxyRoutes(atLimit) + require.NoError(t, err) + _, err = parseProxyRoutes([]string{strings.Repeat("host,", maxProxyRouteHosts) + "host=proxy"}) + assert.ErrorContains(t, err, "maximum 50") + _, err = parseProxyRoutes([]string{strings.Repeat("host,", maxProxyRouteHosts-1) + "host=proxy"}) + require.NoError(t, err) + // Hostname and wildcard validity, duplicates, and overlaps belong to the API. + _, err = parseProxyRoutes([]string{"*.com=proxy", "*.com=proxy"}) + assert.NoError(t, err) +} + +func TestBrowsersCreate_WithProxyRoutes(t *testing.T) { + setupStdoutCapture(t) + var captured kernel.BrowserNewParams + fake := &FakeBrowsersService{ + NewFunc: func(_ context.Context, body kernel.BrowserNewParams, _ ...option.RequestOption) (*kernel.BrowserNewResponse, error) { + captured = body + var resp kernel.BrowserNewResponse + err := json.Unmarshal([]byte(`{"session_id":"sess-routes","network":{"private_hosts":["internal.example"],"proxy_routes":[{"hosts":["api.ipify.org","*.ipify.org"],"proxy":{"id":"resolved-proxy"}}]}}`), &resp) + require.NoError(t, err) + return &resp, nil + }, + } + b := BrowsersCmd{browsers: fake} + err := b.Create(context.Background(), BrowsersCreateInput{ + PrivateHosts: []string{"internal.example"}, + ProxyRoutes: []string{"api.ipify.org,*.ipify.org=name:my-dc-proxy", "other.example=proxy-456"}, + ProxyMode: "direct", + }) + require.NoError(t, err) + require.Len(t, captured.Network.ProxyRoutes, 2) + assert.Equal(t, []string{"api.ipify.org", "*.ipify.org"}, captured.Network.ProxyRoutes[0].Hosts) + assert.Equal(t, "my-dc-proxy", captured.Network.ProxyRoutes[0].Proxy.Name.Value) + assert.False(t, captured.Network.ProxyRoutes[0].Proxy.ID.Valid()) + assert.Equal(t, "proxy-456", captured.Network.ProxyRoutes[1].Proxy.ID.Value) + assert.Equal(t, []string{"internal.example"}, captured.Network.PrivateHosts) + assert.Equal(t, kernel.BrowserProxyModeDirect, captured.Proxy.Mode) + raw, err := captured.MarshalJSON() + require.NoError(t, err) + assert.Contains(t, string(raw), `"proxy_routes":[{"hosts":["api.ipify.org","*.ipify.org"],"proxy":{"name":"my-dc-proxy"}}`) + assert.Contains(t, outBuf.String(), "Private Hosts") + assert.Contains(t, outBuf.String(), "Proxy Routes") + assert.Contains(t, outBuf.String(), "api.ipify.org, *.ipify.org = resolved-proxy") + + jsonOutput := captureStdout(t, func() { + require.NoError(t, b.Create(context.Background(), BrowsersCreateInput{ProxyRoutes: []string{"host=proxy"}, Output: "json"})) + }) + assert.Contains(t, jsonOutput, `"proxy_routes"`) + assert.Contains(t, jsonOutput, `"resolved-proxy"`) + assert.Empty(t, captured.Network.PrivateHosts) + assert.Len(t, captured.Network.ProxyRoutes, 1) + assert.Error(t, b.Create(context.Background(), BrowsersCreateInput{ProxyRoutes: []string{"host="}})) +} + +func TestBrowsersGet_ProxyRoutes(t *testing.T) { + setupStdoutCapture(t) + fake := &FakeBrowsersService{ + GetFunc: func(_ context.Context, _ string, _ kernel.BrowserGetParams, _ ...option.RequestOption) (*kernel.BrowserGetResponse, error) { + var resp kernel.BrowserGetResponse + err := json.Unmarshal([]byte(`{"session_id":"sess-routes","network":{"private_hosts":["internal.example"],"proxy_routes":[{"hosts":["api.ipify.org"],"proxy":{"id":"resolved-proxy"}}]}}`), &resp) + require.NoError(t, err) + return &resp, nil + }, + } + b := BrowsersCmd{browsers: fake} + require.NoError(t, b.Get(context.Background(), BrowsersGetInput{Identifier: "sess-routes"})) + assert.Contains(t, outBuf.String(), "Private Hosts") + assert.Contains(t, outBuf.String(), "Proxy Routes") + assert.Contains(t, outBuf.String(), "api.ipify.org = resolved-proxy") + jsonOutput := captureStdout(t, func() { + require.NoError(t, b.Get(context.Background(), BrowsersGetInput{Identifier: "sess-routes", Output: "json"})) + }) + assert.Contains(t, jsonOutput, `"proxy_routes"`) + assert.Contains(t, jsonOutput, `"resolved-proxy"`) +} + +func TestProxyRouteFlagIsCreateOnly(t *testing.T) { + create, _, err := rootCmd.Find([]string{"browsers", "create"}) + require.NoError(t, err) + assert.NotNil(t, create.Flags().Lookup("proxy-route")) + for _, path := range [][]string{{"browsers", "update"}, {"browser-pools", "create"}, {"browser-pools", "update"}} { + cmd, _, err := rootCmd.Find(path) + require.NoError(t, err) + assert.Nil(t, cmd.Flags().Lookup("proxy-route")) + } + assert.False(t, poolLeaseAllowedFlags()["proxy-route"]) +} + func TestBrowsersCreate_WithRegion(t *testing.T) { setupStdoutCapture(t) diff --git a/cmd/browsers_webmcp.go b/cmd/browsers_webmcp.go index cafabad8..68cd731a 100644 --- a/cmd/browsers_webmcp.go +++ b/cmd/browsers_webmcp.go @@ -19,7 +19,7 @@ import ( // BrowserWebMCPService defines the subset we use for native page tools. type BrowserWebMCPService interface { - ListTools(ctx context.Context, idOrName string, opts ...option.RequestOption) (*kernel.ToolsResponse, error) + ListTools(ctx context.Context, idOrName string, query kernel.BrowserWebmcpListToolsParams, opts ...option.RequestOption) (*kernel.ToolsResponse, error) InvokeTool(ctx context.Context, idOrName string, body kernel.BrowserWebmcpInvokeToolParams, opts ...option.RequestOption) (*kernel.InvocationResult, error) } @@ -39,7 +39,7 @@ func (b BrowsersCmd) WebMCPList(ctx context.Context, in BrowsersWebMCPListInput) if err := validateJSONOutput(in.Output); err != nil { return err } - res, err := b.webmcp.ListTools(ctx, in.Identifier) + res, err := b.webmcp.ListTools(ctx, in.Identifier, kernel.BrowserWebmcpListToolsParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -53,10 +53,10 @@ func (b BrowsersCmd) WebMCPList(ctx context.Context, in BrowsersWebMCPListInput) rows := pterm.TableData{{"Name", "Tool Ref", "Page URL", "Tab ID", "Read Only"}} for _, tool := range res.Tools { readOnly := "-" - if tool.Annotations.JSON.ReadOnly.Valid() { - readOnly = strconv.FormatBool(tool.Annotations.ReadOnly) + if tool.Tool.Annotations.JSON.ReadOnlyHint.Valid() { + readOnly = strconv.FormatBool(tool.Tool.Annotations.ReadOnlyHint) } - rows = append(rows, []string{tool.Name, tool.ToolRef, tool.Source.PageURL, strconv.FormatInt(tool.Source.TabID, 10), readOnly}) + rows = append(rows, []string{tool.Tool.Name, tool.ToolRef, tool.Source.PageURL, strconv.FormatInt(tool.Source.TabID, 10), readOnly}) } PrintTableNoPad(rows, true) return nil diff --git a/cmd/browsers_webmcp_test.go b/cmd/browsers_webmcp_test.go index 179e7d4b..56c9a49e 100644 --- a/cmd/browsers_webmcp_test.go +++ b/cmd/browsers_webmcp_test.go @@ -36,7 +36,7 @@ func executeWebMCPCommand(t *testing.T, handler http.HandlerFunc, stdin string, return stdout, buf.String(), err } -const webMCPToolsFixture = `{"tools":[{"name":"search","tool_ref":"opaque/ref+==","description":"Search the page","input_schema":{"type":"object"},"annotations":{"read_only":true,"autosubmit":false,"consequential":false,"untrusted_content":true},"source":{"window_id":1,"tab_id":42,"page_url":"https://example.com","page_title":"Example","frame":null}}],"future_field":true}` +const webMCPToolsFixture = `{"tools":[{"tool":{"name":"search","description":"Search the page","inputSchema":{"type":"object"},"annotations":{"readOnlyHint":true,"autosubmit":false}},"tool_ref":"opaque/ref+==","source":{"window_id":1,"tab_id":42,"page_url":"https://example.com","page_title":"Example","frame":null}}],"future_field":true}` func TestWebMCPCommandWiring(t *testing.T) { for _, name := range []string{"list", "invoke"} { @@ -93,15 +93,15 @@ func TestWebMCPListEmpty(t *testing.T) { func TestWebMCPListAnnotations(t *testing.T) { for _, tc := range []struct{ annotation, want string }{ - {`{"read_only":true}`, "true"}, - {`{"read_only":false}`, "false"}, + {`{"readOnlyHint":true}`, "true"}, + {`{"readOnlyHint":false}`, "false"}, {`{}`, "-"}, {`null`, "-"}, } { t.Run(tc.annotation, func(t *testing.T) { _, table, err := executeWebMCPCommand(t, func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, `{"tools":[{"name":"search","annotations":%s}]}`, tc.annotation) + fmt.Fprintf(w, `{"tools":[{"tool":{"name":"search","annotations":%s}}]}`, tc.annotation) }, "", "list", "my-browser") require.NoError(t, err) rows := strings.Split(strings.TrimSpace(table), "\n") diff --git a/go.mod b/go.mod index dcae6b62..ca699418 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/kernel/kernel-go-sdk v0.110.0 + github.com/kernel/kernel-go-sdk v0.112.0 github.com/klauspost/compress v1.18.5 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index 1f47cec9..31ac173c 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.110.0 h1:2KkE0hAlJav5xg2818Eg+mIK2p1F2nDZ0rZdA2EO1QQ= -github.com/kernel/kernel-go-sdk v0.110.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.112.0 h1:WCWHRtHQs/z4Q8cwZ/uS/HnxZZi6roge+VYai5d73ic= +github.com/kernel/kernel-go-sdk v0.112.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= From 230818f0b4433386edd6dbafb7b66cf91bfa7097 Mon Sep 17 00:00:00 2001 From: hiroTamada <88675973+hiroTamada@users.noreply.github.com> Date: Thu, 24 Sep 2026 20:06:49 +0000 Subject: [PATCH 2/2] Preserve default private hosts with proxy routes --- cmd/browsers.go | 4 +++- cmd/browsers_test.go | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cmd/browsers.go b/cmd/browsers.go index 6314e514..13ed46a6 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -230,7 +230,9 @@ func buildNetworkParam(privateHosts []string) (kernel.BrowserNetworkConfigParam, if len(hosts) > maxPrivateHosts { return network, fmt.Errorf("too many --private-host entries: %d (maximum %d)", len(hosts), maxPrivateHosts) } - network.PrivateHosts = hosts + if len(hosts) > 0 { + network.PrivateHosts = hosts + } return network, nil } diff --git a/cmd/browsers_test.go b/cmd/browsers_test.go index 0a245e10..801fe04c 100644 --- a/cmd/browsers_test.go +++ b/cmd/browsers_test.go @@ -682,8 +682,12 @@ func TestBrowsersCreate_WithProxyRoutes(t *testing.T) { }) assert.Contains(t, jsonOutput, `"proxy_routes"`) assert.Contains(t, jsonOutput, `"resolved-proxy"`) - assert.Empty(t, captured.Network.PrivateHosts) + assert.Nil(t, captured.Network.PrivateHosts) assert.Len(t, captured.Network.ProxyRoutes, 1) + raw, err = captured.MarshalJSON() + require.NoError(t, err) + assert.Contains(t, string(raw), `"proxy_routes"`) + assert.NotContains(t, string(raw), `"private_hosts"`) assert.Error(t, b.Create(context.Background(), BrowsersCreateInput{ProxyRoutes: []string{"host="}})) }