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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,8 @@ kernel search contents srch_01jsearchresult --limit 3 --content-source browser
- `--allowed-host <host>` - Set an egress allowlist at creation: the only destinations the browser may reach through Kernel-managed egress (repeatable or comma-separated, max 100). Other destinations are refused with a 403 whose `X-Kernel-Proxy-Error` header is `network_policy_denied`. Filters Kernel-managed egress only, not all browser VM traffic. Accepts exact hostnames (`example.com`), a leading wildcard that matches subdomains but not the domain itself (`*.example.com`), public IPs (`8.8.8.8`, `[2001:4860:4860::8888]`), and public CIDRs (`8.8.4.0/24`); no ports, paths, or schemes. `--start-url` must be allowed. Omit for unfiltered egress; an empty list is invalid. Requires proxy v3; cannot be combined with `--pool-id` or `--pool-name`, even with `--yes` (set the allowlist on the pool with `kernel browser-pools create/update --allowed-host`). An existing list can be replaced or removed later with `browsers update --allowed-host` / `--clear-allowed-hosts`, but cannot be added later if omitted at creation or removed.
- `--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. Routes apply from the start of the session, including to `--start-url`. Routes are create-only and cannot be combined with `--pool-id`/`--pool-name`; configure them on the pool instead.
- `--start-url <url>` - Initial page to open on launch
- `--timezone <iana>` - Session timezone, e.g. `Europe/Berlin`. Defaults to the browser region's timezone
- `--locale <bcp47>` - Session locale used for languages and date and number formatting, e.g. `de-DE`. Defaults to the browser region's locale
- `--proxy-id <id>` / `--proxy-name <name>` - Use that proxy for the session regardless of stealth (mutually exclusive with each other and with `--proxy-mode`)
- `--proxy-mode direct|default` - Egress mode instead of a selected proxy: `direct` for no proxy regardless of stealth, `default` for the stealth-derived default (Kernel's stealth proxy with `--stealth`, direct egress otherwise). Omit all proxy flags to get the default.
- `--name <name>` - Optional unique name for the session (used to find it later by name; can be changed with `browsers update --name`)
Expand All @@ -297,6 +299,10 @@ kernel search contents srch_01jsearchresult --limit 3 --content-source browser
- `--clear-name` - Clear the session name
- `--tag <KEY=VALUE>` - Set a tag, repeatable; up to 50 pairs. Replaces the entire tag set (not merged); mutually exclusive with `--clear-tags`
- `--clear-tags` - Remove all tags from the session
- `--timezone <iana>` - Set the session timezone, e.g. `America/Los_Angeles`. Applies without restarting the browser; mutually exclusive with `--clear-timezone`
- `--clear-timezone` - Clear the timezone override and use the browser region's default
- `--locale <bcp47>` - Set the session locale, e.g. `de-DE`. Applies without restarting the browser; mutually exclusive with `--clear-locale`
- `--clear-locale` - Clear the locale override and use the browser region's default
- `--telemetry=all` - Enable telemetry for all categories
- `--telemetry=off` - Disable telemetry
- `--telemetry=<list>` - Per-category config, e.g. `--telemetry=network=on,page=off`
Expand Down
99 changes: 97 additions & 2 deletions cmd/browsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,28 @@ func formatPrivateHosts(network kernel.BrowserNetworkConfig) string {
return strings.Join(network.PrivateHosts, ", ")
}

// browserLocationRows shows the effective timezone and locale with whether each
// is the session's override or the browser region's default.
func browserLocationRows(location kernel.BrowserLocation) [][]string {
if location.Timezone == "" && location.Locale == "" {
return nil
}
return [][]string{
{"Timezone", formatBrowserLocationField(location.Timezone, location.TimezoneSource)},
{"Locale", formatBrowserLocationField(location.Locale, location.LocaleSource)},
}
}

func formatBrowserLocationField(value string, source kernel.BrowserLocationSource) string {
if value == "" {
return "-"
}
if source == "" {
return value
}
return fmt.Sprintf("%s (%s)", value, source)
}

// formatVaultReferences renders the vaults linked to a session for table output,
// preferring each vault's name and falling back to its ID. It returns an empty
// string when no vaults are linked, so the row can be omitted entirely.
Expand Down Expand Up @@ -508,6 +530,8 @@ type BrowsersCreateInput struct {
AllowedHostsProvided bool
ProxyRoutes []string
StartURL string
Timezone string
Locale string
Extensions []string
Vaults []string
Viewport string
Expand Down Expand Up @@ -558,6 +582,14 @@ type BrowsersUpdateInput struct {
TagsProvided bool
ClearTags bool
StartURL string
// Timezone and Locale set the session's location overrides when SetTimezone
// and SetLocale are set; ClearTimezone and ClearLocale remove them.
Timezone string
SetTimezone bool
ClearTimezone bool
Locale string
SetLocale bool
ClearLocale bool
// AllowedHosts replaces the session's egress allowlist when
// AllowedHostsProvided is set; ClearAllowedHosts removes it.
AllowedHosts []string
Expand Down Expand Up @@ -774,6 +806,12 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error {
if in.StartURL != "" {
params.StartURL = kernel.Opt(in.StartURL)
}
if in.Timezone != "" {
params.Location.Timezone = kernel.Opt(in.Timezone)
}
if in.Locale != "" {
params.Location.Locale = kernel.Opt(in.Locale)
}

region, err := parseRegionFlag(in.Region)
if err != nil {
Expand Down Expand Up @@ -881,6 +919,7 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error {

tableData := buildBrowserTableData(browser.SessionID, browser.CdpWsURL, browser.BrowserLiveViewURL, browser.Profile, browser.ProfileSaveChanges, browser.StartURL, browser.Name, browser.Tags)
tableData = append(tableData, []string{"Private Hosts", formatPrivateHosts(browser.Network)}, []string{"Allowed Hosts", formatAllowedHosts(browser.Network)}, []string{"Proxy Routes", formatProxyRoutes(browser.Network)})
tableData = append(tableData, browserLocationRows(browser.Location)...)
PrintTableNoPad(tableData, true)
if len(browser.Vaults) > 0 {
rows := pterm.TableData{{"Attached vault ID", "Name"}}
Expand Down Expand Up @@ -1006,6 +1045,7 @@ func (b BrowsersCmd) Get(ctx context.Context, in BrowsersGetInput) error {
// Append additional detailed fields
tableData = append(tableData, []string{"Created At", util.FormatLocal(browser.CreatedAt)})
tableData = append(tableData, []string{"Region", util.OrDash(string(browser.Region))})
tableData = append(tableData, browserLocationRows(browser.Location)...)
tableData = append(tableData, []string{"Timeout (seconds)", fmt.Sprintf("%d", browser.TimeoutSeconds)})
tableData = append(tableData, []string{"Headless", fmt.Sprintf("%t", browser.Headless)})
tableData = append(tableData, []string{"Stealth", fmt.Sprintf("%t", browser.Stealth)})
Expand Down Expand Up @@ -1124,13 +1164,27 @@ func (b BrowsersCmd) Update(ctx context.Context, in BrowsersUpdateInput) error {
return fmt.Errorf("--name requires a non-empty value; use --clear-name to clear the name")
}

if in.SetTimezone && in.ClearTimezone {
return fmt.Errorf("cannot specify both --timezone and --clear-timezone")
}
if in.SetTimezone && in.Timezone == "" {
return fmt.Errorf("--timezone requires a non-empty value; use --clear-timezone to go back to the browser region's default")
}
if in.SetLocale && in.ClearLocale {
return fmt.Errorf("cannot specify both --locale and --clear-locale")
}
if in.SetLocale && in.Locale == "" {
return fmt.Errorf("--locale requires a non-empty value; use --clear-locale to go back to the browser region's default")
}

hasProxyChange := proxySel.set()
hasProfileChange := in.ProfileID != "" || in.ProfileName != ""
hasViewportChange := in.Viewport != ""
// By this point a set name is guaranteed non-empty (the guard above rejects --name "").
hasNameChange := in.SetName || in.ClearName
hasTagsChange := len(in.Tags) > 0 || in.ClearTags
hasStartURLChange := in.StartURL != ""
hasLocationChange := in.SetTimezone || in.ClearTimezone || in.SetLocale || in.ClearLocale

// The API replaces the allowlist with the given entries, or removes it when
// sent null. An empty list is rejected, so require at least one entry.
Expand All @@ -1154,8 +1208,8 @@ func (b BrowsersCmd) Update(ctx context.Context, in BrowsersUpdateInput) error {
}

// Validate that at least one update option is provided
if !hasProxyChange && !hasProfileChange && !hasViewportChange && in.Telemetry == "" && in.TelemetryCdpExclude == "" && !hasNameChange && !hasTagsChange && !hasStartURLChange && !hasAllowedHostsChange {
return fmt.Errorf("must specify at least one of: --proxy-id, --proxy-name, --proxy-mode, --clear-proxy, --disable-default-proxy, --profile-id, --profile-name, --viewport, --telemetry, --telemetry-cdp-exclude, --name, --clear-name, --tag, --clear-tags, --start-url, --allowed-host, or --clear-allowed-hosts")
if !hasProxyChange && !hasProfileChange && !hasViewportChange && in.Telemetry == "" && in.TelemetryCdpExclude == "" && !hasNameChange && !hasTagsChange && !hasStartURLChange && !hasLocationChange && !hasAllowedHostsChange {
return fmt.Errorf("must specify at least one of: --proxy-id, --proxy-name, --proxy-mode, --clear-proxy, --disable-default-proxy, --profile-id, --profile-name, --viewport, --telemetry, --telemetry-cdp-exclude, --name, --clear-name, --tag, --clear-tags, --start-url, --timezone, --clear-timezone, --locale, --clear-locale, --allowed-host, or --clear-allowed-hosts")
}

params := kernel.BrowserUpdateParams{}
Expand All @@ -1181,6 +1235,19 @@ func (b BrowsersCmd) Update(ctx context.Context, in BrowsersUpdateInput) error {
params.StartURL = kernel.String(in.StartURL)
}

// Handle location changes. Null clears an override so the browser uses its
// region's default.
if in.ClearTimezone {
params.Location.Timezone = param.Null[string]()
} else if in.SetTimezone {
params.Location.Timezone = kernel.Opt(in.Timezone)
}
if in.ClearLocale {
params.Location.Locale = param.Null[string]()
} else if in.SetLocale {
params.Location.Locale = kernel.Opt(in.Locale)
}

// Handle egress allowlist changes. Null removes the allowlist.
if in.ClearAllowedHosts {
params.Network.AllowedHosts = param.NullSlice[[]string]()
Expand Down Expand Up @@ -1265,6 +1332,10 @@ func (b BrowsersCmd) Update(ctx context.Context, in BrowsersUpdateInput) error {
if hasStartURLChange {
pterm.Info.Printf("Start URL: %s\n", util.OrDash(browser.StartURL))
}
if hasLocationChange {
pterm.Info.Printf("Timezone: %s\n", formatBrowserLocationField(browser.Location.Timezone, browser.Location.TimezoneSource))
pterm.Info.Printf("Locale: %s\n", formatBrowserLocationField(browser.Location.Locale, browser.Location.LocaleSource))
}
if hasAllowedHostsChange {
pterm.Info.Printf("Allowed Hosts: %s\n", formatAllowedHosts(browser.Network))
}
Expand Down Expand Up @@ -2937,13 +3008,17 @@ Supported operations:
- Rename or clear the session name (--name or --clear-name)
- Replace or clear the session tags (--tag or --clear-tags)
- Navigate the session to a URL (--start-url)
- Set or clear the session timezone and locale (--timezone, --locale, --clear-timezone, --clear-locale)
- Replace or remove the egress allowlist (--allowed-host or --clear-allowed-hosts)

Notes:
- Profiles can only be loaded into sessions that don't already have a profile.
- --start-url navigation is best-effort: the update succeeds even if the page fails to load.
- --start-url combined with --profile-id/--profile-name overrides the profile's restored tabs.
- --tag replaces the entire tag set (it is not merged with existing tags).
- --timezone and --locale apply without restarting the browser. --clear-timezone and
--clear-locale go back to the browser region's default. Omit a flag to leave that field unchanged.
--start-url in the same update opens once the new location applies.
- --allowed-host replaces an existing allowlist only; it cannot add one to a browser created
without one. Omit to leave the list unchanged; an empty list is invalid.
- --clear-allowed-hosts removes the allowlist and returns to unfiltered egress. Once removed,
Expand Down Expand Up @@ -3006,6 +3081,10 @@ func init() {
browsersUpdateCmd.Flags().Bool("clear-tags", false, "Remove all tags from the browser session")
browsersUpdateCmd.Flags().StringSlice("allowed-host", nil, "Replace an existing egress allowlist (repeat or comma-separated, max 100), using the same entry rules as 'browsers create --allowed-host'. Cannot add one to a browser created without one or after removal. Omit to leave unchanged; an empty list is invalid. Applies without restarting the browser; --start-url must be allowed by the new list. Requires proxy v3; on leased pooled browsers the pool's allowlist is restored on release (mutually exclusive with --clear-allowed-hosts). See notes for propagation timing")
browsersUpdateCmd.Flags().Bool("clear-allowed-hosts", false, "Remove the egress allowlist and return to unfiltered egress; an allowlist cannot be added back to this browser")
browsersUpdateCmd.Flags().String("timezone", "", "Set the session timezone (IANA, e.g. America/Los_Angeles) (mutually exclusive with --clear-timezone)")
browsersUpdateCmd.Flags().Bool("clear-timezone", false, "Clear the session timezone override and use the browser region's default")
browsersUpdateCmd.Flags().String("locale", "", "Set the session locale used for languages and date and number formatting (BCP 47, e.g. de-DE) (mutually exclusive with --clear-locale)")
browsersUpdateCmd.Flags().Bool("clear-locale", false, "Clear the session locale override and use the browser region's default")
browsersUpdateCmd.Flags().String("start-url", "", "Navigate the browser to this URL after applying the update. Overrides the restored tabs when a profile is loaded in the same update. Navigation is best-effort, so failures do not fail the update")

browsersCmd.AddCommand(browsersListCmd)
Expand Down Expand Up @@ -3302,6 +3381,8 @@ unrestricted code execution inside the browser VM and is not sandboxed.`,
browsersCreateCmd.Flags().StringSlice("allowed-host", nil, "Set an egress allowlist at creation: the only destinations the browser may reach through Kernel-managed egress (repeat or comma-separated, max 100); anything else is refused with a 403 (network_policy_denied). Filters Kernel-managed egress only, not all browser VM traffic. Accepts exact hostnames ('example.com'), a leading wildcard matching subdomains only ('*.example.com'), public IPs ('8.8.8.8', '[2001:4860:4860::8888]'), and public CIDRs ('8.8.4.0/24'). No ports, paths, or schemes. --start-url must be allowed. Omit for unfiltered egress; an empty list is invalid. Requires proxy v3; cannot be combined with --pool-id or --pool-name, even with --yes. An existing list can be replaced or removed with 'browsers update'; it cannot be added later if omitted at creation or removed")
browsersCreateCmd.Flags().StringArray("proxy-route", nil, "Route HOST[,HOST...]=PROXY through a proxy (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-* or default egress. Routes also apply to start_url and other setup traffic. Create-only")
browsersCreateCmd.Flags().String("start-url", "", "Initial page to open on launch")
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().Bool("viewport-interactive", false, "Interactively select viewport size from list")
Expand Down Expand Up @@ -3443,6 +3524,8 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
allowedHosts, _ := cmd.Flags().GetStringSlice("allowed-host")
proxyRoutes, _ := cmd.Flags().GetStringArray("proxy-route")
startURL, _ := cmd.Flags().GetString("start-url")
timezone, _ := cmd.Flags().GetString("timezone")
locale, _ := cmd.Flags().GetString("locale")
extensions, _ := cmd.Flags().GetStringSlice("extension")
vaults, _ := cmd.Flags().GetStringArray("vault")
viewport, _ := cmd.Flags().GetString("viewport")
Expand Down Expand Up @@ -3601,6 +3684,8 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
AllowedHostsProvided: cmd.Flags().Changed("allowed-host"),
ProxyRoutes: proxyRoutes,
StartURL: startURL,
Timezone: timezone,
Locale: locale,
Extensions: extensions,
Vaults: vaults,
Viewport: viewport,
Expand Down Expand Up @@ -3680,6 +3765,10 @@ func runBrowsersUpdate(cmd *cobra.Command, args []string) error {
tags, tagsProvided := tagsFromFlag(cmd, "tag")
clearTags, _ := cmd.Flags().GetBool("clear-tags")
startURL, _ := cmd.Flags().GetString("start-url")
timezone, _ := cmd.Flags().GetString("timezone")
clearTimezone, _ := cmd.Flags().GetBool("clear-timezone")
locale, _ := cmd.Flags().GetString("locale")
clearLocale, _ := cmd.Flags().GetBool("clear-locale")
allowedHosts, _ := cmd.Flags().GetStringSlice("allowed-host")
clearAllowedHosts, _ := cmd.Flags().GetBool("clear-allowed-hosts")

Expand All @@ -3706,6 +3795,12 @@ func runBrowsersUpdate(cmd *cobra.Command, args []string) error {
TagsProvided: tagsProvided,
ClearTags: clearTags,
StartURL: startURL,
Timezone: timezone,
SetTimezone: cmd.Flags().Changed("timezone"),
ClearTimezone: clearTimezone,
Locale: locale,
SetLocale: cmd.Flags().Changed("locale"),
ClearLocale: clearLocale,
AllowedHosts: allowedHosts,
AllowedHostsProvided: cmd.Flags().Changed("allowed-host"),
ClearAllowedHosts: clearAllowedHosts,
Expand Down
Loading
Loading