Skip to content
Draft
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ kernel search contents srch_01jsearchresult --limit 3 --content-source browser
- `-s, --stealth` - Launch browser in stealth mode to avoid detection
- `-H, --headless` - Launch browser without GUI access
- `--kiosk` - Launch browser in kiosk mode
- `--gpu` - Launch browser with hardware-accelerated GPU rendering
- `--memory 8GiB|12GiB|16GiB` - Memory for a headful browser: `8GiB` (default) or `16GiB` for CPU browsers, or `12GiB` with `--gpu` for the large GPU browser. Standard GPU browsers get 6GiB.
- `--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 <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 '<host>[,<host>...]=<proxy>'` - 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:<id>` or `name:<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.
Expand Down
4 changes: 2 additions & 2 deletions cmd/browser_pools.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput)
if in.Kiosk.Set {
params.KioskMode = kernel.Bool(in.Kiosk.Value)
}
memory, err := parseMemoryFlag(in.Memory)
memory, err := parseMemoryFlag(in.Memory, poolMemorySizes())
if err != nil {
return err
}
Expand Down Expand Up @@ -432,7 +432,7 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput)
if in.Kiosk.Set {
params.KioskMode = kernel.Bool(in.Kiosk.Value)
}
memory, err := parseMemoryFlag(in.Memory)
memory, err := parseMemoryFlag(in.Memory, poolMemorySizes())
if err != nil {
return err
}
Expand Down
22 changes: 22 additions & 0 deletions cmd/browser_pools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -765,3 +765,25 @@ func TestBrowserPoolsAcquire_WithTelemetryOverride(t *testing.T) {
assert.NoError(t, err)
assert.True(t, captured.Telemetry.Browser.Page.Enabled.Value)
}

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

fake := &FakeBrowserPoolsService{
NewFunc: func(ctx context.Context, body kernel.BrowserPoolNewParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) {
t.Fatal("create must not reach the API with a GPU-only memory size")
return nil, nil
},
UpdateFunc: func(ctx context.Context, id string, body kernel.BrowserPoolUpdateParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) {
t.Fatal("update must not reach the API with a GPU-only memory size")
return nil, nil
},
}
c := BrowserPoolsCmd{client: fake}

err := c.Create(context.Background(), BrowserPoolsCreateInput{Name: "pool", Size: 1, Memory: "12GiB"})
require.EqualError(t, err, "invalid --memory value: 12GiB (must be one of 8GiB, 16GiB)")

err = c.Update(context.Background(), BrowserPoolsUpdateInput{IDOrName: "pool", Memory: "12GiB"})
require.EqualError(t, err, "invalid --memory value: 12GiB (must be one of 8GiB, 16GiB)")
}
29 changes: 18 additions & 11 deletions cmd/browsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,25 +184,32 @@ func parseRegionFlag(region string) (string, error) {
return "", fmt.Errorf("invalid --region value: %s (must be one of %s)", region, strings.Join(availableRegions(), ", "))
}

// availableMemorySizes returns the memory sizes the API accepts when creating a
// browser session. Only headful, non-GPU sessions can request memory; every
// other configuration gets a fixed allocation.
func availableMemorySizes() []string {
// browserMemorySizes returns the memory sizes the API accepts when creating a
// browser session. Headful CPU browsers take 8GiB or 16GiB and headful GPU
// browsers accept 12GiB (the large tier); every other configuration gets a
// fixed allocation.
func browserMemorySizes() []string {
return []string{"8GiB", "12GiB", "16GiB"}
}

// poolMemorySizes returns the memory sizes the API accepts for browser pools.
// Pools never run GPU browsers, so the 12GiB GPU tier is not offered.
func poolMemorySizes() []string {
return []string{"8GiB", "16GiB"}
}

// parseMemoryFlag validates a --memory value. An empty value means the flag was
// not set, which lets the API apply its default (8GiB).
func parseMemoryFlag(memory string) (kernel.BrowserMemoryRequest, error) {
// parseMemoryFlag validates a --memory value against sizes. An empty value
// means the flag was not set, which lets the API apply its per-type default.
func parseMemoryFlag(memory string, sizes []string) (kernel.BrowserMemoryRequest, error) {
if memory == "" {
return "", nil
}
for _, m := range availableMemorySizes() {
for _, m := range sizes {
if strings.EqualFold(memory, m) {
return kernel.BrowserMemoryRequest(m), nil
}
}
return "", fmt.Errorf("invalid --memory value: %s (must be one of %s)", memory, strings.Join(availableMemorySizes(), ", "))
return "", fmt.Errorf("invalid --memory value: %s (must be one of %s)", memory, strings.Join(sizes, ", "))
}

// maxPrivateHosts mirrors the API's cap on network.private_hosts entries.
Expand Down Expand Up @@ -673,7 +680,7 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error {
if in.GPU.Set {
params.GPU = kernel.Opt(in.GPU.Value)
}
memory, err := parseMemoryFlag(in.Memory)
memory, err := parseMemoryFlag(in.Memory, browserMemorySizes())
if err != nil {
return err
}
Expand Down Expand Up @@ -3241,7 +3248,7 @@ unrestricted code execution inside the browser VM and is not sandboxed.`,
browsersCreateCmd.Flags().BoolP("stealth", "s", false, "Launch browser in stealth mode to avoid detection")
browsersCreateCmd.Flags().BoolP("headless", "H", false, "Launch browser without GUI access")
browsersCreateCmd.Flags().Bool("gpu", false, "Launch browser with hardware-accelerated GPU rendering")
browsersCreateCmd.Flags().String("memory", "", "Memory for a headful, non-GPU browser session: '8GiB' (default) or '16GiB'")
browsersCreateCmd.Flags().String("memory", "", "Memory for a headful browser session: '8GiB' (default) or '16GiB' for CPU browsers, '12GiB' for the large GPU browser (with --gpu)")
browsersCreateCmd.Flags().String("invocation-id", "", "Associate the browser session with an invocation")
browsersCreateCmd.Flags().Bool("kiosk", false, "Launch browser in kiosk mode")
browsersCreateCmd.Flags().IntP("timeout", "t", 60, "Timeout in seconds for the browser session")
Expand Down
10 changes: 8 additions & 2 deletions cmd/browsers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -783,7 +783,13 @@ func TestBrowsersCreate_WithMemory(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, kernel.BrowserMemoryRequest8GiB, captured.Memory)

// Omitting the flag sends nothing; the server defaults to 8GiB.
// The large GPU tier is requested through the same flag.
err = b.Create(context.Background(), BrowsersCreateInput{Memory: "12GiB", GPU: BoolFlag{Set: true, Value: true}})
require.NoError(t, err)
assert.Equal(t, kernel.BrowserMemoryRequest("12GiB"), captured.Memory)
assert.True(t, captured.GPU.Value)

// Omitting the flag sends nothing; the server applies its per-type default.
err = b.Create(context.Background(), BrowsersCreateInput{})
require.NoError(t, err)
assert.Empty(t, captured.Memory)
Expand All @@ -793,7 +799,7 @@ func TestBrowsersCreate_WithMemory(t *testing.T) {
assert.NotContains(t, string(raw), "memory")

// An unsupported size is rejected before the request is made.
assert.Error(t, b.Create(context.Background(), BrowsersCreateInput{Memory: "4GiB"}))
assert.EqualError(t, b.Create(context.Background(), BrowsersCreateInput{Memory: "4GiB"}), "invalid --memory value: 4GiB (must be one of 8GiB, 12GiB, 16GiB)")
}

func TestBrowsersCreate_WithChromePolicy(t *testing.T) {
Expand Down
Loading