Merge branch 'main' into fix_gemini_system_in_user_drops_parts

This commit is contained in:
Magnus Müller
2026-08-30 02:51:07 -07:00
committed by GitHub
6 changed files with 204 additions and 254 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ The key product of Browser Use Cloud is the completion of user tasks.
- Profile Sync is the best way to handle authentication for tasks. This feature allows users to upload their local browser cookies (where the user is already logged into the services they need authentication for) to a Browser Profile that can be used for tasks on the cloud. To initiate a Profile Sync, a user must run `export BROWSER_USE_API_KEY=<your_key> && curl -fsSL https://browser-use.com/profile.sh | sh` and follow the steps in the interactive terminal.
## Quickstart
To get started, direct the user to first must create an account, purchase credits (or simply claim the five free tasks given on account creation), and generate an API key on the Browser Use online platform: https://cloud.browser-use.com/. These are the only steps that can only be done on the platform.
To get started, direct the user to first create an account, claim the $15 one-time signup credit if eligible (or purchase credits), and generate an API key on the Browser Use online platform: https://cloud.browser-use.com/. These are the only steps that can only be done on the platform.
Avoid giving the user all of the following steps at once as it may seem overwheling. Instead present one step at a time and only continue when asked. Do as much for the user as you are able to.
+12 -1
View File
@@ -25,6 +25,14 @@ def _get_enable_default_extensions_default() -> bool:
return True
def _get_headless_default() -> bool | None:
"""Get the default value for headless from BROWSER_USE_HEADLESS env var, or None to fall back to display detection."""
env_val = os.getenv('BROWSER_USE_HEADLESS')
if env_val is not None:
return env_val.lower() not in ('0', 'false', 'no', 'off', '')
return None
CHROME_DEBUG_PORT = 9242 # use a non-default port to avoid conflicts with other tools / devs using 9222
DOMAIN_OPTIMIZATION_THRESHOLD = 100 # Convert domain lists to sets for O(1) lookup when >= this size
CHROME_PROFILE_TRANSIENT_FILE_PATTERNS = (
@@ -419,7 +427,10 @@ class BrowserLaunchArgs(BaseModel):
validation_alias=AliasChoices('browser_binary_path', 'chrome_binary_path'),
description='Path to the chromium-based browser executable to use.',
)
headless: bool | None = Field(default=None, description='Whether to run the browser in headless or windowed mode.')
headless: bool | None = Field(
default_factory=_get_headless_default,
description='Whether to run the browser in headless or windowed mode. Can be set via BROWSER_USE_HEADLESS environment variable.',
)
args: list[CliArgStr] = Field(
default_factory=list, description='List of *extra* CLI args to pass to the browser when launching.'
)
+1 -1
View File
@@ -21,7 +21,7 @@ dependencies = [
"httpx==0.28.1",
"posthog==7.7.0",
"psutil==7.2.2",
"pydantic==2.12.5",
"pydantic>=2.12.5,<2.14",
"pyobjc==12.1; platform_system == 'darwin'",
"python-dotenv==1.2.2",
"requests==2.33.0",
+55 -143
View File
@@ -1,179 +1,91 @@
---
name: remote-browser
description: Controls a local browser from a sandboxed remote machine. Use when the agent is running in a sandbox (no GUI) and needs to navigate websites, interact with web pages, fill forms, take screenshots, or expose local dev servers via tunnels.
description: Controls an isolated Browser Use Cloud browser from a sandboxed machine with the current Browser Use CLI.
allowed-tools: Bash(browser-use:*)
---
# Browser Automation for Sandboxed Agents
# Remote Browser
This skill is for agents running on **sandboxed remote machines** (cloud VMs, CI, coding agents) that need to control a headless browser.
Use this skill when an agent runs on a machine without a usable local Chrome and needs an isolated browser. The current Browser Use CLI runs Python from stdin. Do not use the removed `open`, `state`, `click`, `input`, `tab`, `cloud connect`, or `--connect` commands.
## Prerequisites
## Check the CLI
```bash
browser-use doctor # Verify installation
browser-use --doctor
browser-use skill show
```
For setup details, see https://github.com/browser-use/browser-use/blob/main/browser_use/skill_cli/README.md
If setup fails, follow the current [Browser Use skill](../browser-use/SKILL.md).
## Core Workflow
## Start an isolated browser
1. **Navigate**: `browser-use open <url>` — starts headless browser if needed
2. **Inspect**: `browser-use state` — returns clickable elements with indices
3. **Interact**: use indices from state (`browser-use click 5`, `browser-use input 3 "text"`)
4. **Verify**: `browser-use state` or `browser-use screenshot` to confirm
5. **Repeat**: browser stays open between commands
6. **Cleanup**: `browser-use close` when done
## Browser Modes
Authenticate once:
```bash
browser-use open <url> # Default: headless Chromium
browser-use cloud connect # Provision cloud browser and connect
browser-use --connect open <url> # Auto-discover running Chrome via CDP
browser-use --cdp-url ws://localhost:9222/... open <url> # Connect via CDP URL
browser-use auth login
```
## Commands
Pick a short unique name. `r7k2` below is only an example.
```bash
# Navigation
browser-use open <url> # Navigate to URL
browser-use back # Go back in history
browser-use scroll down # Scroll down (--amount N for pixels)
browser-use scroll up # Scroll up
browser-use tab list # List all tabs with lock status
browser-use tab new [url] # Open a new tab (blank or with URL)
browser-use tab switch <index> # Switch to tab by index
browser-use tab close <index> [index...] # Close one or more tabs
# Page State — always run state first to get element indices
browser-use state # URL, title, clickable elements with indices
browser-use screenshot [path.png] # Screenshot (base64 if no path, --full for full page)
# Interactions — use indices from state
browser-use click <index> # Click element by index
browser-use click <x> <y> # Click at pixel coordinates
browser-use type "text" # Type into focused element
browser-use input <index> "text" # Click element, then type
browser-use keys "Enter" # Send keyboard keys (also "Control+a", etc.)
browser-use select <index> "option" # Select dropdown option
browser-use upload <index> <path> # Upload file to file input
browser-use hover <index> # Hover over element
browser-use dblclick <index> # Double-click element
browser-use rightclick <index> # Right-click element
# Data Extraction
browser-use eval "js code" # Execute JavaScript, return result
browser-use get title # Page title
browser-use get html [--selector "h1"] # Page HTML (or scoped to selector)
browser-use get text <index> # Element text content
browser-use get value <index> # Input/textarea value
browser-use get attributes <index> # Element attributes
browser-use get bbox <index> # Bounding box (x, y, width, height)
# Wait
browser-use wait selector "css" # Wait for element (--state visible|hidden|attached|detached, --timeout ms)
browser-use wait text "text" # Wait for text to appear
# Cookies
browser-use cookies get [--url <url>] # Get cookies (optionally filtered)
browser-use cookies set <name> <value> # Set cookie (--domain, --secure, --http-only, --same-site, --expires)
browser-use cookies clear [--url <url>] # Clear cookies
browser-use cookies export <file> # Export to JSON
browser-use cookies import <file> # Import from JSON
# Python — persistent session with browser access
browser-use python "code" # Execute Python (variables persist across calls)
browser-use python --file script.py # Run file
browser-use python --vars # Show defined variables
browser-use python --reset # Clear namespace
# Session
browser-use close # Close browser and stop daemon
browser-use sessions # List active sessions
browser-use close --all # Close all sessions
browser-use <<'PY'
start_remote_daemon("r7k2")
PY
```
The Python `browser` object provides: `browser.url`, `browser.title`, `browser.html`, `browser.goto(url)`, `browser.back()`, `browser.click(index)`, `browser.type(text)`, `browser.input(index, text)`, `browser.keys(keys)`, `browser.upload(index, path)`, `browser.screenshot(path)`, `browser.scroll(direction, amount)`, `browser.wait(seconds)`.
## Tunnels
Expose local dev servers to the browser via Cloudflare tunnels.
Use the same name for every command in this browser:
```bash
browser-use tunnel <port> # Start tunnel (idempotent)
browser-use tunnel list # Show active tunnels
browser-use tunnel stop <port> # Stop tunnel
browser-use tunnel stop --all # Stop all tunnels
BU_NAME=r7k2 browser-use <<'PY'
new_tab("https://example.com")
wait_for_load()
print(page_info())
PY
```
## Command Chaining
Each remote daemon is a separate Browser Use Cloud browser. Use a different name for each parallel task. Remote browsers can bill until they stop or time out.
Commands can be chained with `&&`. The browser persists via the daemon, so chaining is safe and efficient.
## Inspect and interact
Helpers are pre-imported. Keep multi-step work in one heredoc when practical.
```bash
browser-use open https://example.com && browser-use state
browser-use input 5 "user@example.com" && browser-use input 6 "password" && browser-use click 7
BU_NAME=r7k2 browser-use <<'PY'
print(page_info())
print(js("document.title"))
fill_input('input[name="q"]', "browser automation")
press_key("Enter")
wait_for_load()
print(page_info())
PY
```
Chain when you don't need intermediate output. Run separately when you need to parse `state` to discover indices first.
Useful helpers:
## Common Workflows
- Navigate: `new_tab(url)`, `goto_url(url)`, `wait_for_load()`
- Inspect: `page_info()`, `js(code)`, `cdp(method, ...)`
- Interact: `click_at_xy(x, y)`, `type_text(text)`, `fill_input(selector, text)`, `press_key(key)`, `scroll(x, y)`
- Tabs: `list_tabs()`, `switch_tab(target)`, `close_tab(target)`
- Files and proof: `capture_screenshot()`, `wait_for_element(selector)`
### Exposing Local Dev Servers
Prefer the accessibility tree for element discovery:
```python
nodes = cdp("Accessibility.getFullAXTree")["nodes"]
```
Use a targeted `js(...)` query when the accessibility tree lacks the element. Verify each action with `page_info()`, a focused DOM check, or a screenshot.
## Stop the browser
When the work is done, stop the exact named browser:
```bash
python -m http.server 3000 & # Start dev server
browser-use tunnel 3000 # → https://abc.trycloudflare.com
browser-use open https://abc.trycloudflare.com # Browse the tunnel
browser-use <<'PY'
stop_remote_daemon("r7k2")
PY
```
Tunnels are independent of browser sessions and persist across `browser-use close`.
## Multi-Agent (--connect mode)
Multiple agents can share one browser via `--connect`. Each agent gets its own tab — other agents can't interfere.
**Setup**: Register once, then pass the index with every `--connect` command:
```bash
INDEX=$(browser-use register) # → prints "1"
browser-use --connect $INDEX open <url> # Navigate in agent's own tab
browser-use --connect $INDEX state # Get state from agent's tab
browser-use --connect $INDEX click <element> # Click in agent's tab
```
- **Tab locking**: When an agent mutates a tab (click, type, navigate), that tab is locked to it. Other agents get an error if they try to mutate the same tab.
- **Read-only access**: `state`, `screenshot`, `get`, and `wait` commands work on any tab regardless of locks.
- **Agent sessions expire** after 5 minutes of inactivity. Run `browser-use register` again to get a new index.
## Global Options
| Option | Description |
|--------|-------------|
| `--headed` | Show browser window |
| `--connect` | Auto-discover running Chrome via CDP |
| `--cdp-url <url>` | Connect via CDP URL (`http://` or `ws://`) |
| `--session NAME` | Target a named session (default: "default") |
| `--json` | Output as JSON |
## Tips
1. **Always run `state` first** to see available elements and their indices
2. **Sessions persist** — browser stays open between commands until you close it
3. **Tunnels are independent** — they persist across `browser-use close`
4. **`tunnel` is idempotent** — calling again for the same port returns the existing URL
## Troubleshooting
- **Browser won't start?** `browser-use close` then retry. Run `browser-use doctor` to check.
- **Element not found?** `browser-use scroll down` then `browser-use state`
- **Tunnel not working?** `which cloudflared` to check, `browser-use tunnel list` to see active tunnels
## Cleanup
```bash
browser-use close # Close browser session
browser-use tunnel stop --all # Stop tunnels (if any)
```
Do not leave an unused remote browser running.
@@ -57,6 +57,40 @@ def test_docs_install_browser_use_skill_from_package_alias():
assert 'raw.githubusercontent.com/browser-use/browser-harness/main/SKILL.md' not in readme
def test_remote_browser_skill_uses_current_cli():
remote_skill = (ROOT / 'skills' / 'remote-browser' / 'SKILL.md').read_text(encoding='utf-8')
for removed_command in (
'browser-use open',
'browser-use state',
'browser-use click',
'browser-use input',
'browser-use tab',
'browser-use screenshot',
'browser-use eval',
'browser-use cookies',
'browser-use close',
'browser-use sessions',
'browser-use tunnel',
'browser-use wait',
'browser-use register',
'browser-use cloud connect',
'browser-use --connect',
'browser_use/skill_cli/README.md',
):
assert removed_command not in remote_skill
for current_command in (
"browser-use <<'PY'",
'start_remote_daemon("r7k2")',
'BU_NAME=r7k2 browser-use',
'new_tab("https://example.com")',
'print(page_info())',
'stop_remote_daemon("r7k2")',
):
assert current_command in remote_skill
def test_browser_use_cli_installs_browser_harness_package_skill(tmp_path):
bin_dir = _fake_browser_harness_tools(tmp_path, '---\nname: browser-harness\n---\n\n# Browser Harness\n')
+101 -108
View File
@@ -1,123 +1,116 @@
"""Tests for extension configuration environment variables."""
import os
"""Tests for browser configuration environment variables."""
import pytest
from browser_use.browser import BrowserSession
from browser_use.browser.profile import (
BrowserProfile,
_get_enable_default_extensions_default,
_get_headless_default,
)
class TestDisableExtensionsEnvVar:
"""Test BROWSER_USE_DISABLE_EXTENSIONS environment variable."""
TRUTHY_STRINGS = ['true', 'True', 'TRUE', '1', 'yes', 'on']
FALSY_STRINGS = ['false', 'False', 'FALSE', '0', 'no', 'off', '']
def test_default_value_is_true(self):
"""Without env var set, enable_default_extensions should default to True."""
# Clear the env var if it exists
original = os.environ.pop('BROWSER_USE_DISABLE_EXTENSIONS', None)
try:
# Import fresh to get the default
from browser_use.browser.profile import _get_enable_default_extensions_default
assert _get_enable_default_extensions_default() is True
finally:
if original is not None:
os.environ['BROWSER_USE_DISABLE_EXTENSIONS'] = original
class TestConfigEnvVars:
"""Tests for browser profile env var configuration."""
def test_default_values_without_env(self, monkeypatch: pytest.MonkeyPatch):
"""Verify default values when environment variables are unset."""
monkeypatch.delenv('BROWSER_USE_DISABLE_EXTENSIONS', raising=False)
monkeypatch.delenv('BROWSER_USE_HEADLESS', raising=False)
assert _get_enable_default_extensions_default() is True
assert _get_headless_default() is None
@pytest.mark.parametrize(
'env_value,expected_enabled',
'env_var,getter,expected',
[
# Truthy values for DISABLE = extensions disabled (False)
('true', False),
('True', False),
('TRUE', False),
('1', False),
('yes', False),
('on', False),
# Falsy values for DISABLE = extensions enabled (True)
('false', True),
('False', True),
('FALSE', True),
('0', True),
('no', True),
('off', True),
('', True),
('BROWSER_USE_DISABLE_EXTENSIONS', _get_enable_default_extensions_default, False),
('BROWSER_USE_HEADLESS', _get_headless_default, True),
],
)
def test_env_var_values(self, env_value: str, expected_enabled: bool):
"""Test various env var values are parsed correctly."""
original = os.environ.get('BROWSER_USE_DISABLE_EXTENSIONS')
try:
os.environ['BROWSER_USE_DISABLE_EXTENSIONS'] = env_value
from browser_use.browser.profile import _get_enable_default_extensions_default
def test_env_var_truthy_values(
self,
monkeypatch: pytest.MonkeyPatch,
env_var: str,
getter,
expected: bool,
):
"""Test truthy env var values are parsed correctly."""
for val in TRUTHY_STRINGS:
monkeypatch.setenv(env_var, val)
assert getter() is expected, f'Failed for {env_var}={val}'
result = _get_enable_default_extensions_default()
assert result is expected_enabled, (
f"Expected enable_default_extensions={expected_enabled} for DISABLE_EXTENSIONS='{env_value}', got {result}"
)
finally:
if original is not None:
os.environ['BROWSER_USE_DISABLE_EXTENSIONS'] = original
else:
os.environ.pop('BROWSER_USE_DISABLE_EXTENSIONS', None)
@pytest.mark.parametrize(
'env_var,getter,expected',
[
('BROWSER_USE_DISABLE_EXTENSIONS', _get_enable_default_extensions_default, True),
('BROWSER_USE_HEADLESS', _get_headless_default, False),
],
)
def test_env_var_falsy_values(
self,
monkeypatch: pytest.MonkeyPatch,
env_var: str,
getter,
expected: bool,
):
"""Test falsy env var values are parsed correctly."""
for val in FALSY_STRINGS:
monkeypatch.setenv(env_var, val)
assert getter() is expected, f'Failed for {env_var}={val}'
def test_browser_profile_uses_env_var(self):
"""Test that BrowserProfile picks up the env var."""
original = os.environ.get('BROWSER_USE_DISABLE_EXTENSIONS')
try:
# Test with env var set to true (disable extensions)
os.environ['BROWSER_USE_DISABLE_EXTENSIONS'] = 'true'
@pytest.mark.parametrize(
'env_var,attr_name,truthy_val,falsy_val',
[
('BROWSER_USE_DISABLE_EXTENSIONS', 'enable_default_extensions', False, True),
('BROWSER_USE_HEADLESS', 'headless', True, False),
],
)
def test_browser_profile_and_session_env_var(
self,
monkeypatch: pytest.MonkeyPatch,
env_var: str,
attr_name: str,
truthy_val: bool,
falsy_val: bool,
):
"""Test that BrowserProfile and BrowserSession pick up env vars."""
# Test truthy env value
monkeypatch.setenv(env_var, 'true')
profile = BrowserProfile()
assert getattr(profile, attr_name) is truthy_val
session = BrowserSession()
assert getattr(session.browser_profile, attr_name) is truthy_val
from browser_use.browser.profile import BrowserProfile
# Test falsy env value
monkeypatch.setenv(env_var, 'false')
profile_falsy = BrowserProfile()
assert getattr(profile_falsy, attr_name) is falsy_val
session_falsy = BrowserSession()
assert getattr(session_falsy.browser_profile, attr_name) is falsy_val
profile = BrowserProfile(headless=True)
assert profile.enable_default_extensions is False, (
'BrowserProfile should disable extensions when BROWSER_USE_DISABLE_EXTENSIONS=true'
)
# Test with env var set to false (enable extensions)
os.environ['BROWSER_USE_DISABLE_EXTENSIONS'] = 'false'
profile2 = BrowserProfile(headless=True)
assert profile2.enable_default_extensions is True, (
'BrowserProfile should enable extensions when BROWSER_USE_DISABLE_EXTENSIONS=false'
)
finally:
if original is not None:
os.environ['BROWSER_USE_DISABLE_EXTENSIONS'] = original
else:
os.environ.pop('BROWSER_USE_DISABLE_EXTENSIONS', None)
def test_explicit_param_overrides_env_var(self):
"""Test that explicit enable_default_extensions parameter overrides env var."""
original = os.environ.get('BROWSER_USE_DISABLE_EXTENSIONS')
try:
os.environ['BROWSER_USE_DISABLE_EXTENSIONS'] = 'true'
from browser_use.browser.profile import BrowserProfile
# Explicitly set to True should override env var
profile = BrowserProfile(headless=True, enable_default_extensions=True)
assert profile.enable_default_extensions is True, 'Explicit param should override env var'
finally:
if original is not None:
os.environ['BROWSER_USE_DISABLE_EXTENSIONS'] = original
else:
os.environ.pop('BROWSER_USE_DISABLE_EXTENSIONS', None)
def test_browser_session_uses_env_var(self):
"""Test that BrowserSession picks up the env var via BrowserProfile."""
original = os.environ.get('BROWSER_USE_DISABLE_EXTENSIONS')
try:
os.environ['BROWSER_USE_DISABLE_EXTENSIONS'] = '1'
from browser_use.browser import BrowserSession
session = BrowserSession(headless=True)
assert session.browser_profile.enable_default_extensions is False, (
'BrowserSession should disable extensions when BROWSER_USE_DISABLE_EXTENSIONS=1'
)
finally:
if original is not None:
os.environ['BROWSER_USE_DISABLE_EXTENSIONS'] = original
else:
os.environ.pop('BROWSER_USE_DISABLE_EXTENSIONS', None)
@pytest.mark.parametrize(
'env_var,attr_name,env_val,explicit_arg,expected',
[
('BROWSER_USE_DISABLE_EXTENSIONS', 'enable_default_extensions', 'true', {'enable_default_extensions': True}, True),
('BROWSER_USE_DISABLE_EXTENSIONS', 'enable_default_extensions', 'false', {'enable_default_extensions': False}, False),
('BROWSER_USE_HEADLESS', 'headless', 'true', {'headless': False}, False),
('BROWSER_USE_HEADLESS', 'headless', 'false', {'headless': True}, True),
],
)
def test_explicit_parameter_overrides_env_var(
self,
monkeypatch: pytest.MonkeyPatch,
env_var: str,
attr_name: str,
env_val: str,
explicit_arg: dict,
expected: bool,
):
"""Test that explicit constructor parameters override env vars."""
monkeypatch.setenv(env_var, env_val)
profile = BrowserProfile(**explicit_arg)
assert getattr(profile, attr_name) is expected