Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ Commands with JSON output support:
- `--telemetry=all` - Enable telemetry for all categories
- `--telemetry=off` - Disable telemetry
- `--telemetry=<list>` - Per-category config, e.g. `--telemetry=network=on,page=off`
- `--chrome-policy <json>` - Custom Chrome enterprise policy as a JSON object. Kernel-managed policies (extensions, proxy, automation) are rejected server-side.
- `--chrome-policy-file <path>` - Read the Chrome enterprise policy from a file (use `-` for stdin). Mutually exclusive with `--chrome-policy`.
- `--output json`, `-o json` - Output raw JSON object
- _Note: When a pool is specified, omit other session configuration flags—pool settings determine profile, proxy, viewport, etc._
- `kernel browsers delete <id-or-name>` - Delete a browser by ID or name
Expand Down Expand Up @@ -258,11 +260,12 @@ Commands with JSON output support:
- `--timeout <seconds>` - Idle timeout for browsers acquired from the pool
- `--stealth`, `--headless`, `--kiosk` - Default pool configuration
- `--profile-id`, `--profile-name`, `--save-changes`, `--proxy-id`, `--start-url`, `--extension`, `--viewport` - Same semantics as `kernel browsers create`
- `--chrome-policy <json>` / `--chrome-policy-file <path>` - Custom Chrome enterprise policy applied to every browser in the pool, as a JSON object or from a file (`-` for stdin). Same semantics as `kernel browsers create`.
- `--output json`, `-o json` - Output raw JSON object
- `kernel browser-pools get <id-or-name>` - Get pool details
- `--output json`, `-o json` - Output raw JSON object
- `kernel browser-pools update <id-or-name>` - Update pool configuration
- Same flags as create plus `--clear-start-url` (remove the pool's start URL) and `--discard-all-idle` (discard all idle browsers and refill)
- Same flags as create plus `--clear-start-url` (remove the pool's start URL) and `--discard-all-idle` (discard all idle browsers and refill). An empty `--chrome-policy '{}'` is ignored and does not clear an existing policy; recreate the pool to remove one.
- `--output json`, `-o json` - Output raw JSON object
- `kernel browser-pools delete <id-or-name>` - Delete a pool
- `--force` - Force delete even if browsers are leased
Expand Down Expand Up @@ -657,6 +660,10 @@ kernel browsers create --kiosk
# Create a browser with a profile for session state
kernel browsers create --profile-name my-profile

# Create a browser with a custom Chrome enterprise policy
kernel browsers create --chrome-policy '{"BookmarkBarEnabled": false}'
kernel browsers create --chrome-policy-file policy.json

# Delete a browser
kernel browsers delete browser123

Expand Down
39 changes: 39 additions & 0 deletions cmd/browser_pools.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ type BrowserPoolsCreateInput struct {
StartURL string
Extensions []string
Viewport string
ChromePolicy string
ChromePolicyFile string
Output string
}

Expand Down Expand Up @@ -166,6 +168,14 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput)
params.Viewport = *viewport
}

chromePolicy, err := parseChromePolicy(in.ChromePolicy, in.ChromePolicyFile)
if err != nil {
return err
}
if len(chromePolicy) > 0 {
params.ChromePolicy = chromePolicy
}

pool, err := c.client.New(ctx, params)
if err != nil {
return util.CleanedUpSdkError{Err: err}
Expand Down Expand Up @@ -245,6 +255,8 @@ type BrowserPoolsUpdateInput struct {
ClearStartURL bool
Extensions []string
Viewport string
ChromePolicy string
ChromePolicyFile string
DiscardAllIdle BoolFlag
Output string
}
Expand Down Expand Up @@ -316,6 +328,19 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput)
params.Viewport = *viewport
}

chromePolicy, err := parseChromePolicy(in.ChromePolicy, in.ChromePolicyFile)
if err != nil {
return err
}
if len(chromePolicy) > 0 {
params.ChromePolicy = chromePolicy
} else if (in.ChromePolicy != "" || in.ChromePolicyFile != "") && in.Output != "json" {
// An empty policy ({}) cannot clear an existing one: omitzero drops it before it
// reaches the server. Warn instead of silently doing nothing, but stay quiet on the
// json path so stdout remains valid JSON.
pterm.Warning.Println("An empty chrome policy is ignored and does not clear the pool's existing policy; recreate the pool to remove a policy.")
}

pool, err := c.client.Update(ctx, in.IDOrName, params)
if err != nil {
return util.CleanedUpSdkError{Err: err}
Expand Down Expand Up @@ -541,6 +566,9 @@ func init() {
browserPoolsCreateCmd.Flags().String("start-url", "", "Initial page to open for new browsers")
browserPoolsCreateCmd.Flags().StringSlice("extension", []string{}, "Extension IDs or names")
browserPoolsCreateCmd.Flags().String("viewport", "", "Viewport size (e.g. 1280x800)")
browserPoolsCreateCmd.Flags().String("chrome-policy", "", "Custom Chrome enterprise policy as a JSON object")
browserPoolsCreateCmd.Flags().String("chrome-policy-file", "", "Read Chrome enterprise policy (JSON object) from a file (use '-' for stdin)")
browserPoolsCreateCmd.MarkFlagsMutuallyExclusive("chrome-policy", "chrome-policy-file")

addJSONOutputFlag(browserPoolsGetCmd)

Expand All @@ -559,6 +587,9 @@ func init() {
browserPoolsUpdateCmd.Flags().Bool("clear-start-url", false, "Clear the pool start URL")
browserPoolsUpdateCmd.Flags().StringSlice("extension", []string{}, "Extension IDs or names")
browserPoolsUpdateCmd.Flags().String("viewport", "", "Viewport size (e.g. 1280x800)")
browserPoolsUpdateCmd.Flags().String("chrome-policy", "", "Custom Chrome enterprise policy as a JSON object")
browserPoolsUpdateCmd.Flags().String("chrome-policy-file", "", "Read Chrome enterprise policy (JSON object) from a file (use '-' for stdin)")
browserPoolsUpdateCmd.MarkFlagsMutuallyExclusive("chrome-policy", "chrome-policy-file")
browserPoolsUpdateCmd.Flags().Bool("discard-all-idle", false, "Discard all idle browsers")
addJSONOutputFlag(browserPoolsUpdateCmd)

Expand Down Expand Up @@ -615,6 +646,8 @@ func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error {
startURL, _ := cmd.Flags().GetString("start-url")
extensions, _ := cmd.Flags().GetStringSlice("extension")
viewport, _ := cmd.Flags().GetString("viewport")
chromePolicy, _ := cmd.Flags().GetString("chrome-policy")
chromePolicyFile, _ := cmd.Flags().GetString("chrome-policy-file")
output, _ := cmd.Flags().GetString("output")

in := BrowserPoolsCreateInput{
Expand All @@ -632,6 +665,8 @@ func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error {
StartURL: startURL,
Extensions: extensions,
Viewport: viewport,
ChromePolicy: chromePolicy,
ChromePolicyFile: chromePolicyFile,
Output: output,
}

Expand Down Expand Up @@ -664,6 +699,8 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error {
clearStartURL, _ := cmd.Flags().GetBool("clear-start-url")
extensions, _ := cmd.Flags().GetStringSlice("extension")
viewport, _ := cmd.Flags().GetString("viewport")
chromePolicy, _ := cmd.Flags().GetString("chrome-policy")
chromePolicyFile, _ := cmd.Flags().GetString("chrome-policy-file")
discardIdle, _ := cmd.Flags().GetBool("discard-all-idle")
output, _ := cmd.Flags().GetString("output")

Expand All @@ -684,6 +721,8 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error {
ClearStartURL: clearStartURL,
Extensions: extensions,
Viewport: viewport,
ChromePolicy: chromePolicy,
ChromePolicyFile: chromePolicyFile,
DiscardAllIdle: BoolFlag{Set: cmd.Flags().Changed("discard-all-idle"), Value: discardIdle},
Output: output,
}
Expand Down
106 changes: 106 additions & 0 deletions cmd/browser_pools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
type FakeBrowserPoolsService struct {
AcquireFunc func(ctx context.Context, id string, body kernel.BrowserPoolAcquireParams, opts ...option.RequestOption) (*kernel.BrowserPoolAcquireResponse, error)
ListFunc func(ctx context.Context, query kernel.BrowserPoolListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.BrowserPool], error)
NewFunc func(ctx context.Context, body kernel.BrowserPoolNewParams, opts ...option.RequestOption) (*kernel.BrowserPool, error)
UpdateFunc func(ctx context.Context, id string, body kernel.BrowserPoolUpdateParams, opts ...option.RequestOption) (*kernel.BrowserPool, error)
}

func (f *FakeBrowserPoolsService) List(ctx context.Context, query kernel.BrowserPoolListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.BrowserPool], error) {
Expand All @@ -24,6 +26,9 @@ func (f *FakeBrowserPoolsService) List(ctx context.Context, query kernel.Browser
}

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
}

Expand All @@ -32,6 +37,9 @@ func (f *FakeBrowserPoolsService) Get(ctx context.Context, id string, opts ...op
}

func (f *FakeBrowserPoolsService) Update(ctx context.Context, id string, body kernel.BrowserPoolUpdateParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) {
if f.UpdateFunc != nil {
return f.UpdateFunc(ctx, id, body, opts...)
}
return &kernel.BrowserPool{}, nil
}

Expand Down Expand Up @@ -128,3 +136,101 @@ func TestBuildAcquireParams(t *testing.T) {
assert.Len(t, empty.Tags, 0)
assert.False(t, empty.AcquireTimeoutSeconds.Valid())
}

func TestBrowserPoolsCreate_WithChromePolicy(t *testing.T) {
setupStdoutCapture(t)

var captured kernel.BrowserPoolNewParams
fake := &FakeBrowserPoolsService{
NewFunc: func(ctx context.Context, body kernel.BrowserPoolNewParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) {
captured = body
return &kernel.BrowserPool{ID: "pool-cp"}, nil
},
}

c := BrowserPoolsCmd{client: fake}
err := c.Create(context.Background(), BrowserPoolsCreateInput{
Size: 1,
ChromePolicy: `{"BookmarkBarEnabled": false}`,
})
assert.NoError(t, err)
assert.Equal(t, map[string]any{"BookmarkBarEnabled": false}, captured.ChromePolicy)
}

func TestBrowserPoolsCreate_ChromePolicyEmptyObjectOmitted(t *testing.T) {
setupStdoutCapture(t)

var captured kernel.BrowserPoolNewParams
fake := &FakeBrowserPoolsService{
NewFunc: func(ctx context.Context, body kernel.BrowserPoolNewParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) {
captured = body
return &kernel.BrowserPool{ID: "pool-cp"}, nil
},
}

c := BrowserPoolsCmd{client: fake}
err := c.Create(context.Background(), BrowserPoolsCreateInput{Size: 1, ChromePolicy: "{}"})
assert.NoError(t, err)
assert.Nil(t, captured.ChromePolicy)
}

func TestBrowserPoolsUpdate_WithChromePolicy(t *testing.T) {
setupStdoutCapture(t)

var captured kernel.BrowserPoolUpdateParams
fake := &FakeBrowserPoolsService{
UpdateFunc: func(ctx context.Context, id string, body kernel.BrowserPoolUpdateParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) {
captured = body
return &kernel.BrowserPool{ID: id}, nil
},
}

c := BrowserPoolsCmd{client: fake}
err := c.Update(context.Background(), BrowserPoolsUpdateInput{
IDOrName: "pool-1",
ChromePolicy: `{"BookmarkBarEnabled": false}`,
})
assert.NoError(t, err)
assert.Equal(t, map[string]any{"BookmarkBarEnabled": false}, captured.ChromePolicy)
}

func TestBrowserPoolsUpdate_EmptyChromePolicyWarnsAndDoesNotClear(t *testing.T) {
setupStdoutCapture(t)

var captured kernel.BrowserPoolUpdateParams
fake := &FakeBrowserPoolsService{
UpdateFunc: func(ctx context.Context, id string, body kernel.BrowserPoolUpdateParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) {
captured = body
return &kernel.BrowserPool{ID: id}, nil
},
}

c := BrowserPoolsCmd{client: fake}
err := c.Update(context.Background(), BrowserPoolsUpdateInput{
IDOrName: "pool-1",
ChromePolicy: "{}",
})
assert.NoError(t, err)
assert.Nil(t, captured.ChromePolicy)
assert.Contains(t, outBuf.String(), "does not clear")
}

func TestBrowserPoolsUpdate_EmptyChromePolicyQuietInJSONMode(t *testing.T) {
setupStdoutCapture(t)

fake := &FakeBrowserPoolsService{
UpdateFunc: func(ctx context.Context, id string, body kernel.BrowserPoolUpdateParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) {
return &kernel.BrowserPool{ID: id}, nil
},
}

c := BrowserPoolsCmd{client: fake}
err := c.Update(context.Background(), BrowserPoolsUpdateInput{
IDOrName: "pool-1",
ChromePolicy: "{}",
Output: "json",
})
assert.NoError(t, err)
// The warning must not leak onto stdout in json mode, where it would corrupt the payload.
assert.NotContains(t, outBuf.String(), "does not clear")
}
Loading
Loading