Skip to content
Merged
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
4 changes: 2 additions & 2 deletions cmd/browser_pools.go
Original file line number Diff line number Diff line change
Expand Up @@ -912,7 +912,7 @@ func init() {
browserPoolsCreateCmd.Flags().StringSlice("allowed-host", nil, "Set an egress allowlist for browsers in the pool: the only destinations they may reach through Kernel-managed egress (repeat or comma-separated, max 100); anything else is refused with a 403 (network_policy_denied). Accepts the same entries as 'browsers create --allowed-host'. Leased browsers can replace or remove it with 'browsers update'; per-lease changes are reset to the pool's allowlist on release (or the browser is replaced). Requires proxy v3")
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("viewport", "", "Browser viewport WIDTHxHEIGHT[@RATE] (positive dimensions; headful browsers require width and height of at least 64)")
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.Flags().String("telemetry", "", "Configure telemetry for browsers warmed into the pool (opt-in): --telemetry=all (default set), --telemetry=off (disable), or --telemetry=console,network (capture exactly those categories)")
Expand Down Expand Up @@ -946,7 +946,7 @@ func init() {
browserPoolsUpdateCmd.Flags().StringSlice("allowed-host", nil, "Set or replace the pool's egress allowlist (repeat or comma-separated, max 100), using the same entry rules as 'browsers create --allowed-host'. An empty list is invalid. Changing the allowlist automatically replaces idle browsers; leased browsers keep theirs until release, when it is reset to the new list or the browser is replaced. Network flags replace the pool's whole network configuration, so pass --private-host and --proxy-route too to keep them. Requires proxy v3")
browserPoolsUpdateCmd.Flags().Bool("clear-allowed-hosts", false, "Remove the pool's egress allowlist and replace idle browsers (combine with --private-host or --proxy-route to keep those settings)")
browserPoolsUpdateCmd.Flags().String("expected-network", "", "Optimistic concurrency precondition: exact network JSON object from the last pool response, or '{}' when it had none. Omit for an unconditional update")
browserPoolsUpdateCmd.Flags().String("viewport", "", "Viewport size (e.g. 1280x800)")
browserPoolsUpdateCmd.Flags().String("viewport", "", "Browser viewport WIDTHxHEIGHT[@RATE] (positive dimensions; headful browsers require width and height of at least 64)")
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.Flags().Bool("clear-chrome-policy", false, "Remove the pool's custom Chrome enterprise policy")
Expand Down
4 changes: 2 additions & 2 deletions cmd/browsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3268,7 +3268,7 @@ func init() {
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")
browsersUpdateCmd.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")
browsersUpdateCmd.Flags().String("viewport", "", "Browser viewport WIDTHxHEIGHT[@RATE] (positive dimensions; headful browsers require width and height of at least 64)")
browsersUpdateCmd.Flags().Bool("force", false, "Force viewport resize even when a live view or recording/replay is active")
browsersUpdateCmd.Flags().String("telemetry", "", "Update telemetry: --telemetry=all (reset to default set), --telemetry=off (disable), or --telemetry=console,network (merge those categories into the current selection)")
browsersUpdateCmd.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")
Expand Down Expand Up @@ -3585,7 +3585,7 @@ unrestricted code execution inside the browser VM and is not sandboxed.`,
browsersCreateCmd.Flags().String("timezone", "", "Session timezone (IANA, e.g. Europe/Berlin); defaults to the browser region's timezone")
browsersCreateCmd.Flags().String("locale", "", "Session locale used for languages and date and number formatting (BCP 47, e.g. de-DE); defaults to the browser region's locale")
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")
browsersCreateCmd.Flags().String("viewport", "", "Browser viewport WIDTHxHEIGHT[@RATE] (positive dimensions; headful browsers require width and height of at least 64)")
browsersCreateCmd.Flags().Bool("viewport-interactive", false, "Interactively select viewport size from list")
browsersCreateCmd.Flags().StringArray("vault", nil, "Vault ID or name to attach for credential/card fill at creation (repeatable, max 20; incompatible with pools; see vaults --help)")
browsersCreateCmd.Flags().String("pool-id", "", "Browser pool ID to acquire from (mutually exclusive with --pool-name)")
Expand Down
17 changes: 14 additions & 3 deletions cmd/browsers_webmcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ func (b BrowsersCmd) WebMCPInvoke(ctx context.Context, in BrowsersWebMCPInvokeIn
for key, value := range fields {
input[key] = value
}
serialized, err := json.Marshal(input)
if err != nil {
return fmt.Errorf("invalid input: could not serialize JSON object: %w", err)
}
if len(serialized) > 4<<20 {
return fmt.Errorf("invalid input: JSON object exceeds 4 MiB after serialization")
}
params := kernel.BrowserWebmcpInvokeToolParams{InvokeRequest: kernel.InvokeRequestParam{
ToolRef: in.ToolRef, Input: input, TimeoutSec: in.TimeoutSec,
}}
Expand Down Expand Up @@ -129,14 +136,18 @@ func newBrowsersWebMCPCommand() *cobra.Command {
"backed by a declarative form that does not autosubmit instead returns once " +
"its fields are populated (awaiting_submission): inspect the form, obtain any required confirmation, " +
"then submit it through Playwright or computer interaction without invoking " +
"the tool again.",
"the tool again. If the tab or frame disappears, or the request times out after invocation starts, " +
"the outcome is unknown; a returned status of unknown has the same meaning. An unrelated reload, " +
"navigation, or tab crash can instead report tool_page_unloaded. Neither failure is retried " +
"because the tool may have had side effects. " +
"CDP-backed custom tool output above 240 KiB fails instead of being truncated.",
Args: cobra.ExactArgs(1),
RunE: runBrowsersWebMCPInvoke,
}
invoke.Flags().String("tool-ref", "", "Opaque tool reference from webmcp list")
_ = invoke.MarkFlagRequired("tool-ref")
invoke.Flags().String("input", "", "Tool input as a JSON object")
invoke.Flags().String("input-file", "", "Path to a JSON object file (use '-' for stdin)")
invoke.Flags().String("input", "", "Tool input as a JSON object (maximum 4 MiB after JSON serialization)")
invoke.Flags().String("input-file", "", "Path to a JSON object file (use '-' for stdin; maximum 4 MiB after JSON serialization)")
invoke.MarkFlagsOneRequired("input", "input-file")
invoke.MarkFlagsMutuallyExclusive("input", "input-file")
invoke.Flags().Int64("timeout-sec", 0, "Maximum execution time in seconds, 1-120 (default per server)")
Expand Down
30 changes: 30 additions & 0 deletions cmd/browsers_webmcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,36 @@ func TestWebMCPInvokeNeverRetries(t *testing.T) {
}
}

func TestWebMCPInvokeToolPageUnloadedNeverRetries(t *testing.T) {
calls := 0
stdout, _, err := executeWebMCPCommand(t, func(w http.ResponseWriter, r *http.Request) {
calls++
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Should-Retry", "false")
w.WriteHeader(http.StatusGone)
fmt.Fprint(w, `{"code":"tool_page_unloaded","invocation_id":"inv-unloaded","message":"Tool page was replaced"}`)
}, "", "invoke", "browser", "--tool-ref", "ref", "--input", "{}")
require.Error(t, err)
assert.Equal(t, 1, calls)
assert.Empty(t, stdout)
assert.EqualError(t, util.CleanedUpSdkError{Err: err}, "tool_page_unloaded: Tool page was replaced")
}

func TestWebMCPInvokeInputLimitAndHelp(t *testing.T) {
cmd, _, err := newBrowsersWebMCPCommand().Find([]string{"invoke"})
require.NoError(t, err)
assert.Contains(t, cmd.Long, "tool_page_unloaded")
assert.Contains(t, cmd.Long, "may have had side effects")
assert.Contains(t, cmd.Flags().Lookup("input").Usage, "4 MiB")
assert.Contains(t, cmd.Flags().Lookup("input-file").Usage, "4 MiB")

oversized := `{"value":"` + strings.Repeat("x", 4<<20) + `"}`
_, _, err = executeWebMCPCommand(t, func(w http.ResponseWriter, r *http.Request) {
t.Error("oversized input reached API")
}, "", "invoke", "browser", "--tool-ref", "ref", "--input", oversized)
require.ErrorContains(t, err, "exceeds 4 MiB")
}

func TestWebMCPInvokeFailedStatus(t *testing.T) {
for _, status := range []string{"error", "canceled"} {
t.Run(status, func(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion cmd/proxies/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func (f *FakeProxyService) Update(ctx context.Context, id string, body kernel.Pr
if f.UpdateFunc != nil {
return f.UpdateFunc(ctx, id, body, opts...)
}
return &kernel.ProxyUpdateResponse{ID: id, Name: body.Name, Type: kernel.ProxyUpdateResponseTypeIsp}, nil
return &kernel.ProxyUpdateResponse{ID: id, Name: body.Name.Value, Type: kernel.ProxyUpdateResponseTypeIsp}, nil
}

func (f *FakeProxyService) Delete(ctx context.Context, id string, opts ...option.RequestOption) error {
Expand Down
9 changes: 9 additions & 0 deletions cmd/proxies/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error {
if len(in.BypassHosts) > 0 {
params.BypassHosts = normalizeBypassHosts(in.BypassHosts)
}
if in.IPAddress != "" && proxyType != kernel.ProxyNewParamsTypeIsp {
return fmt.Errorf("--ip-address is only supported for ISP proxies")
}

// Build config based on type
switch proxyType {
Expand All @@ -51,6 +54,9 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error {
if in.Country != "" {
config.Country = kernel.Opt(in.Country)
}
if in.IPAddress != "" {
config.IPAddress = kernel.Opt(in.IPAddress)
}
params.Config = kernel.ProxyNewParamsConfigUnion{
OfIsp: &config,
}
Expand Down Expand Up @@ -190,6 +196,7 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error {
}
rows = append(rows, []string{"Name", name})
rows = append(rows, []string{"Type", string(proxy.Type)})
rows = append(rows, []string{"IP Address", util.OrDash(proxy.IPAddress)})
rows = append(rows, []string{"Bypass Hosts", formatBypassHosts(proxy.BypassHosts)})

// Display protocol (default to https if not set)
Expand All @@ -215,6 +222,7 @@ func runProxiesCreate(cmd *cobra.Command, args []string) error {
proxyType, _ := cmd.Flags().GetString("type")
name, _ := cmd.Flags().GetString("name")
protocol, _ := cmd.Flags().GetString("protocol")
ipAddress, _ := cmd.Flags().GetString("ip-address")
country, _ := cmd.Flags().GetString("country")
city, _ := cmd.Flags().GetString("city")
state, _ := cmd.Flags().GetString("state")
Expand All @@ -236,6 +244,7 @@ func runProxiesCreate(cmd *cobra.Command, args []string) error {
Name: name,
Type: proxyType,
Protocol: protocol,
IPAddress: ipAddress,
BypassHosts: bypassHosts,
Country: country,
City: city,
Expand Down
34 changes: 34 additions & 0 deletions cmd/proxies/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -456,3 +456,37 @@ func TestProxyCreate_ISP_WithoutCountry(t *testing.T) {
assert.Contains(t, output, "Creating isp proxy")
assert.Contains(t, output, "Successfully created proxy")
}

func TestProxyCreate_ISP_WithReservedIPAddress(t *testing.T) {
fake := &FakeProxyService{
NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) {
require.NotNil(t, body.Config.OfIsp)
assert.True(t, body.Config.OfIsp.IPAddress.Valid())
assert.Equal(t, "203.0.113.10", body.Config.OfIsp.IPAddress.Value)
assert.False(t, body.Config.OfIsp.Country.Valid())
return &kernel.ProxyNewResponse{
ID: "isp-reserved",
Type: kernel.ProxyNewResponseTypeIsp,
IPAddress: "203.0.113.10",
}, nil
},
}

err := ProxyCmd{proxies: fake}.Create(context.Background(), ProxyCreateInput{
Name: "Pinned ISP",
Type: "isp",
IPAddress: "203.0.113.10",
})

assert.NoError(t, err)
}

func TestProxyCreate_RejectsIPAddressForNonISP(t *testing.T) {
err := ProxyCmd{proxies: &FakeProxyService{}}.Create(context.Background(), ProxyCreateInput{
Name: "Residential",
Type: "residential",
IPAddress: "203.0.113.10",
})

assert.EqualError(t, err, "--ip-address is only supported for ISP proxies")
}
4 changes: 4 additions & 0 deletions cmd/proxies/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func (p ProxyCmd) Get(ctx context.Context, in ProxyGetInput) error {
}
rows = append(rows, []string{"Name", name})
rows = append(rows, []string{"Type", string(item.Type)})
rows = append(rows, []string{"IP Address", util.OrDash(item.IPAddress)})
rows = append(rows, []string{"Bypass Hosts", formatBypassHosts(item.BypassHosts)})

// Display protocol (default to https if not set)
Expand Down Expand Up @@ -76,6 +77,9 @@ func getProxyConfigRows(proxy *kernel.ProxyGetResponse) [][]string {
if config.Country != "" {
rows = append(rows, []string{"Country", config.Country})
}
if config.IPAddress != "" {
rows = append(rows, []string{"Reserved IP", config.IPAddress})
}
case kernel.ProxyGetResponseTypeResidential:
if config.Country != "" {
rows = append(rows, []string{"Country", config.Country})
Expand Down
18 changes: 15 additions & 3 deletions cmd/proxies/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ func (p ProxyCmd) List(ctx context.Context, in ProxyListInput) error {
if in.Query != "" {
params.Query = kernel.String(in.Query)
}
if in.IPAddress != "" {
params.IPAddress = kernel.String(in.IPAddress)
}
if in.Limit > 0 {
params.Limit = kernel.Int(int64(in.Limit))
}
Expand Down Expand Up @@ -59,7 +62,7 @@ func (p ProxyCmd) List(ctx context.Context, in ProxyListInput) error {

// Prepare table data
tableData := pterm.TableData{
{"ID", "Name", "Type", "Protocol", "Bypass Hosts", "Config", "Status", "Last Checked"},
{"ID", "Name", "Type", "IP Address", "Protocol", "Bypass Hosts", "Config", "Status", "Last Checked"},
}

for _, proxy := range items {
Expand Down Expand Up @@ -94,6 +97,7 @@ func (p ProxyCmd) List(ctx context.Context, in ProxyListInput) error {
proxy.ID,
name,
string(proxy.Type),
util.OrDash(proxy.IPAddress),
protocol,
formatBypassHosts(proxy.BypassHosts),
configStr,
Expand All @@ -110,8 +114,15 @@ func formatProxyConfig(proxy *kernel.ProxyListResponse) string {
config := &proxy.Config
switch proxy.Type {
case kernel.ProxyListResponseTypeIsp:
parts := []string{}
if config.Country != "" {
return fmt.Sprintf("Country: %s", config.Country)
parts = append(parts, fmt.Sprintf("Country: %s", config.Country))
}
if config.IPAddress != "" {
parts = append(parts, fmt.Sprintf("Reserved IP: %s", config.IPAddress))
}
if len(parts) > 0 {
return strings.Join(parts, ", ")
}
case kernel.ProxyListResponseTypeResidential:
parts := []string{}
Expand Down Expand Up @@ -162,9 +173,10 @@ func runProxiesList(cmd *cobra.Command, args []string) error {
output, _ := cmd.Flags().GetString("output")
name, _ := cmd.Flags().GetString("name")
query, _ := cmd.Flags().GetString("query")
ipAddress, _ := cmd.Flags().GetString("ip-address")
limit, _ := cmd.Flags().GetInt("limit")
offset, _ := cmd.Flags().GetInt("offset")
svc := client.Proxies
p := ProxyCmd{proxies: &svc}
return p.List(cmd.Context(), ProxyListInput{Name: name, Query: query, Limit: limit, Offset: offset, Output: output})
return p.List(cmd.Context(), ProxyListInput{Name: name, Query: query, IPAddress: ipAddress, Limit: limit, Offset: offset, Output: output})
}
3 changes: 2 additions & 1 deletion cmd/proxies/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,10 @@ func TestProxyList_ForwardsLimitOffset(t *testing.T) {
}

p := ProxyCmd{proxies: fake}
err := p.List(context.Background(), ProxyListInput{Limit: 5, Offset: 10})
err := p.List(context.Background(), ProxyListInput{IPAddress: "203.0.113.10", Limit: 5, Offset: 10})

assert.NoError(t, err)
assert.Equal(t, "203.0.113.10", captured.IPAddress.Value)
assert.Equal(t, int64(5), captured.Limit.Value)
assert.Equal(t, int64(10), captured.Offset.Value)
}
Loading
Loading