From a7716d902cedc5f88ba6a3173191f4768d8e9df0 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:35:53 +0000 Subject: [PATCH 1/2] CLI: Update SDK to 9e90177b921114c93e264ca9792537bf2d8de754 and add missing flags Keep the CLI aligned with the latest kernel-go-sdk release while exposing browser process env/TTY options and browser pool chrome policy support that were already present in the SDK. Tested: go test ./cmd/... && go build ./... Tested: kernel browsers process exec --env Tested: kernel browsers process spawn --allocate-tty --cols --rows --env Tested: kernel browser-pools create/update --chrome-policy Made-with: Cursor --- cmd/browser_pools.go | 56 +++++++++++++++++++++++ cmd/browser_pools_test.go | 72 ++++++++++++++++++++++++++++++ cmd/browsers.go | 93 ++++++++++++++++++++++++++++++++++----- cmd/browsers_test.go | 47 ++++++++++++++++++++ go.mod | 2 +- go.sum | 4 +- 6 files changed, 261 insertions(+), 13 deletions(-) create mode 100644 cmd/browser_pools_test.go diff --git a/cmd/browser_pools.go b/cmd/browser_pools.go index fbb63f8..f445ac3 100644 --- a/cmd/browser_pools.go +++ b/cmd/browser_pools.go @@ -2,6 +2,7 @@ package cmd import ( "context" + "encoding/json" "fmt" "strings" @@ -86,6 +87,7 @@ type BrowserPoolsCreateInput struct { ProfileName string ProfileSaveChanges BoolFlag ProxyID string + ChromePolicy string Extensions []string Viewport string Output string @@ -131,6 +133,14 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput) if in.ProxyID != "" { params.ProxyID = kernel.String(in.ProxyID) } + chromePolicy, err := parseChromePolicy(in.ChromePolicy) + if err != nil { + pterm.Error.Println(err.Error()) + return nil + } + if len(chromePolicy) > 0 { + params.ChromePolicy = chromePolicy + } params.Extensions = buildExtensionsParam(in.Extensions) @@ -196,6 +206,7 @@ func (c BrowserPoolsCmd) Get(ctx context.Context, in BrowserPoolsGetInput) error {"Kiosk Mode", fmt.Sprintf("%t", cfg.KioskMode)}, {"Profile", formatProfile(cfg.Profile)}, {"Proxy ID", util.OrDash(cfg.ProxyID)}, + {"Chrome Policy", formatChromePolicy(cfg.ChromePolicy)}, {"Extensions", formatExtensions(cfg.Extensions)}, {"Viewport", formatViewport(cfg.Viewport)}, } @@ -217,6 +228,7 @@ type BrowserPoolsUpdateInput struct { ProfileName string ProfileSaveChanges BoolFlag ProxyID string + ChromePolicy string Extensions []string Viewport string DiscardAllIdle BoolFlag @@ -267,6 +279,14 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput) if in.ProxyID != "" { params.ProxyID = kernel.String(in.ProxyID) } + chromePolicy, err := parseChromePolicy(in.ChromePolicy) + if err != nil { + pterm.Error.Println(err.Error()) + return nil + } + if len(chromePolicy) > 0 { + params.ChromePolicy = chromePolicy + } params.Extensions = buildExtensionsParam(in.Extensions) @@ -472,6 +492,7 @@ func init() { browserPoolsCreateCmd.Flags().String("profile-name", "", "Profile name") browserPoolsCreateCmd.Flags().Bool("save-changes", false, "Save changes to profile") browserPoolsCreateCmd.Flags().String("proxy-id", "", "Proxy ID") + browserPoolsCreateCmd.Flags().String("chrome-policy", "", "JSON object of Chrome enterprise policy overrides to apply to all browsers in the pool") browserPoolsCreateCmd.Flags().StringSlice("extension", []string{}, "Extension IDs or names") browserPoolsCreateCmd.Flags().String("viewport", "", "Viewport size (e.g. 1280x800)") @@ -488,6 +509,7 @@ func init() { browserPoolsUpdateCmd.Flags().String("profile-name", "", "Profile name") browserPoolsUpdateCmd.Flags().Bool("save-changes", false, "Save changes to profile") browserPoolsUpdateCmd.Flags().String("proxy-id", "", "Proxy ID") + browserPoolsUpdateCmd.Flags().String("chrome-policy", "", "JSON object of Chrome enterprise policy overrides to apply to all browsers in the pool") browserPoolsUpdateCmd.Flags().StringSlice("extension", []string{}, "Extension IDs or names") browserPoolsUpdateCmd.Flags().String("viewport", "", "Viewport size (e.g. 1280x800)") browserPoolsUpdateCmd.Flags().Bool("discard-all-idle", false, "Discard all idle browsers") @@ -539,6 +561,7 @@ func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error { profileName, _ := cmd.Flags().GetString("profile-name") saveChanges, _ := cmd.Flags().GetBool("save-changes") proxyID, _ := cmd.Flags().GetString("proxy-id") + chromePolicy, _ := cmd.Flags().GetString("chrome-policy") extensions, _ := cmd.Flags().GetStringSlice("extension") viewport, _ := cmd.Flags().GetString("viewport") output, _ := cmd.Flags().GetString("output") @@ -555,6 +578,7 @@ func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error { ProfileName: profileName, ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges}, ProxyID: proxyID, + ChromePolicy: chromePolicy, Extensions: extensions, Viewport: viewport, Output: output, @@ -585,6 +609,7 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error { profileName, _ := cmd.Flags().GetString("profile-name") saveChanges, _ := cmd.Flags().GetBool("save-changes") proxyID, _ := cmd.Flags().GetString("proxy-id") + chromePolicy, _ := cmd.Flags().GetString("chrome-policy") extensions, _ := cmd.Flags().GetStringSlice("extension") viewport, _ := cmd.Flags().GetString("viewport") discardIdle, _ := cmd.Flags().GetBool("discard-all-idle") @@ -603,6 +628,7 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error { ProfileName: profileName, ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges}, ProxyID: proxyID, + ChromePolicy: chromePolicy, Extensions: extensions, Viewport: viewport, DiscardAllIdle: BoolFlag{Set: cmd.Flags().Changed("discard-all-idle"), Value: discardIdle}, @@ -687,6 +713,23 @@ func buildExtensionsParam(extensions []string) []kernel.BrowserExtensionParam { return result } +func parseChromePolicy(raw string) (map[string]any, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil, nil + } + + var policy map[string]any + if err := json.Unmarshal([]byte(raw), &policy); err != nil { + return nil, fmt.Errorf("invalid --chrome-policy JSON: %w", err) + } + if policy == nil { + return nil, fmt.Errorf("--chrome-policy must be a JSON object") + } + + return policy, nil +} + func buildViewportParam(viewport string) (*kernel.BrowserViewportParam, error) { if viewport == "" { return nil, nil @@ -735,6 +778,19 @@ func formatExtensions(extensions []kernel.BrowserExtension) string { return util.JoinOrDash(names...) } +func formatChromePolicy(policy map[string]any) string { + if len(policy) == 0 { + return "-" + } + + data, err := json.Marshal(policy) + if err != nil { + return fmt.Sprintf("%v", policy) + } + + return string(data) +} + func formatViewport(viewport kernel.BrowserViewport) string { if viewport.Width == 0 || viewport.Height == 0 { return "-" diff --git a/cmd/browser_pools_test.go b/cmd/browser_pools_test.go new file mode 100644 index 0000000..826ba65 --- /dev/null +++ b/cmd/browser_pools_test.go @@ -0,0 +1,72 @@ +package cmd + +import ( + "context" + "testing" + + "github.com/kernel/kernel-go-sdk" + "github.com/kernel/kernel-go-sdk/option" + "github.com/stretchr/testify/assert" +) + +type fakeBrowserPoolsService struct { + newFunc func(ctx context.Context, body kernel.BrowserPoolNewParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) +} + +func (f *fakeBrowserPoolsService) List(ctx context.Context, opts ...option.RequestOption) (*[]kernel.BrowserPool, error) { + return &[]kernel.BrowserPool{}, nil +} + +func (f *fakeBrowserPoolsService) New(ctx context.Context, body kernel.BrowserPoolNewParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) { + if f.newFunc != nil { + return f.newFunc(ctx, body, opts...) + } + return &kernel.BrowserPool{}, nil +} + +func (f *fakeBrowserPoolsService) Get(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.BrowserPool, error) { + return &kernel.BrowserPool{}, nil +} + +func (f *fakeBrowserPoolsService) Update(ctx context.Context, id string, body kernel.BrowserPoolUpdateParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) { + return &kernel.BrowserPool{}, nil +} + +func (f *fakeBrowserPoolsService) Delete(ctx context.Context, id string, body kernel.BrowserPoolDeleteParams, opts ...option.RequestOption) error { + return nil +} + +func (f *fakeBrowserPoolsService) Acquire(ctx context.Context, id string, body kernel.BrowserPoolAcquireParams, opts ...option.RequestOption) (*kernel.BrowserPoolAcquireResponse, error) { + return &kernel.BrowserPoolAcquireResponse{}, nil +} + +func (f *fakeBrowserPoolsService) Release(ctx context.Context, id string, body kernel.BrowserPoolReleaseParams, opts ...option.RequestOption) error { + return nil +} + +func (f *fakeBrowserPoolsService) Flush(ctx context.Context, id string, opts ...option.RequestOption) error { + return nil +} + +func TestBrowserPoolsCreate_MapsChromePolicy(t *testing.T) { + setupStdoutCapture(t) + + fake := &fakeBrowserPoolsService{ + newFunc: func(ctx context.Context, body kernel.BrowserPoolNewParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) { + assert.Equal(t, map[string]any{ + "HomepageLocation": "https://example.com", + "ShowHomeButton": true, + }, body.ChromePolicy) + return &kernel.BrowserPool{ID: "pool_123", Name: "test-pool"}, nil + }, + } + + cmd := BrowserPoolsCmd{client: fake} + err := cmd.Create(context.Background(), BrowserPoolsCreateInput{ + Name: "test-pool", + Size: 1, + ChromePolicy: `{"HomepageLocation":"https://example.com","ShowHomeButton":true}`, + }) + + assert.NoError(t, err) +} diff --git a/cmd/browsers.go b/cmd/browsers.go index d799667..9103082 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -170,6 +170,23 @@ func parseViewport(viewport string) (width, height, refreshRate int64, err error return w, h, refreshRate, nil } +func parseStringMapFlag(values []string, flagName string) (map[string]string, error) { + if len(values) == 0 { + return nil, nil + } + + parsed := make(map[string]string, len(values)) + for _, pair := range values { + parts := strings.SplitN(pair, "=", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid %s value: %s (expected KEY=value)", flagName, pair) + } + parsed[parts[0]] = parts[1] + } + + return parsed, nil +} + // Inputs for each command type BrowsersCreateInput struct { PersistenceID string @@ -1257,18 +1274,23 @@ type BrowsersProcessExecInput struct { Timeout int AsUser string AsRoot BoolFlag + Env []string Output string } type BrowsersProcessSpawnInput struct { - Identifier string - Command string - Args []string - Cwd string - Timeout int - AsUser string - AsRoot BoolFlag - Output string + Identifier string + Command string + Args []string + Cwd string + Timeout int + AsUser string + AsRoot BoolFlag + AllocateTTY BoolFlag + Cols int64 + Rows int64 + Env []string + Output string } type BrowsersProcessKillInput struct { @@ -1396,6 +1418,13 @@ func (b BrowsersCmd) ProcessExec(ctx context.Context, in BrowsersProcessExecInpu if in.AsRoot.Set { params.AsRoot = kernel.Opt(in.AsRoot.Value) } + env, err := parseStringMapFlag(in.Env, "--env") + if err != nil { + return err + } + if len(env) > 0 { + params.Env = env + } res, err := b.process.Exec(ctx, br.SessionID, params) if err != nil { return util.CleanedUpSdkError{Err: err} @@ -1463,6 +1492,27 @@ func (b BrowsersCmd) ProcessSpawn(ctx context.Context, in BrowsersProcessSpawnIn if in.AsRoot.Set { params.AsRoot = kernel.Opt(in.AsRoot.Value) } + if in.AllocateTTY.Set { + params.AllocateTty = kernel.Opt(in.AllocateTTY.Value) + } + if in.Cols > 0 || in.Rows > 0 { + if !in.AllocateTTY.Set || !in.AllocateTTY.Value { + return fmt.Errorf("--cols and --rows require --allocate-tty") + } + if in.Cols > 0 { + params.Cols = kernel.Opt(in.Cols) + } + if in.Rows > 0 { + params.Rows = kernel.Opt(in.Rows) + } + } + env, err := parseStringMapFlag(in.Env, "--env") + if err != nil { + return err + } + if len(env) > 0 { + params.Env = env + } res, err := b.process.Spawn(ctx, br.SessionID, params) if err != nil { return util.CleanedUpSdkError{Err: err} @@ -2297,6 +2347,7 @@ func init() { procExec.Flags().Int("timeout", 0, "Timeout in seconds") procExec.Flags().String("as-user", "", "Run as user") procExec.Flags().Bool("as-root", false, "Run as root") + procExec.Flags().StringArray("env", nil, "Environment variable in KEY=value format (repeatable)") procExec.Flags().StringP("output", "o", "", "Output format: json for raw API response") procSpawn := &cobra.Command{Use: "spawn [--] [command...]", Short: "Execute a command asynchronously", Args: cobra.MinimumNArgs(1), RunE: runBrowsersProcessSpawn} procSpawn.Flags().String("command", "", "Command to execute (optional; if omitted, trailing args are executed via /bin/bash -c)") @@ -2305,6 +2356,10 @@ func init() { procSpawn.Flags().Int("timeout", 0, "Timeout in seconds") procSpawn.Flags().String("as-user", "", "Run as user") procSpawn.Flags().Bool("as-root", false, "Run as root") + procSpawn.Flags().Bool("allocate-tty", false, "Allocate a pseudo-terminal (PTY) for interactive shells") + procSpawn.Flags().Int64("cols", 0, "Initial terminal columns when --allocate-tty is enabled") + procSpawn.Flags().Int64("rows", 0, "Initial terminal rows when --allocate-tty is enabled") + procSpawn.Flags().StringArray("env", nil, "Environment variable in KEY=value format (repeatable)") procSpawn.Flags().StringP("output", "o", "", "Output format: json for raw API response") procKill := &cobra.Command{Use: "kill ", Short: "Send a signal to a process", Args: cobra.ExactArgs(2), RunE: runBrowsersProcessKill} procKill.Flags().String("signal", "TERM", "Signal to send (TERM, KILL, INT, HUP)") @@ -2806,6 +2861,7 @@ func runBrowsersProcessExec(cmd *cobra.Command, args []string) error { timeout, _ := cmd.Flags().GetInt("timeout") asUser, _ := cmd.Flags().GetString("as-user") asRoot, _ := cmd.Flags().GetBool("as-root") + env, _ := cmd.Flags().GetStringArray("env") if command == "" && len(args) > 1 { // Treat trailing args after identifier as a shell command shellCmd := strings.Join(args[1:], " ") @@ -2814,7 +2870,7 @@ func runBrowsersProcessExec(cmd *cobra.Command, args []string) error { } output, _ := cmd.Flags().GetString("output") b := BrowsersCmd{browsers: &svc, process: &svc.Process} - return b.ProcessExec(cmd.Context(), BrowsersProcessExecInput{Identifier: args[0], Command: command, Args: argv, Cwd: cwd, Timeout: timeout, AsUser: asUser, AsRoot: BoolFlag{Set: cmd.Flags().Changed("as-root"), Value: asRoot}, Output: output}) + return b.ProcessExec(cmd.Context(), BrowsersProcessExecInput{Identifier: args[0], Command: command, Args: argv, Cwd: cwd, Timeout: timeout, AsUser: asUser, AsRoot: BoolFlag{Set: cmd.Flags().Changed("as-root"), Value: asRoot}, Env: env, Output: output}) } func runBrowsersProcessSpawn(cmd *cobra.Command, args []string) error { @@ -2826,6 +2882,10 @@ func runBrowsersProcessSpawn(cmd *cobra.Command, args []string) error { timeout, _ := cmd.Flags().GetInt("timeout") asUser, _ := cmd.Flags().GetString("as-user") asRoot, _ := cmd.Flags().GetBool("as-root") + allocateTTY, _ := cmd.Flags().GetBool("allocate-tty") + cols, _ := cmd.Flags().GetInt64("cols") + rows, _ := cmd.Flags().GetInt64("rows") + env, _ := cmd.Flags().GetStringArray("env") if command == "" && len(args) > 1 { shellCmd := strings.Join(args[1:], " ") command = "/bin/bash" @@ -2833,7 +2893,20 @@ func runBrowsersProcessSpawn(cmd *cobra.Command, args []string) error { } output, _ := cmd.Flags().GetString("output") b := BrowsersCmd{browsers: &svc, process: &svc.Process} - return b.ProcessSpawn(cmd.Context(), BrowsersProcessSpawnInput{Identifier: args[0], Command: command, Args: argv, Cwd: cwd, Timeout: timeout, AsUser: asUser, AsRoot: BoolFlag{Set: cmd.Flags().Changed("as-root"), Value: asRoot}, Output: output}) + return b.ProcessSpawn(cmd.Context(), BrowsersProcessSpawnInput{ + Identifier: args[0], + Command: command, + Args: argv, + Cwd: cwd, + Timeout: timeout, + AsUser: asUser, + AsRoot: BoolFlag{Set: cmd.Flags().Changed("as-root"), Value: asRoot}, + AllocateTTY: BoolFlag{Set: cmd.Flags().Changed("allocate-tty"), Value: allocateTTY}, + Cols: cols, + Rows: rows, + Env: env, + Output: output, + }) } func runBrowsersProcessKill(cmd *cobra.Command, args []string) error { diff --git a/cmd/browsers_test.go b/cmd/browsers_test.go index 2bb2c71..125179c 100644 --- a/cmd/browsers_test.go +++ b/cmd/browsers_test.go @@ -907,6 +907,25 @@ func TestBrowsersProcessExec_PrintsSummary(t *testing.T) { assert.Contains(t, out, "Duration") } +func TestBrowsersProcessExec_MapsEnv(t *testing.T) { + setupStdoutCapture(t) + fake := &FakeProcessService{ + ExecFunc: func(ctx context.Context, id string, body kernel.BrowserProcessExecParams, opts ...option.RequestOption) (*kernel.BrowserProcessExecResponse, error) { + assert.Equal(t, "id", id) + assert.Equal(t, map[string]string{"FOO": "bar", "HELLO": "world"}, body.Env) + return &kernel.BrowserProcessExecResponse{ExitCode: 0, DurationMs: 10}, nil + }, + } + fakeBrowsers := newFakeBrowsersServiceWithSimpleGet() + b := BrowsersCmd{browsers: fakeBrowsers, process: fake} + err := b.ProcessExec(context.Background(), BrowsersProcessExecInput{ + Identifier: "id", + Command: "env", + Env: []string{"FOO=bar", "HELLO=world"}, + }) + assert.NoError(t, err) +} + func TestBrowsersProcessSpawn_PrintsInfo(t *testing.T) { setupStdoutCapture(t) fake := &FakeProcessService{} @@ -918,6 +937,34 @@ func TestBrowsersProcessSpawn_PrintsInfo(t *testing.T) { assert.Contains(t, out, "PID") } +func TestBrowsersProcessSpawn_MapsTTYAndEnv(t *testing.T) { + setupStdoutCapture(t) + fake := &FakeProcessService{ + SpawnFunc: func(ctx context.Context, id string, body kernel.BrowserProcessSpawnParams, opts ...option.RequestOption) (*kernel.BrowserProcessSpawnResponse, error) { + assert.Equal(t, "id", id) + assert.True(t, body.AllocateTty.Valid()) + assert.True(t, body.AllocateTty.Value) + assert.True(t, body.Cols.Valid()) + assert.Equal(t, int64(120), body.Cols.Value) + assert.True(t, body.Rows.Valid()) + assert.Equal(t, int64(40), body.Rows.Value) + assert.Equal(t, map[string]string{"FOO": "bar"}, body.Env) + return &kernel.BrowserProcessSpawnResponse{ProcessID: "proc-1", Pid: 123, StartedAt: time.Now()}, nil + }, + } + fakeBrowsers := newFakeBrowsersServiceWithSimpleGet() + b := BrowsersCmd{browsers: fakeBrowsers, process: fake} + err := b.ProcessSpawn(context.Background(), BrowsersProcessSpawnInput{ + Identifier: "id", + Command: "bash", + AllocateTTY: BoolFlag{Set: true, Value: true}, + Cols: 120, + Rows: 40, + Env: []string{"FOO=bar"}, + }) + assert.NoError(t, err) +} + func TestBrowsersProcessKill_PrintsSuccess(t *testing.T) { setupStdoutCapture(t) fake := &FakeProcessService{} diff --git a/go.mod b/go.mod index bbc599b..b826424 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.44.1-0.20260323174449-5e56fc5d99a6 + github.com/kernel/kernel-go-sdk v0.45.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 github.com/samber/lo v1.51.0 diff --git a/go.sum b/go.sum index 2c777ed..d7c31ef 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.44.1-0.20260323174449-5e56fc5d99a6 h1:RBlGCN3IagI0b+XrWsb5FOUV/18tniuL6oHFAb7MMHE= -github.com/kernel/kernel-go-sdk v0.44.1-0.20260323174449-5e56fc5d99a6/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.45.0 h1:RIFpSDmhAWllo692FZL3Os3TRce5oHvyj8LPfwXce5Y= +github.com/kernel/kernel-go-sdk v0.45.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= From 26d22ee28767fcd6d72d33e9a5997b6d041ff450 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 19:05:05 +0000 Subject: [PATCH 2/2] CLI: Update SDK to 91f2aa6572a40330669e39ec4d40cd0b1ee75812 and add missing flags Align the CLI with the latest kernel-go-sdk by exposing browser default stealth proxy control and the new proxy health check URL parameter. This also updates the CLI dependency to the SDK release that includes these API changes. Tested: go test ./cmd ./cmd/proxies -run 'TestBrowsersUpdate_|TestProxyCheck_' Tested: go build ./... Tested: /tmp/kernel-cli/bin/kernel browsers create --headless --stealth -t 30 -o json Tested: /tmp/kernel-cli/bin/kernel browsers update --disable-default-proxy -o json Tested: /tmp/kernel-cli/bin/kernel proxies create --type datacenter --country US --name -o json Tested: /tmp/kernel-cli/bin/kernel proxies check --url https://example.com -o json Made-with: Cursor --- cmd/browsers.go | 48 ++++++++++++++++++++++---------------- cmd/browsers_test.go | 19 +++++++++++++++ cmd/proxies/check.go | 10 ++++++-- cmd/proxies/check_test.go | 27 ++++++++++++++++++++- cmd/proxies/common_test.go | 6 ++--- cmd/proxies/proxies.go | 3 ++- cmd/proxies/types.go | 3 ++- go.mod | 2 +- go.sum | 4 ++-- 9 files changed, 91 insertions(+), 31 deletions(-) diff --git a/cmd/browsers.go b/cmd/browsers.go index 9103082..22c3a46 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -221,15 +221,16 @@ type BrowsersGetInput struct { } type BrowsersUpdateInput struct { - Identifier string - ProxyID string - ClearProxy bool - ProfileID string - ProfileName string - ProfileSaveChanges BoolFlag - Viewport string - Force bool - Output string + Identifier string + ProxyID string + ClearProxy bool + DisableDefaultProxy BoolFlag + ProfileID string + ProfileName string + ProfileSaveChanges BoolFlag + Viewport string + Force bool + Output string } // BrowsersCmd is a cobra-independent command handler for browsers operations. @@ -610,7 +611,7 @@ func (b BrowsersCmd) Update(ctx context.Context, in BrowsersUpdateInput) error { return fmt.Errorf("cannot specify both --proxy-id and --clear-proxy") } - hasProxyChange := in.ProxyID != "" || in.ClearProxy + hasProxyChange := in.ProxyID != "" || in.ClearProxy || in.DisableDefaultProxy.Set hasProfileChange := in.ProfileID != "" || in.ProfileName != "" hasViewportChange := in.Viewport != "" @@ -626,7 +627,7 @@ func (b BrowsersCmd) Update(ctx context.Context, in BrowsersUpdateInput) error { // Validate that at least one update option is provided if !hasProxyChange && !hasProfileChange && !hasViewportChange { - return fmt.Errorf("must specify at least one of: --proxy-id, --clear-proxy, --profile-id, --profile-name, or --viewport") + return fmt.Errorf("must specify at least one of: --proxy-id, --clear-proxy, --disable-default-proxy, --profile-id, --profile-name, or --viewport") } params := kernel.BrowserUpdateParams{} @@ -637,6 +638,9 @@ func (b BrowsersCmd) Update(ctx context.Context, in BrowsersUpdateInput) error { } else if in.ProxyID != "" { params.ProxyID = kernel.Opt(in.ProxyID) } + if in.DisableDefaultProxy.Set { + params.DisableDefaultProxy = kernel.Opt(in.DisableDefaultProxy.Value) + } // Handle profile changes if hasProfileChange { @@ -2260,6 +2264,7 @@ var browsersUpdateCmd = &cobra.Command{ Supported operations: - Change or remove proxy (--proxy-id or --clear-proxy) + - Disable the default stealth proxy (--disable-default-proxy) - Load a profile into a session that doesn't have one (--profile-id or --profile-name) - Change viewport dimensions (--viewport) - Force viewport resize during active live view or recording (--force with --viewport) @@ -2297,6 +2302,7 @@ func init() { browsersUpdateCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") browsersUpdateCmd.Flags().String("proxy-id", "", "ID of the proxy to use for the browser session") browsersUpdateCmd.Flags().Bool("clear-proxy", false, "Remove the proxy from the browser session") + browsersUpdateCmd.Flags().Bool("disable-default-proxy", false, "Disable the default stealth proxy so the browser connects directly; use --disable-default-proxy=false to re-enable it") browsersUpdateCmd.Flags().String("profile-id", "", "Profile ID to load into the browser session (mutually exclusive with --profile-name)") browsersUpdateCmd.Flags().String("profile-name", "", "Profile name to load into the browser session (mutually exclusive with --profile-id)") browsersUpdateCmd.Flags().Bool("save-changes", false, "If set, save changes back to the profile when the session ends") @@ -2781,6 +2787,7 @@ func runBrowsersUpdate(cmd *cobra.Command, args []string) error { out, _ := cmd.Flags().GetString("output") proxyID, _ := cmd.Flags().GetString("proxy-id") clearProxy, _ := cmd.Flags().GetBool("clear-proxy") + disableDefaultProxy, _ := cmd.Flags().GetBool("disable-default-proxy") profileID, _ := cmd.Flags().GetString("profile-id") profileName, _ := cmd.Flags().GetString("profile-name") saveChanges, _ := cmd.Flags().GetBool("save-changes") @@ -2790,15 +2797,16 @@ func runBrowsersUpdate(cmd *cobra.Command, args []string) error { svc := client.Browsers b := BrowsersCmd{browsers: &svc} return b.Update(cmd.Context(), BrowsersUpdateInput{ - Identifier: args[0], - ProxyID: proxyID, - ClearProxy: clearProxy, - ProfileID: profileID, - ProfileName: profileName, - ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges}, - Viewport: viewport, - Force: force, - Output: out, + Identifier: args[0], + ProxyID: proxyID, + ClearProxy: clearProxy, + DisableDefaultProxy: BoolFlag{Set: cmd.Flags().Changed("disable-default-proxy"), Value: disableDefaultProxy}, + ProfileID: profileID, + ProfileName: profileName, + ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges}, + Viewport: viewport, + Force: force, + Output: out, }) } diff --git a/cmd/browsers_test.go b/cmd/browsers_test.go index 125179c..86d4947 100644 --- a/cmd/browsers_test.go +++ b/cmd/browsers_test.go @@ -1491,6 +1491,25 @@ func TestBrowsersUpdate_WithViewportNoForce(t *testing.T) { assert.False(t, captured.Viewport.Force.Valid()) } +func TestBrowsersUpdate_WithDisableDefaultProxy(t *testing.T) { + setupStdoutCapture(t) + var captured kernel.BrowserUpdateParams + fake := &FakeBrowsersService{UpdateFunc: func(ctx context.Context, id string, body kernel.BrowserUpdateParams, opts ...option.RequestOption) (*kernel.BrowserUpdateResponse, error) { + captured = body + return &kernel.BrowserUpdateResponse{SessionID: "session123"}, nil + }} + b := BrowsersCmd{browsers: fake} + + err := b.Update(context.Background(), BrowsersUpdateInput{ + Identifier: "session123", + DisableDefaultProxy: BoolFlag{Set: true, Value: true}, + }) + + assert.NoError(t, err) + assert.True(t, captured.DisableDefaultProxy.Valid()) + assert.True(t, captured.DisableDefaultProxy.Value) +} + func TestBrowsersUpdate_ForceWithoutViewport_Errors(t *testing.T) { setupStdoutCapture(t) fake := &FakeBrowsersService{} diff --git a/cmd/proxies/check.go b/cmd/proxies/check.go index 5f47fae..debf1e1 100644 --- a/cmd/proxies/check.go +++ b/cmd/proxies/check.go @@ -20,7 +20,12 @@ func (p ProxyCmd) Check(ctx context.Context, in ProxyCheckInput) error { pterm.Info.Printf("Running health check on proxy %s...\n", in.ID) } - proxy, err := p.proxies.Check(ctx, in.ID) + params := kernel.ProxyCheckParams{} + if in.URL != "" { + params.URL = kernel.Opt(in.URL) + } + + proxy, err := p.proxies.Check(ctx, in.ID, params) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -154,7 +159,8 @@ func getProxyCheckConfigRows(proxy *kernel.ProxyCheckResponse) [][]string { func runProxiesCheck(cmd *cobra.Command, args []string) error { client := util.GetKernelClient(cmd) output, _ := cmd.Flags().GetString("output") + url, _ := cmd.Flags().GetString("url") svc := client.Proxies p := ProxyCmd{proxies: &svc} - return p.Check(cmd.Context(), ProxyCheckInput{ID: args[0], Output: output}) + return p.Check(cmd.Context(), ProxyCheckInput{ID: args[0], URL: url, Output: output}) } diff --git a/cmd/proxies/check_test.go b/cmd/proxies/check_test.go index 8f24ecb..82de52f 100644 --- a/cmd/proxies/check_test.go +++ b/cmd/proxies/check_test.go @@ -13,7 +13,7 @@ func TestProxyCheck_ShowsBypassHosts(t *testing.T) { buf := captureOutput(t) fake := &FakeProxyService{ - CheckFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) { + CheckFunc: func(ctx context.Context, id string, body kernel.ProxyCheckParams, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) { return &kernel.ProxyCheckResponse{ ID: id, Name: "Proxy 1", @@ -34,3 +34,28 @@ func TestProxyCheck_ShowsBypassHosts(t *testing.T) { assert.Contains(t, output, "internal.service.local") assert.Contains(t, output, "Proxy health check passed") } + +func TestProxyCheck_PassesURL(t *testing.T) { + buf := captureOutput(t) + var captured kernel.ProxyCheckParams + + fake := &FakeProxyService{ + CheckFunc: func(ctx context.Context, id string, body kernel.ProxyCheckParams, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) { + captured = body + return &kernel.ProxyCheckResponse{ + ID: id, + Name: "Proxy 1", + Type: kernel.ProxyCheckResponseTypeDatacenter, + Status: kernel.ProxyCheckResponseStatusAvailable, + }, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Check(context.Background(), ProxyCheckInput{ID: "proxy-1", URL: "https://example.com"}) + + assert.NoError(t, err) + assert.True(t, captured.URL.Valid()) + assert.Equal(t, "https://example.com", captured.URL.Value) + assert.Contains(t, buf.String(), "Proxy health check passed") +} diff --git a/cmd/proxies/common_test.go b/cmd/proxies/common_test.go index 48f13cf..df49b76 100644 --- a/cmd/proxies/common_test.go +++ b/cmd/proxies/common_test.go @@ -41,7 +41,7 @@ type FakeProxyService struct { GetFunc func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyGetResponse, error) NewFunc func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) DeleteFunc func(ctx context.Context, id string, opts ...option.RequestOption) error - CheckFunc func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) + CheckFunc func(ctx context.Context, id string, body kernel.ProxyCheckParams, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) } func (f *FakeProxyService) List(ctx context.Context, opts ...option.RequestOption) (*[]kernel.ProxyListResponse, error) { @@ -73,9 +73,9 @@ func (f *FakeProxyService) Delete(ctx context.Context, id string, opts ...option return nil } -func (f *FakeProxyService) Check(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) { +func (f *FakeProxyService) Check(ctx context.Context, id string, body kernel.ProxyCheckParams, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) { if f.CheckFunc != nil { - return f.CheckFunc(ctx, id, opts...) + return f.CheckFunc(ctx, id, body, opts...) } return &kernel.ProxyCheckResponse{ID: id, Type: kernel.ProxyCheckResponseTypeDatacenter}, nil } diff --git a/cmd/proxies/proxies.go b/cmd/proxies/proxies.go index 2440d3a..3a93843 100644 --- a/cmd/proxies/proxies.go +++ b/cmd/proxies/proxies.go @@ -66,7 +66,7 @@ var proxiesDeleteCmd = &cobra.Command{ var proxiesCheckCmd = &cobra.Command{ Use: "check ", Short: "Run a health check on a proxy", - Long: "Run a health check on a proxy to verify it's working and update its status.", + Long: "Run a health check on a proxy to verify it's working and update its status. Optionally test against a specific public URL.", Args: cobra.ExactArgs(1), RunE: runProxiesCheck, } @@ -115,4 +115,5 @@ func init() { // Check flags proxiesCheckCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + proxiesCheckCmd.Flags().String("url", "", "Optional public HTTP or HTTPS URL to test reachability against") } diff --git a/cmd/proxies/types.go b/cmd/proxies/types.go index c8e7a38..eb0220e 100644 --- a/cmd/proxies/types.go +++ b/cmd/proxies/types.go @@ -13,7 +13,7 @@ type ProxyService interface { Get(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.ProxyGetResponse, err error) New(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (res *kernel.ProxyNewResponse, err error) Delete(ctx context.Context, id string, opts ...option.RequestOption) (err error) - Check(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.ProxyCheckResponse, err error) + Check(ctx context.Context, id string, body kernel.ProxyCheckParams, opts ...option.RequestOption) (res *kernel.ProxyCheckResponse, err error) } // ProxyCmd handles proxy operations independent of cobra. @@ -62,5 +62,6 @@ type ProxyDeleteInput struct { type ProxyCheckInput struct { ID string + URL string Output string } diff --git a/go.mod b/go.mod index b826424..ec1bab5 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.45.0 + github.com/kernel/kernel-go-sdk v0.46.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 github.com/samber/lo v1.51.0 diff --git a/go.sum b/go.sum index d7c31ef..431809d 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.45.0 h1:RIFpSDmhAWllo692FZL3Os3TRce5oHvyj8LPfwXce5Y= -github.com/kernel/kernel-go-sdk v0.45.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.46.0 h1:S6OICIzyc6zTy1UdDgWFe9FFOsyJAcPaewKR/U0ZSHA= +github.com/kernel/kernel-go-sdk v0.46.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=