Skip to content

Commit af6beba

Browse files
authored
Merge branch 'main' into hypeship/1pw-fill-unknown-wording
2 parents 4c7f5c2 + a03ac09 commit af6beba

27 files changed

Lines changed: 738 additions & 88 deletions

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ Be mindful that these operations affect production resources.
2525
- The Makefile's `lint` target uses `|| true`, so it always exits 0 even when lint issues exist. Pre-existing lint warnings (errcheck, staticcheck) are present in the codebase and expected.
2626
- The `go-keyring` dependency requires D-Bus and `libsecret` on Linux. These are pre-installed in the Cloud VM.
2727
- `kernel create` works locally without authentication. Most other commands (`deploy`, `invoke`, `browsers`, etc.) require a `KERNEL_API_KEY` env var or `kernel login` OAuth flow.
28-
- Go module path is `github.com/kernel/cli`. The project requires Go 1.25.0 (specified in `go.mod`).
28+
- Go module path is `github.com/kernel/cli`. The project requires Go 1.27.2 (specified in `go.mod`).

‎DEVELOPMENT.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ brew install onkernel/tap/kernel
1212

1313
Install the following tools:
1414

15-
- Go 1.25+ ( https://go.dev/doc/install ); the required version is recorded in `go.mod`
15+
- Go 1.27+ ( https://go.dev/doc/install ); the required version is recorded in `go.mod`
1616
- [Goreleaser Pro](https://goreleaser.com/install/#pro) - **IMPORTANT: You must install goreleaser-pro, not the standard version, as this is required for our release process**
1717
- [chglog](https://github.com/goreleaser/chglog)
1818

‎README.md‎

Lines changed: 25 additions & 11 deletions
Large diffs are not rendered by default.

‎cmd/browser_pools.go‎

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ type BrowserPoolsCreateInput struct {
157157
ProxyID string
158158
Region string
159159
PrivateHosts []string
160+
ProxyRoutes []string
160161
StartURL string
161162
Extensions []string
162163
Viewport string
@@ -240,7 +241,14 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput)
240241
if err != nil {
241242
return err
242243
}
243-
if len(network.PrivateHosts) > 0 {
244+
routes, err := parseProxyRoutes(in.ProxyRoutes)
245+
if err != nil {
246+
return err
247+
}
248+
if len(routes) > 0 {
249+
network.ProxyRoutes = routes
250+
}
251+
if len(network.PrivateHosts) > 0 || len(network.ProxyRoutes) > 0 {
244252
params.Network = network
245253
}
246254

@@ -334,6 +342,7 @@ func (c BrowserPoolsCmd) Get(ctx context.Context, in BrowserPoolsGetInput) error
334342
{"Extensions", formatExtensions(cfg.Extensions)},
335343
{"Viewport", formatViewport(cfg.Viewport)},
336344
{"Private Hosts", formatPrivateHosts(cfg.Network)},
345+
{"Proxy Routes", formatProxyRoutes(cfg.Network)},
337346
{"Telemetry", formatPoolTelemetry(cfg.Telemetry)},
338347
}
339348

@@ -363,6 +372,8 @@ type BrowserPoolsUpdateInput struct {
363372
ClearExtensions bool
364373
PrivateHosts []string
365374
ClearPrivateHosts bool
375+
ProxyRoutes []string
376+
ClearProxyRoutes bool
366377
Viewport string
367378
ChromePolicy string
368379
ChromePolicyFile string
@@ -395,6 +406,9 @@ func validateBrowserPoolUpdateInput(in BrowserPoolsUpdateInput) error {
395406
if len(normalizePrivateHosts(in.PrivateHosts)) > 0 && in.ClearPrivateHosts {
396407
return fmt.Errorf("cannot specify both --private-host and --clear-private-hosts")
397408
}
409+
if len(in.ProxyRoutes) > 0 && in.ClearProxyRoutes {
410+
return fmt.Errorf("cannot specify both --proxy-route and --clear-proxy-routes")
411+
}
398412
return nil
399413
}
400414

@@ -494,7 +508,14 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput)
494508
if err != nil {
495509
return err
496510
}
497-
if len(network.PrivateHosts) > 0 {
511+
routes, err := parseProxyRoutes(in.ProxyRoutes)
512+
if err != nil {
513+
return err
514+
}
515+
if len(routes) > 0 {
516+
network.ProxyRoutes = routes
517+
}
518+
if len(network.PrivateHosts) > 0 || len(network.ProxyRoutes) > 0 {
498519
params.Network = network
499520
}
500521

@@ -509,6 +530,14 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput)
509530
}
510531
if in.ClearPrivateHosts {
511532
extraFields["network"] = map[string]any{}
533+
} else if in.ClearProxyRoutes {
534+
// The API replaces the whole network object, so keep any --private-host
535+
// entries alongside the explicit empty route list.
536+
clearRoutes := map[string]any{"proxy_routes": []any{}}
537+
if len(network.PrivateHosts) > 0 {
538+
clearRoutes["private_hosts"] = network.PrivateHosts
539+
}
540+
extraFields["network"] = clearRoutes
512541
}
513542
if len(extraFields) > 0 {
514543
params.SetExtraFields(extraFields)
@@ -811,6 +840,7 @@ func init() {
811840
browserPoolsCreateCmd.Flags().String("proxy-id", "", "Proxy ID")
812841
browserPoolsCreateCmd.Flags().String("region", "", "Geographic region for the pool: 'us-east', 'us-west', 'eu-west', or 'ap-southeast'. Fixed once the pool is created; requires a Start-Up or Enterprise plan and defaults to us-east")
813842
browserPoolsCreateCmd.Flags().StringSlice("private-host", nil, "Destinations browsers in the pool reach directly through their own network instead of Kernel-managed egress, for private hosts on a VPN or tunnel they join (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")
843+
browserPoolsCreateCmd.Flags().StringArray("proxy-route", nil, "Route HOST[,HOST...]=PROXY through a proxy for browsers in the pool (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-id or default egress. Requires proxy v3")
814844
browserPoolsCreateCmd.Flags().String("start-url", "", "Initial page to open for new browsers")
815845
browserPoolsCreateCmd.Flags().StringSlice("extension", []string{}, "Extension IDs or names")
816846
browserPoolsCreateCmd.Flags().String("viewport", "", "Viewport size (e.g. 1280x800)")
@@ -841,13 +871,18 @@ func init() {
841871
browserPoolsUpdateCmd.Flags().StringSlice("extension", []string{}, "Extension IDs or names")
842872
browserPoolsUpdateCmd.Flags().Bool("clear-extensions", false, "Remove all pool extensions")
843873
browserPoolsUpdateCmd.Flags().StringSlice("private-host", nil, "Replace the destinations browsers in the pool reach directly through their own network instead of Kernel-managed egress (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'). Only applies to browsers created after the update")
844-
browserPoolsUpdateCmd.Flags().Bool("clear-private-hosts", false, "Remove the private-host override and restore the default private ranges")
874+
browserPoolsUpdateCmd.Flags().Bool("clear-private-hosts", false, "Remove the pool's network configuration (private-host override and proxy routes) and restore the default private ranges")
875+
browserPoolsUpdateCmd.Flags().StringArray("proxy-route", nil, "Replace the pool's proxy routes with HOST[,HOST...]=PROXY (repeatable, max 10 routes and 50 hosts per route). PROXY is an ID by default; use id:ID or name:NAME explicitly. Network flags replace the pool's whole network configuration, so pass --private-host too to keep a private-host override. Only applies to browsers created after the update. Requires proxy v3")
876+
browserPoolsUpdateCmd.Flags().Bool("clear-proxy-routes", false, "Remove the pool's proxy routes (combine with --private-host to keep a private-host override)")
845877
browserPoolsUpdateCmd.Flags().String("viewport", "", "Viewport size (e.g. 1280x800)")
846878
browserPoolsUpdateCmd.Flags().String("chrome-policy", "", "Custom Chrome enterprise policy as a JSON object")
847879
browserPoolsUpdateCmd.Flags().String("chrome-policy-file", "", "Read Chrome enterprise policy (JSON object) from a file (use '-' for stdin)")
848880
browserPoolsUpdateCmd.Flags().Bool("clear-chrome-policy", false, "Remove the pool's custom Chrome enterprise policy")
849881
browserPoolsUpdateCmd.MarkFlagsMutuallyExclusive("chrome-policy", "chrome-policy-file")
850882
browserPoolsUpdateCmd.MarkFlagsMutuallyExclusive("private-host", "clear-private-hosts")
883+
browserPoolsUpdateCmd.MarkFlagsMutuallyExclusive("proxy-route", "clear-proxy-routes")
884+
browserPoolsUpdateCmd.MarkFlagsMutuallyExclusive("proxy-route", "clear-private-hosts")
885+
browserPoolsUpdateCmd.MarkFlagsMutuallyExclusive("clear-proxy-routes", "clear-private-hosts")
851886
browserPoolsUpdateCmd.Flags().String("telemetry", "", "Update pool telemetry: --telemetry=all (reset to default set), --telemetry=off (disable), or --telemetry=console,network (merge those categories into the current selection). Applies only to browsers warmed after the update.")
852887
browserPoolsUpdateCmd.Flags().String("telemetry-cdp-exclude", "", "Leave the named CDP methods out of control telemetry's cdp_command events, comma-separated (e.g. Input.dispatchMouseEvent,Page.captureScreenshot); --telemetry-cdp-exclude=none clears the list. Excluded commands are still relayed to the browser, they just produce no event")
853888
browserPoolsUpdateCmd.Flags().Bool("discard-all-idle", false, "Discard all idle browsers")
@@ -915,6 +950,7 @@ func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error {
915950
proxyID, _ := cmd.Flags().GetString("proxy-id")
916951
region, _ := cmd.Flags().GetString("region")
917952
privateHosts, _ := cmd.Flags().GetStringSlice("private-host")
953+
proxyRoutes, _ := cmd.Flags().GetStringArray("proxy-route")
918954
startURL, _ := cmd.Flags().GetString("start-url")
919955
extensions, _ := cmd.Flags().GetStringSlice("extension")
920956
viewport, _ := cmd.Flags().GetString("viewport")
@@ -939,6 +975,7 @@ func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error {
939975
ProxyID: proxyID,
940976
Region: region,
941977
PrivateHosts: privateHosts,
978+
ProxyRoutes: proxyRoutes,
942979
StartURL: startURL,
943980
Extensions: extensions,
944981
Viewport: viewport,
@@ -983,6 +1020,8 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error {
9831020
clearExtensions, _ := cmd.Flags().GetBool("clear-extensions")
9841021
privateHosts, _ := cmd.Flags().GetStringSlice("private-host")
9851022
clearPrivateHosts, _ := cmd.Flags().GetBool("clear-private-hosts")
1023+
proxyRoutes, _ := cmd.Flags().GetStringArray("proxy-route")
1024+
clearProxyRoutes, _ := cmd.Flags().GetBool("clear-proxy-routes")
9861025
viewport, _ := cmd.Flags().GetString("viewport")
9871026
chromePolicy, _ := cmd.Flags().GetString("chrome-policy")
9881027
chromePolicyFile, _ := cmd.Flags().GetString("chrome-policy-file")
@@ -1014,6 +1053,8 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error {
10141053
ClearExtensions: clearExtensions,
10151054
PrivateHosts: privateHosts,
10161055
ClearPrivateHosts: clearPrivateHosts,
1056+
ProxyRoutes: proxyRoutes,
1057+
ClearProxyRoutes: clearProxyRoutes,
10171058
Viewport: viewport,
10181059
ChromePolicy: chromePolicy,
10191060
ChromePolicyFile: chromePolicyFile,

‎cmd/browser_pools_test.go‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,11 @@ func TestBrowserPoolsUpdate_RejectsInvalidDurableInputs(t *testing.T) {
602602
input: BrowserPoolsUpdateInput{PrivateHosts: []string{"internal.example"}, ClearPrivateHosts: true},
603603
wantErr: "cannot specify both --private-host and --clear-private-hosts",
604604
},
605+
{
606+
name: "conflicting proxy route modes",
607+
input: BrowserPoolsUpdateInput{ProxyRoutes: []string{"a.example=proxy-1"}, ClearProxyRoutes: true},
608+
wantErr: "cannot specify both --proxy-route and --clear-proxy-routes",
609+
},
605610
}
606611

607612
for _, tt := range tests {
@@ -639,6 +644,48 @@ func TestBrowserPoolsCreate_WithPrivateHosts(t *testing.T) {
639644
assert.Equal(t, []string{"*.example.ts.net", "100.64.0.0/10"}, captured.Network.PrivateHosts)
640645
}
641646

647+
func TestBrowserPoolsCreate_WithProxyRoutes(t *testing.T) {
648+
setupStdoutCapture(t)
649+
650+
var captured kernel.BrowserPoolNewParams
651+
fake := &FakeBrowserPoolsService{
652+
NewFunc: func(ctx context.Context, body kernel.BrowserPoolNewParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) {
653+
captured = body
654+
return &kernel.BrowserPool{ID: "pool-routes"}, nil
655+
},
656+
}
657+
c := BrowserPoolsCmd{client: fake}
658+
659+
require.NoError(t, c.Create(context.Background(), BrowserPoolsCreateInput{
660+
Size: 1,
661+
ProxyRoutes: []string{"api.ipify.org,*.ipify.org=name:my-dc-proxy", "other.example=proxy-456"},
662+
}))
663+
require.Len(t, captured.Network.ProxyRoutes, 2)
664+
assert.Equal(t, []string{"api.ipify.org", "*.ipify.org"}, captured.Network.ProxyRoutes[0].Hosts)
665+
assert.Equal(t, "my-dc-proxy", captured.Network.ProxyRoutes[0].Proxy.Name.Value)
666+
assert.Equal(t, "proxy-456", captured.Network.ProxyRoutes[1].Proxy.ID.Value)
667+
assert.Empty(t, captured.Network.PrivateHosts)
668+
669+
assert.Error(t, c.Create(context.Background(), BrowserPoolsCreateInput{Size: 1, ProxyRoutes: []string{"host="}}))
670+
}
671+
672+
func TestBrowserPoolsGet_ShowsProxyRoutes(t *testing.T) {
673+
setupStdoutCapture(t)
674+
675+
fake := &FakeBrowserPoolsService{
676+
GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.BrowserPool, error) {
677+
var pool kernel.BrowserPool
678+
err := json.Unmarshal([]byte(`{"id":"pool-1","browser_pool_config":{"size":1,"network":{"proxy_routes":[{"hosts":["*.ipify.org"],"proxy":{"name":"my-dc-proxy"}}]}}}`), &pool)
679+
return &pool, err
680+
},
681+
}
682+
require.NoError(t, (BrowserPoolsCmd{client: fake}).Get(context.Background(), BrowserPoolsGetInput{IDOrName: "pool-1"}))
683+
684+
out := outBuf.String()
685+
assert.Contains(t, out, "Proxy Routes")
686+
assert.Contains(t, out, "*.ipify.org = my-dc-proxy")
687+
}
688+
642689
func TestBrowserPoolsUpdate_PrivateHostModes(t *testing.T) {
643690
tests := []struct {
644691
name string
@@ -655,6 +702,26 @@ func TestBrowserPoolsUpdate_PrivateHostModes(t *testing.T) {
655702
input: BrowserPoolsUpdateInput{ClearPrivateHosts: true},
656703
wantJSON: `"network":{}`,
657704
},
705+
{
706+
name: "replace proxy routes",
707+
input: BrowserPoolsUpdateInput{ProxyRoutes: []string{"*.ipify.org=name:dc"}},
708+
wantJSON: `"network":{"proxy_routes":[{"hosts":["*.ipify.org"],"proxy":{"name":"dc"}}]}`,
709+
},
710+
{
711+
name: "routes with private hosts",
712+
input: BrowserPoolsUpdateInput{PrivateHosts: []string{"10.0.0.0/8"}, ProxyRoutes: []string{"a.example=proxy-1"}},
713+
wantJSON: `"network":{"private_hosts":["10.0.0.0/8"],"proxy_routes":[{"hosts":["a.example"],"proxy":{"id":"proxy-1"}}]}`,
714+
},
715+
{
716+
name: "clear proxy routes",
717+
input: BrowserPoolsUpdateInput{ClearProxyRoutes: true},
718+
wantJSON: `"network":{"proxy_routes":[]}`,
719+
},
720+
{
721+
name: "clear proxy routes keeps private hosts",
722+
input: BrowserPoolsUpdateInput{PrivateHosts: []string{"10.0.0.0/8"}, ClearProxyRoutes: true},
723+
wantJSON: `"network":{"private_hosts":["10.0.0.0/8"],"proxy_routes":[]}`,
724+
},
658725
}
659726

660727
for _, tt := range tests {

0 commit comments

Comments
 (0)