Actor input primitives can diverge from the normal Browser Use action
handlers: offscreen clicks use stale coordinates, native dropdown
selection can silently fail, and literal keys can miss character events.
This change shares the existing input, keyboard, and dropdown paths and
fixes Actor's CDP input state.
- Measure click and hover coordinates after scrolling; preserve button
and modifier semantics, release pressed buttons on errors, and surface
ambiguous click timeouts.
- Make checkbox checking idempotent. Select native options by label or
value, including option groups, with disabled-option validation and
selection verification.
- Preserve empty append operations, support native date/time filling,
and report navigation errors.
- Track mouse position and held buttons for drag/multi-click operations;
add bounded key holds, screenshot clips, element scrolling, and
browser-host file-input primitives.
Validation: required pre-commit hooks, including Ruff and Pyright; local
headless Chrome assertions for offscreen targets, dropdowns and option
groups, checkboxes, text/date input, mouse/key cleanup, screenshots,
uploads, and failed navigation. These are controlled browser checks, not
a claim of universal website compatibility.
Validation refreshed on September 24 UTC at `95967882`: all required
pre-commit hooks passed (including Ruff and Pyright); focused existing
tests passed 13 with 7 skipped; local Chrome outcome assertions passed
for keyboard input, offscreen clicks/hover, native select and optgroup
behavior, checkbox idempotence, date input, held mouse state, and
cancellation cleanup. GitHub reports 129 successful checks and one
skipped documentation deployment.
## Why
The supported-models docs already document OpenAI-compatible providers
such as Qwen, ModelScope, and Novita via `ChatOpenAI` + `base_url`.
However, PZERO users currently have to infer the API host, environment
variable, and model ID conventions themselves.
Fixes#5579.
## What changed
Added a **PZERO** section under **OpenAI-Compatible APIs** in
`skills/open-source/references/models.md`.
The documentation includes:
- `ChatOpenAI` configuration with the PZERO `/v1` base URL
- `PZERO_API_KEY` environment variable and link to the PZERO agents page
- Default model: `deepseek-v4-flash`
- Notes on using `/v1` rather than `/v1/chat/completions`
- PZERO catalog model IDs without the `openai/` prefix
- `use_vision=False` for the text-only default model
- Link to the public PZERO model catalog
No provider implementation or code changes are required; this is a
documentation-only change.
## Testing
- [ ] Verified the new PZERO section matches the existing
Novita/ModelScope documentation format
- [ ] Optional: Tested the example with a valid `PZERO_API_KEY`
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds a PZERO section under OpenAI-Compatible APIs in
`skills/open-source/references/models.md` so PZERO users no longer have
to infer the base URL, env var, and model ID conventions. Fixes#5579.
- Documents `ChatOpenAI` with `base_url="https://api.pzero.studio/v1"`
and `api_key` read from `os.environ["PZERO_API_KEY"]`, so the key must
be set explicitly; links to the PZERO agents page for keys.
- Shows `deepseek-v4-flash` as the default model and notes that catalog
model IDs are passed without the `openai/` prefix.
- Notes the `/v1` base URL (not `/v1/chat/completions`) and the model
list endpoint at `GET https://api.pzero.studio/v1/models` (no auth
required).
- Warns that the default model is text-only, so set `use_vision=False`
unless selecting a vision-capable model.
- Docs-only change; no code changes required.
<sup>Written for commit 4b328e99c6.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5648?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
**Option B of two. Do not merge both.** Sibling: #5773, which deletes
the mentions instead.
## What is broken
`browser-use/bu-30b-a3b-preview` is still a live route in the Cloud
gateway
(`backend/llm_use/gateway/pricing.py` `MODAL_MODELS`, `service.py`
`_call_modal`), but
nothing is serving it. The Modal app behind that route,
`browser-use-llm-prod` in
`browser-use/deploy-llm` (`deploy.py:18`, 2x H200 `min_containers`), was
last deployed on
2025-12-16 and has not been deployed since.
Production, service `browser-use-production-llm-use`, last 90 days: 5
Modal client
initializations and 5 `Modal LLM call failed: Error code: 503`, paired
within 14 seconds.
Every observed call failed. The gateway turns that upstream 503 into a
generic HTTP 500 for
the caller, which is why it reads as "the model does not exist".
## Why not just delete it
The weights are public and people are using them.
https://huggingface.co/browser-use/bu-30b-a3b-preview is a public repo:
31B, 2.34k downloads
in the last month, 265 likes. The model exists. Only our hosting of it
does not.
## What this PR does
Says what the model actually is - open weights you run yourself.
- `examples/models/bu_oss.py` now starts from `vllm serve` and connects
with `ChatOpenAI`
against `http://localhost:8000/v1`. No `BROWSER_USE_API_KEY`, no
dependency on the dead
Cloud route. `BU_OSS_BASE_URL` / `BU_OSS_API_KEY` override the endpoint.
- `browser_use/llm/browser_use/chat.py` - the docstring now says Cloud
does not serve it.
- `skills/open-source/references/models.md` - drops the priceless OSS
pricing row and adds a
short self-hosting section with the vLLM command from the model card and
the weights URL.
Not touched: the gateway route, and
`tests/ci/models/test_llm_browseruse.py:85`.
## Checks
`ruff check`, `ruff format --check` and `git diff --check` clean.
The rewritten example was imported and constructed in this worktree:
`ChatOpenAI(model='browser-use/bu-30b-a3b-preview',
base_url='http://localhost:8000/v1', ...)`
resolves to provider `openai` with that base URL. No agent was run, and
no GPU was started,
so the end-to-end self-hosted run is not claimed here.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Updates docs and the example for `browser-use/bu-30b-a3b-preview` so the
model is presented as open weights users host themselves instead of a
Browser Use Cloud model.
- `examples/models/bu_oss.py` now uses a vLLM server and connects via
`ChatOpenAI`, with optional `BU_OSS_BASE_URL`/`BU_OSS_API_KEY`
overrides.
- The `ChatBrowserUse` docstring states the model is self-hosted only
and points to the example.
- The skills model table drops the Cloud pricing row and adds the vLLM
command and weights URL.
<sup>Written for commit 09dbfcbca9.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5774?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Why
A neutral audit of ChatGPT, Perplexity, Google AI Mode, Gemini and
Claude (50 answers) shows every engine describes Browser Use as "an
open-source Python library" and misses the cloud browser, the hosted
agent API and Browser Harness. The README is the most-crawled page we
have, so it should say what the product is and where the
machine-readable map lives.
## Change
One blockquote above "Which Browser Use do I need?" pointing AI agents
and crawlers to https://browser-use.com/llms.txt and
https://docs.browser-use.com/llms.txt, and naming the three surfaces in
one sentence.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_016rqDxQUeLj81KTgS46m72e
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds a blockquote to the README pointing AI agents and crawlers to the
product's `llms.txt` files so answer engines stop describing Browser Use
as a Python-only library.
The blockquote links to `browser-use.com/llms.txt` and
`docs.browser-use.com/llms.txt` and names all three product surfaces:
the open-source agent, the cloud browser, and the hosted agent API.
<sup>Written for commit d5b77ab66c.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5775?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
Name Claude Code, Codex, Hermes, and OpenClaw in the CLI quickstart
introduction so readers know where to paste the setup prompt.
Validation: pre-commit passed for README.md; git diff --check passed.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Names Claude Code, Codex, Hermes, and OpenClaw in the CLI quickstart so
readers know which agents can receive the browser setup prompt.
<sup>Written for commit b752b973d3.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5762?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
Use “Browser Use Benchmark v2” as the README section heading and remove
the repeated title from the plot image so the chart follows the heading
directly.
Add Browser Harness JS to Related Repositories after confirming that it
is public and MIT-licensed. The browser-use-js repository redirects to
Browser Use Pi, which is already listed.
The plot uses a lossless JPEG crop removing only its top 80 rows.
Decoded-pixel comparison confirms that every retained pixel matches the
original.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Replaces the generic “Benchmark” README heading with “Browser Use
Benchmark v2” and removes the repeated title from the plot so the chart
follows the heading directly. Adds the public, MIT-licensed Browser
Harness JS repository to Related Repositories; the existing Browser Use
Pi entry remains the destination for the redirected `browser-use-js`
repository.
- The plot is a lossless crop with only its top 80 title rows removed,
so all retained pixels remain unchanged.
- The documentation README links to the source image and records the
crop rationale.
<sup>Written for commit 2957f8a1c8.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5761?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
Restore the original branded README header, including the AI browser
agent artwork, downloads, GitHub stars, Discord, and other navigation
badges, while retaining the orange bridge painting beneath it.
Shorten the CLI introduction to one sentence and remove OpenCode from
the README and diagram. Update the diagram captions to “Easiest way to
scale up,” “Automate your own browser tasks,” and “Run fully local.”
Identify Browser Harness as our CLI and add Browser Use Pi, Video Use,
and macOS Harness to Related Repositories with one-line descriptions
verified against their public READMEs.
Validation: pre-commit and git diff checks passed. README anchors
resolve; both SVGs parse and contain the requested captions. The Python
examples are unchanged. Both diagram themes and the GitHub header were
visually inspected; header assets load on desktop and mobile, with no
page overflow at 390 pixels.
The README header used several rows of badges before explaining how to
start. Replace them with one purpose line, a short description of the
three entry points, and Quickstart/Demos/Docs/Cloud navigation. Add the
existing orange key-bridge painting from the Browser Use website beneath
the introduction.
Add a Related Repositories section near the bottom for Browser Harness,
Cloud SDK, and Benchmark. Move community links and the download badge
into the footer, preserve the driving-test GIF and product diagram, and
document the artwork source.
Validation: pre-commit and git diff checks passed. The artwork URL
returns the original website JPEG successfully (169 kB). All README
heading anchors and the three repository links resolve. GitHub renders
the artwork at 720 pixels on desktop and 324 pixels on mobile without
page overflow; the related-repositories table fits mobile width.
The README left the first Python example without a run command or
visible result, and its FAQ blurred the free library, paid models,
managed browsers, and fully hosted agent.
This update keeps OpenAI Luna as the default, adds commented BU2 and
cloud-browser alternatives with API-key instructions, and completes the
save/run/result flow. It refreshes the FAQ with current model and
authentication guidance, a self-contained custom-tool example, and
separate browser/agent hosting choices. Hermes and OpenClaw now appear
consistently in the CLI copy and both diagram themes.
Validation: pre-commit passed for all changed files; all three Python
snippets and the commented alternatives parse; model/browser
configuration and custom-tool registration/execution were checked
against the current library without model calls or browser provisioning;
all 23 Markdown links resolve. Both diagram themes were visually
inspected.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Refreshes the README quickstart and FAQ so the Python example is
runnable end-to-end and the free library is clearly separated from paid
models, managed browsers, and the hosted agent.
The quickstart now includes the run command and output, plus commented
BU2 and cloud-browser alternatives with API-key instructions. The FAQ
updates model recommendations, replaces the custom-tool snippet with a
runnable example, and clarifies browser vs. agent hosting choices.
Hermes and OpenClaw now appear consistently in CLI copy and both diagram
themes.
<sup>Written for commit bfc01ebfad.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5757?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
Make the three Browser Use paths consistent between the diagram and
quickstarts: fully hosted cloud, CLI, and Python library. Combine the
open source Browser Use agent and its Python library into one diagram
block connected directly to local and cloud browsers.
Rename the demo heading to “Navigate the web like a human does.” and
mention choosing a date and time. Keep the inline animation, remove the
separate Johannes link, expand the hosted-service description, and
remove the no-card text and Odysseys claim.
Validation: pre-commit, whitespace checks, Python snippet parsing,
numbered section order and anchors, SVG parsing, and single-block
structure in both diagram themes passed. Inspected both diagram themes
and the actual GitHub README on desktop and at 390px mobile width. All
three quickstart anchors resolve, the inline GIF remains present, and
there is no page overflow.
The README required opening X to watch the driving-test demo and did not
show how the Python library fits alongside the CLI and hosted service.
Embed the complete recording as a looping GIF and lead the product guide
with an editable diagram of all three paths.
- Fully hosted cloud runs OpenCode, Browser Use CLI, and a cloud
browser.
- The CLI connects existing agents, including Pi, to local or cloud
browsers.
- The Python library runs the Browser Use agent directly and can use
local or cloud browsers.
Remove the Playwright branch, the vertical diagram expansion, the three
artwork cards, and the duplicate open-source/cloud comparison. Follow
the graphic with the CLI quickstart, existing direct-OpenAI Luna
example, and hosted API docs. Simplify the $15 credit text and remove
the pricing text-file link.
The SVGs are adapted from the SDK's product diagram. The GIF preserves
Johannes's public 20.4-second recording at 960×540 and 10 fps; it is
hosted externally so it does not add media to repository clones.
Validation: pre-commit, whitespace checks, Python snippet parsing, local
image paths, SVG parsing, and visual inspection of both diagram themes
passed. Verified actual GitHub rendering on desktop and at 390px mobile
width, all three section anchors, and automatic GIF playback without
clicking. The public attachment downloads without authentication and
matches the local GIF byte for byte.
Demo preview:

The README's older form and extraction demos did not reflect the current
product or website. Feature Johannes's public driving-test booking demo
and explain the local, managed-browser, and hosted-agent options using
the website's existing artwork.
Reuse the current light/dark product diagrams from the SDK docs, with a
separate expandable vertical view for mobile. The Python quickstart
calls OpenAI directly with `ChatOpenAI(model='gpt-5.6-luna',
reasoning_effort='xhigh')`, an `OPENAI_API_KEY`, and explicit `.env`
loading. Preserve the benchmark plot and its qualification.
Sources: [driving-test
demo](https://x.com/mathisdittrich/status/2078619618265141560), [public
showcase](https://browser-use.com/showcase), and [product
guide](https://docs.browser-use.com/cloud/which-product). Asset
provenance is recorded in `static/readme/README.md`.
Validation:
- Pre-commit and `git diff --check` passed.
- All three Python snippets parse; local image references resolve and
all four SVGs parse.
- Verified actual GitHub rendering at desktop and 390px mobile widths,
including the vertical diagram disclosure. The product table fits
without horizontal scrolling.
- New website/docs links return HTTP 200. No live model invocation was
performed.
Replace the README's BU Bench V1 plot with the supplied Browser Use
Benchmark v2 artwork. Reduce the GPT-6 ASTRA label and 77.3% score text,
retaining the score value.
The caption links to `browser-use/benchmark`, explains that the
benchmark targets the hardest browser tasks, and notes that smaller
models can achieve very high success rates on easier tasks. Preserve the
benchmark repository's qualification that the plotted results cover a
60-task subset. Remove the outdated reference to the plot as an
open-source versus hosted-agent comparison.
Validation: pre-commit checks and `git diff --check` passed; the edited
chart's labels and plotted values were visually checked against the
supplied artwork.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Replaces the README's BU Bench V1 plot with the new Browser Use
Benchmark v2 artwork. Updates the caption and surrounding text to
describe the new benchmark, including its focus on the hardest browser
tasks and the 60-task subset used for the plotted results. Also removes
the outdated open-source vs hosted-agent comparison and the "see plot
above" reference in the cloud agent section.
<sup>Written for commit f40fa559fb.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5752?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
Passing OpenRouter-specific fields such as `extra_body={"provider":
{"order": ["test-provider"]}}` currently fails before any HTTP request
with `AsyncCompletions.create() got an unexpected keyword argument
'provider'`.
Both text and structured-output paths unpack `extra_body` into SDK
keyword arguments. Pass it through the SDK's `extra_body` parameter
instead, so custom fields become part of the JSON request body.
Regression tests exercise the real OpenAI SDK with an HTTPX mock
transport and verify custom fields reach the outgoing JSON for both
output modes. They also cover omitted and empty extra bodies and
structured response parsing.
Validation:
- Before the fix: 2 regression failures, 8 passing tests in the
OpenRouter model suite.
- After the fix: 15 tests passed across the OpenRouter model and
token-cost suites.
- All applicable pre-commit hooks passed, including Ruff and Pyright.
- No real API key or live provider request was used.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes passing OpenRouter-specific fields through the SDK's `extra_body`
parameter so custom fields like provider routing reach the HTTP request
body instead of failing before any request is sent.
- Text and structured-output paths now forward `extra_body` instead of
unpacking it into SDK keyword arguments.
- Adds regression tests with a mock HTTP transport covering both output
modes and omitted or empty `extra_body` values.
<sup>Written for commit d05053ed60.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5703?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Fix
Read the documented `BROWSER_USE_DISABLE_SECURITY` setting when
resolving local MCP browser configuration.
The default remains secure. An unset variable leaves the stored profile
unchanged; explicit `true` or `false` overrides it without rewriting the
config file. Existing explicit browser-session parameters still take
priority.
Only the config declaration/mapping and its regression tests change.
This does not add a tool-controlled security switch or alter the normal
BrowserProfile default.
## Verification
- Before the mapping fix: four new regression cases failed; fourteen
passed.
- After: all eighteen focused config tests pass, including unset,
persisted true/false and explicit environment overrides.
- The related profile arguments, extension-security and lazy-config
checks also pass: twenty-seven local cases in total.
- All applicable pre-commit hooks pass.
- Four fresh owned headless Chrome sessions exercised the actual MCP
browser initialization and two synthetic loopback origins. Unset and
false kept cross-origin fetch blocked with no `--disable-web-security`
flag. True enabled the flag and allowed the synthetic response. An
explicit false session override restored the block even with the
environment set to true.
- CI's hosted task evaluation reports 2/2, but both tasks log that they
skipped because `BROWSER_USE_API_KEY` is absent. Those are not counted
as agent or provider validation.
The local proof used no provider calls, shared browser profile or
production request. No release or deployment was performed. The explicit
true setting intentionally disables browser web-security checks, as
already documented.
## Summary
- use the canonical LLM model property throughout the related adapter
paths
- align the internal proxy with the canonical interface
- add focused regression coverage for a minimal adapter
## Tests
- `uv run pytest -q
tests/ci/test_beta_agent.py::test_beta_agent_runs_through_sdk_and_reuses_session_for_followup
tests/ci/test_agent_cloud_events.py`
- `uv run ruff check browser_use/beta/service.py
browser_use/agent/cloud_events.py tests/ci/test_agent_cloud_events.py
tests/ci/test_beta_agent.py`
- `uv run ruff format --check browser_use/beta/service.py
browser_use/agent/cloud_events.py tests/ci/test_agent_cloud_events.py
tests/ci/test_beta_agent.py`
- `uv run pre-commit run --all-files`
## Description
Closes#5539
The `uv`-generated `browser-use.exe` console-script launcher can be
blocked by Windows Smart App Control before Python starts. The package
already exposes `browser_use.cli:main`, but it had no package module
entry point, so users could not use the interpreter-based fallback.
## Type of Change
- [x] Bug fix (non-breaking change that fixes a known issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Add `browser_use/__main__.py` delegating to the existing CLI `main()`.
- Add a subprocess regression test proving `python -m browser_use doctor
--help` matches the existing CLI module entry point.
## Testing
- [x] Focused CLI tests pass (`uv run pytest -q
tests/ci/test_browser_use_cli.py`)
- [x] Ruff check passes (`uv run ruff check browser_use/__main__.py
tests/ci/test_browser_use_cli.py`)
- [x] Ruff format check passes (`uv run ruff format --check
browser_use/__main__.py tests/ci/test_browser_use_cli.py`)
- [x] Pre-commit passes on changed files (`uv run pre-commit run --files
browser_use/__main__.py tests/ci/test_browser_use_cli.py`)
- [x] New test added for the module entry point
- [ ] Full test suite
- [ ] Pyright on the full repository
### Test Output
```text
uv run pytest -q tests/ci/test_browser_use_cli.py
4 passed in 1.72s
uv run ruff check browser_use/__main__.py tests/ci/test_browser_use_cli.py
All checks passed!
uv run ruff format --check browser_use/__main__.py tests/ci/test_browser_use_cli.py
2 files already formatted
uv run pre-commit run --files browser_use/__main__.py tests/ci/test_browser_use_cli.py
all applicable hooks passed
```
## Real Behavior Proof
- Environment: Windows 10 host, CPython 3.12.14 managed by `uv`, source
checkout from `main`.
- Exact command / steps: `uv run python -m browser_use doctor --help`
and `uv run python -m browser_use.cli doctor --help`.
- Observed result: both commands exited successfully and printed `usage:
browser-use doctor [--fix-snap]`.
- Not tested: Windows 11 Smart App Control enforcement itself, because
this environment does not expose that policy state. The module path
avoids the generated console-script executable; signed distribution
remains an upstream packaging concern.
## Runtime Rollout Safety
- Rollout-managed feature(s): none.
- Minimum rollout channel: next package release.
- Stable/default behavior changed: no; this adds an alternate invocation
path.
- Kill switch / disable path: not applicable.
- Unsafe override required: none.
- Qualification impact: focused CLI tests and pre-commit checks.
- Rollback path: remove `browser_use/__main__.py` and its focused test.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have added tests that prove the fix is effective
- [x] New and existing focused tests pass locally
- [x] I did **not** edit `CHANGELOG.md`
## Screenshots (if applicable)
Not applicable.
## Additional Notes
This provides the interpreter-based workaround requested in #5539
without disabling Smart App Control or changing the existing
console-script behavior.
## Summary
- resolve `DEEPSEEK_API_KEY` and `CEREBRAS_API_KEY` explicitly instead
of letting `AsyncOpenAI` fall back to `OPENAI_API_KEY`
- raise a 401 `ModelProviderError` when neither an explicit `api_key`
nor the provider's env var is set
- extends the fix from #5599 to the two remaining adapters with the same
defect
## Details
`ChatDeepSeek` and `ChatCerebras` pass `api_key=self.api_key` into
`AsyncOpenAI` while pointing `base_url` at `api.deepseek.com` and
`api.cerebras.ai`. When `api_key` is `None`, the SDK resolves
`OPENAI_API_KEY` and authenticates those third-party endpoints with it.
This is the same defect described in item 2 of #5598 and fixed for
`ChatOpenRouter` and `ChatVercel` in #5599. `ChatOrcaRouter` already
guards against it, and states the hazard in an inline comment:
> `AsyncOpenAI` falls back to `OPENAI_API_KEY` when `api_key` is unset,
which would send an unrelated provider's key to the OrcaRouter endpoint.
`skills/open-source/references/models.md` already documents
`DEEPSEEK_API_KEY` (line 15) and `CEREBRAS_API_KEY` (line 18) as these
providers' env vars, and `config.py` exposes a `DEEPSEEK_API_KEY`
property — neither adapter read them. No docs change is needed; this
makes the code match what was already documented.
## Scope
I audited the nine adapters that authenticate with an API key at
construction time, using a script that sets `OPENAI_API_KEY` to a
canary, unsets every provider-specific variable, and inspects the
resolved key and `base_url` on the constructed client.
Five adapters point an OpenAI-SDK client at a non-OpenAI `base_url`.
Three already guard (`ChatOpenRouter`, `ChatVercel`, `ChatOrcaRouter`);
these two did not.
Verified unaffected and deliberately unchanged: `ChatGroq` and
`ChatAnthropic` use their own vendor SDKs and never resolved the canary.
`ChatOpenAI` resolves `OPENAI_API_KEY` while pointed at
`api.openai.com`, which is correct.
Before / after, with all other rows byte-identical:
```
LEAK the canary to a foreign endpoint : ['ChatDeepSeek', 'ChatCerebras']
LEAK the canary to a foreign endpoint : none
```
## Tests
- `uv run pytest -q tests/ci/models/test_llm_deepseek.py
tests/ci/models/test_llm_cerebras.py`
- `uv run pre-commit run --files browser_use/llm/deepseek/chat.py
browser_use/llm/cerebras/chat.py tests/ci/models/test_llm_deepseek.py
tests/ci/models/test_llm_cerebras.py`
The new tests fail on `main` and pass with this change:
tests/ci/models/test_llm_deepseek.py::test_provider_key_does_not_fall_back_to_openai_key
FAILED
AssertionError: assert 'wrong-provider-key' == 'deepseek-key'
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes DeepSeek and Cerebras clients sending `OPENAI_API_KEY` to their
endpoints when no explicit key is provided. Each adapter now uses
`DEEPSEEK_API_KEY` or `CEREBRAS_API_KEY` when no `api_key` is passed,
and raises a 401 `ModelProviderError` if neither is set.
- Explicit `api_key` takes precedence over the provider env var.
- Setups relying on `OPENAI_API_KEY` for these providers must switch to
the provider-specific env var.
- Adds regression tests for fallback prevention and explicit key
precedence.
<sup>Written for commit 24f60f71bb.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5672?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Summary
- normalize casing and whitespace before applying the existing `display:
none` filter for `<code>` elements
- prevent hidden code payloads from entering clean Markdown when inline
CSS uses valid case or whitespace variants
- add real-browser regression coverage for lowercase, mixed-case,
tab-separated, and `!important` declarations
Fixes#5637
## Testing
- `uv run pytest tests/ci/test_html_serializer_style.py -q` — 4 passed
- `uv run pytest tests/ci/test_markdown_extractor.py
tests/ci/test_markdown_chunking.py -q` — 38 passed
- `uv run pre-commit run --files
browser_use/dom/serializer/html_serializer.py
tests/ci/test_html_serializer_style.py` — all hooks passed
Each regression case verifies that Chrome computes the element as
`display: none` and that `extract_clean_markdown` omits the hidden
payload while preserving visible content.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes hidden code payloads from leaking into clean Markdown when inline
`style` uses valid case or whitespace variants like `Display: None` or
`DISPLAY:\tNONE`. Fixes#5637.
- Normalizes style casing and whitespace before applying the existing
`display: none` filter.
- Adds regression tests for lower-case, mixed-case, tab-separated, and
`!important` declarations.
<sup>Written for commit c9b3e5507a.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5640?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Why
`AWSBedrockMessageSerializer._is_url_image()` checked the raw URL
suffix, so otherwise supported image URLs were rejected when they
contained a query string or fragment. The same check was case-sensitive
for the HTTP scheme.
That prevents common signed/CDN image URLs from reaching the existing
Bedrock download path.
## What changed
- Parse URLs with `urlsplit()` and classify supported extensions from
the path, independent of query strings and fragments.
- Accept HTTP(S) schemes case-insensitively while rejecting missing or
malformed authorities without leaking parser exceptions.
- Prefer a recognized response `Content-Type`, then use the parsed path
extension when the server returns a generic or missing content type.
- Reject BMP URLs at the classifier because Bedrock Converse supports
JPEG, PNG, GIF, and WebP—not BMP.
- Add real local HTTP-server coverage for signed query preservation,
uppercase schemes, content-type precedence, generic/missing MIME
fallback, and malformed URLs.
## Verification
- `uv run pytest tests/ci/models/test_aws_bedrock_serializer.py
tests/ci/models/test_chat_anthropic_bedrock_client_config.py
tests/ci/models/test_chat_anthropic_bedrock_empty_content.py` — 22
passed
- `uv run pre-commit run --all-files` — all hooks passed, including Ruff
and Pyright
- `./bin/test.sh` — 1,157 passed, 34 skipped
- `git diff --check origin/main..HEAD` — clean
The regression was also reproduced against `origin/main`: a queried PNG
was rejected before the change. The HTTP tests use a real local server
rather than mocked download calls.
Fixes#5658
Developed with AI assistance; I reviewed the final diff and validation
results.
## Summary
- `pixtral_large` and `mistral_pixtral-large` resolved to
`pixtral-large-latest`, which Mistral no longer accepts
- remap both to `mistral-medium-latest`, the replacement named on
Mistral's model card
- add factory tests covering all five Mistral aliases
## Details
`get_llm_by_name` maps Pixtral Large in two places — `mistral_aliases`
for the unprefixed form and `mistral_map` for the provider-prefixed
form. Both pointed at `pixtral-large-latest`. Mistral lists Pixtral
Large (`pixtral-large-2411`) in its deprecated and retired models table,
and the identifier is no longer accepted.
Verified against `api.mistral.ai` on 2026-09-04, paid tier, identical
request shape for all five:
mistral-medium-latest HTTP 200
mistral-small-latest HTTP 200
codestral-latest HTTP 200
mistral-large-latest HTTP 200
pixtral-large-latest HTTP 400
{"type":"invalid_model","message":"Invalid model: pixtral-large-latest"}
The four passing controls rule out a credentials or tier issue: the same
key succeeds on every other alias in the map. Only
`pixtral-large-latest` is rejected, and Mistral reports it as an invalid
identifier rather than a gated one.
Pixtral Large's model card names Mistral Medium 3.5 as its replacement,
so both entries now resolve to `mistral-medium-latest`. Mistral Medium
3.5 is multimodal, so image support is preserved. Removing the aliases
outright was the alternative,
but that would surface the same failure as an opaque API error rather
than resolving to a working model — happy to change it if you'd rather
they be dropped.
The other four aliases were checked and left unchanged.
## Tests
- `uv run pytest -q tests/ci/models/test_llm_model_factory.py`
- `uv run pre-commit run --files browser_use/llm/models.py
tests/ci/models/test_llm_model_factory.py`
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Remaps both Mistral Pixtral Large aliases (`pixtral_large` and
`mistral_pixtral-large`) from the retired `pixtral-large-latest` to
`mistral-medium-latest`, which Mistral's model card lists as the
replacement, so requests no longer fail with an invalid model error.
- Mistral rejects `pixtral-large-latest` with HTTP 400; the other four
Mistral aliases still return HTTP 200.
- Mistral Medium 3.5 is multimodal, so image support is preserved.
- Adds tests covering all five Mistral aliases.
<sup>Written for commit 10729fbd68.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5673?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
Fixes#5628Fixes#5624
### Problem
`GoogleMessageSerializer.serialize_messages()` lost system instructions
in two independent ways, so this fixes both together — they live in the
same function and share the same state.
**1. Only the last system message survived (#5628).** With the default
`include_system_in_user=False`, each system message assigned to the same
variable:
```python
else:
system_message = message.content # overwrites the previous one
```
**2. The system text vanished entirely when an assistant turn came first
(#5624).** With `include_system_in_user=True`, the prepend was gated on
`not formatted_messages`:
```python
if include_system_in_user and system_parts and role == 'user' and not formatted_messages:
```
For a `system -> assistant -> user` ordering, `formatted_messages` is
already non-empty by the time the user message arrives, so the text was
never prepended — and because `system_message` stays `None` on that
branch, it was not returned as a system instruction either. It was
simply dropped.
### Fix
- Always collect into `system_parts`, regardless of the flag, so nothing
is overwritten.
- Drop the `not formatted_messages` guard. The documented behaviour
targets the first *user* message; what precedes it is irrelevant.
`system_parts` is cleared after use, so it still fires exactly once.
- Join whatever remains in `system_parts` into the returned instruction.
With `include_system_in_user=False` that is every system message, in
order. With it set, this only triggers when there was no user message to
merge into — previously that case discarded the text silently.
A single system message still produces the identical instruction string,
so existing callers see no change.
### Tests
New `tests/ci/models/test_google_serializer.py` covering the unchanged
single-message case, both messages surviving in order, the `system ->
assistant -> user` prepend, and the no-user-message fallback.
### Evidence
On `main` (fix reverted, new tests present):
```
test_single_system_message_becomes_the_system_instruction PASSED
test_all_system_messages_reach_the_system_instruction FAILED
test_system_text_is_prepended_even_when_an_assistant_message_comes_first FAILED
test_system_text_falls_back_to_the_instruction_when_there_is_no_user_message FAILED
3 failed, 1 passed in 6.63s
```
With this branch: `4 passed`.
`tests/ci/models` and `tests/ci/security` pass (212 tests), along with
`ruff check`, `ruff format`, and `pyright`.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes#5628 and #5624: `GoogleMessageSerializer.serialize_messages()`
dropped system messages in two ways, collapsing multiple system messages
to the last one and losing system text entirely when an assistant
message preceded the first user message. The serializer now preserves
every system message, prepends them to the first user message even after
an assistant turn, and returns leftover text as the system instruction
when no user message exists or when the first user turn has already been
serialized. Single system messages still produce the identical
instruction string, and regression tests cover all five cases.
<sup>Written for commit 007d63516c.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5664?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
Fixes#5632
`AnthropicMessageSerializer` compared data URL schemes and image media
types case-sensitively. `data:image/PNG;base64,...` kept the PNG bytes
but labeled them `image/jpeg`; `DATA:image/png;base64,...` was treated
as a remote URL source.
Scheme names (RFC 3986) and MIME type/subtype names (RFC 2045) are
case-insensitive. The parser now lowercases those for matching and emits
the canonical lowercase media types Anthropic expects, without changing
the base64 payload.
Verified with `uv run pytest
tests/ci/models/test_anthropic_data_url_case.py` (9 passed) and `ruff
check` on the touched files.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes#5632 by treating data URL schemes and media types
case-insensitively in `AnthropicMessageSerializer`.
`data:image/PNG;base64,...` previously relabeled PNG bytes as
`image/jpeg`, and `DATA:image/png;base64,...` was treated as a remote
URL; now both are parsed as base64 images with canonical lowercase media
types, leaving the base64 payload unchanged.
- Adds tests for case variants, canonical media type output, and remote
URL handling.
<sup>Written for commit 72fc5d2a7d.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5636?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Problem
The agent's file tools only write text formats, so any task that needs
to **upload an image/file** (a photo, a GIF, a document) is unwinnable
by construction — the model burns steps on `DataTransfer` JS hacks and
then gives up or fabricates. In evals this made whole upload-validation
tasks impossible for every model.
## Fix
`write_file` now accepts small image extensions (`.png .gif .jpg .jpeg
.webp`) where `content` is the **base64 of a valid image**. The bytes
are decoded and written to disk, so the file is a real, uploadable
image. A 1×1 PNG is ~92 base64 chars (~23 tokens), a GIF just 56 — cheap
for the model to emit.
- `Base64BinaryFile` writes decoded **bytes** to disk (not the base64
text) and `read()` returns a `[binary png file, N bytes]` stub, so
base64 never enters the agent prompt (`describe()` runs every step).
- **Strict** base64 decode — invalid content returns an error and **no
file is created**, so a corrupt 'image' can't pass upload's `file_size >
0` check.
- Registered in `_file_types` so `upload_file.get_file` resolves it by
basename (the load-bearing bit — upload only finds FileSystem files
whose extension is registered), and in `from_state` so restored sessions
keep it. Removed the now-supported extensions from
`UNSUPPORTED_BINARY_EXTENSIONS`.
- Untouched: text `write_file`, PDF/DOCX rendering (still
markdown→document), and rejection of other binaries (`.mp4 .zip .exe
…`).
## Testing
5 new tests + all 13 existing image tests pass (18 total): real PNG
magic bytes on disk, uploadable by basename, no base64 leak in
`describe()`, invalid-base64 rejected with no corrupt file, state
round-trip. Verified end-to-end that a written `.png` resolves through
the exact `upload_file` path.
Scoped deliberately to tiny fixtures (images only, strict decode) — not
a general binary-write feature.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
`write_file` can now create tiny binary images (.png, .gif, .jpg/.jpeg,
.webp) from base64 so upload flows use real files. We validate base64
and magic bytes, reject mismatched types, and only register files after
a successful write to avoid ghost entries.
- **New Features**
- Added `Base64BinaryFile` that decodes bytes to disk; `read()` returns
“[binary <ext> file, N bytes]” so base64 stays out of prompts.
- `write_file` accepts `.png .gif .jpg .jpeg .webp`; strict base64 +
magic-byte checks (incl. RIFF/WEBP) and extension match; invalid or
mismatched content errors and no file created; new files are registered
only on success.
- Binary images cannot be appended (append is rejected to prevent
corruption).
- Registered new types in `_file_types`/`from_state`, pruned
`UNSUPPORTED_BINARY_EXTENSIONS`, extended structured reads to include
`gif`/`webp`, and allowed these extensions in filename validation.
- Updated help text to document small-image support; other binaries
(.mp4, .zip, etc.) remain blocked.
<sup>Written for commit 115a3dd6e102740ce4abd59b5b800ca1d3346c01.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5280?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Summary
Pin the publish workflow's uv binary to 0.12.9 instead of resolving the
latest uv release at publish time.
This is split from #5667 so the Browser Use runtime dependency release
can proceed while this workflow-only supply-chain hardening receives the
required workflow-guardians review.
## Verification
- workflow diff is one exact version pin
- Browser Use 0.13.10 was locally built successfully using uv 0.12.9
- repository workflow checks will validate YAML and release setup
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Pins the publish workflow's `uv` binary to 0.12.9 and pins
`actions/checkout` and `astral-sh/setup-uv` to commit SHAs instead of
mutable version tags. This prevents unexpected `uv` or action updates
from affecting releases.
<sup>Written for commit 68f9a0992d.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5669?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Summary
- release Browser Use 0.13.10 with exact pins for browser-harness
0.1.13, Pydantic 2.13.5, pydantic-settings 2.15.0, MCP 2.1.1, and pypdf
6.16.2
- migrate both MCP servers and the MCP client/controller to the MCP 2
low-level server API
- pin Hatchling 1.32.0
- make pydantic-settings an explicit runtime dependency instead of
relying on MCP 1 to provide it transitively
The publish-workflow uv pin is split into #5669 because repository
policy requires workflow-guardians approval for `.github/workflows/**`.
This PR replaces #5667 with identical source tree changes but clean
history so the workflow-only rule is scoped correctly.
## Security
- pypdf 6.16.2 resolves all three open Dependabot alerts on main
(GHSA-23w6-3w8w-8484, GHSA-763m-79hh-57f2, GHSA-jp53-mhqp-8xcg)
- all external direct, optional, dev, and build dependencies in
pyproject.toml are exact-pinned
- all newly selected PyPI artifacts checked have digital provenance
attestations
- isolated installed-runtime `pip-audit`: no known vulnerabilities
## Verification
- all GitHub test shards passed on the identical code tree in #5667
- code style, type checker, CodeQL, GitGuardian, and Cubic review passed
- clean wheel installs and CLI smoke passed on macOS, Linux, Windows,
and uvx
- real stdio MCP initialization/list-tools passed for both `browser-use
--mcp` and `browser-use --cli-mcp`
- 204 focused PDF/save/filesystem tests passed
- wheel and sdist build passed
A local live OpenAI agent probe could navigate to example.com, but the
configured local OpenAI credential returns 401. GitHub model adapter
tests passed.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Pins release dependencies for 0.13.10 and migrates the MCP servers and
client to MCP 2's low-level request-handler API.
- Exact pins: `browser-harness==0.1.13`, `pydantic==2.13.5`,
`pydantic-settings==2.15.0`, `mcp==2.1.1`, `pypdf==6.16.2`,
`hatchling==1.32.0`.
- `pydantic-settings` is now an explicit runtime dependency instead of
coming transitively via MCP 1.
- The MCP server now reports the real package version instead of the
hardcoded `0.1.0`.
**Migration**
- Request handlers registered via `add_request_handler` replace the
`list_tools`/`call_tool` decorators.
- Tool schemas and results use MCP 2 field names: `input_schema`,
`is_error`, `read_only_hint`.
- Tool failures and unknown tool calls now return `is_error=True`
results instead of plain error text.
**Security**
- `pypdf==6.16.2` fixes three Dependabot alerts (GHSA-23w6-3w8w-8484,
GHSA-763m-79hh-57f2, GHSA-jp53-mhqp-8xcg).
<sup>Written for commit 743f630923.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5670?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Summary
- bump `browser-use` to 0.13.9
- pin `browser-harness==0.1.12`
- sync the bundled Browser Use skill from the immutable Browser Harness
v0.1.12 tag
- allow the documented iTerm app name in codespell
This ships the persistent local approval flow from browser-harness
v0.1.12, including the `mac-approve` guidance.
## Validation
- `uv run pre-commit run --all-files --show-diff-on-failure`
- isolated full suite: 1,136 passed, 34 skipped
- `uv build --wheel`
- clean wheel install verified browser-use 0.13.9, browser-harness
0.1.12, and packaged `mac-approve` skill guidance
- live Anthropic provider test passed (`claude-sonnet-4-6`)
- end-to-end agent smoke passed: opened Hacker News and extracted the
top three stories with points and comment counts in two steps, zero
errors
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Ships `browser-use` 0.13.9 with `browser-harness` 0.1.12. This replaces
the standalone `mac-approve` run with a persistent approval flow: keep
the original browser command running and call `mac-approve` with the
matching `BU_NAME`.
- Syncs the bundled Browser Use skill from the Browser Harness v0.1.12
tag.
- Documents `BH_TAB_MARKER=0` to leave page titles unchanged.
- Adds guidance to avoid slow per-character typing for long text.
- Allows the documented `iterm` app name in codespell.
<sup>Written for commit 0e0983e30e.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5666?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Summary
- extract BrowserSession handler registration into a reusable helper
- restore session handlers after `stop()` replaces the event bus
- restore session handlers after `kill()` replaces the event bus
- add regression coverage for both reset paths
## Problem
`BrowserSession.stop()` and `BrowserSession.kill()` clear the current
event
bus and replace it with a new `ResilientEventBus`.
The BrowserSession handlers are registered only from
`model_post_init()`,
which runs when the session object is constructed. The replacement event
bus
therefore has no handlers, including no `BrowserStartEvent` handler.
As a result, calling `start()` on the same BrowserSession after `stop()`
or
`kill()` dispatches the event without reconnecting or launching a
browser.
## Solution
Move BrowserSession handler registration into a dedicated helper and
invoke
it both during model initialization and after creating a replacement
event
bus.
The existing duplicate-handler checks remain in place.
## Testing
- Added a parameterized regression test covering both `stop()` and
`kill()`.
- Verified that the replacement bus is a new instance.
- Verified that its complete BrowserSession handler mapping matches the
original bus.
Commands run:
```bash
pre-commit run --files \
browser_use/browser/session.py \
tests/ci/browser/test_session_start.py
pytest \
tests/ci/browser/test_session_start.py::TestBrowserSessionEventSystem::test_event_bus_initialization \
tests/ci/browser/test_session_start.py::TestBrowserSessionEventSystem::test_session_handlers_registered_after_event_bus_reset \
tests/ci/browser/test_session_start.py::TestBrowserSessionEventSystem::test_event_handlers_registration \
tests/ci/test_event_bus_resilience.py -q
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes `BrowserSession` losing its event handlers when `stop()` or `kill()` replaces the event bus, so calling `start()` again now reconnects and launches as expected.
- **Bug Fixes**
- Re-registers session handlers after `stop()` or `kill()` creates a new `ResilientEventBus`.
- Keeps duplicate-handler safeguards.
- Adds regression tests covering both reset paths.
<sup>Written for commit 780274dc69. Summary will update on new commits.</sup>
<a href="https://cubic.dev/pr/browser-use/browser-use/pull/5230?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
Fixes#4471 (partial — addresses Part 1: Chromium launch watchdog
timeout)
## Problem
When Chrome/Chromium fails to start on headless Linux (e.g. due to
missing
sandbox capabilities, a missing virtual display, or absent system
dependencies), `_wait_for_cdp_url` previously polled the CDP endpoint
for the
entire 30-second timeout before raising a generic `TimeoutError`:
```
BrowserStartEvent (30s TIMEOUT)
BrowserLaunchEvent (30s)
DownloadsWatchdog (0s) OK
LocalBrowserWatchdog (30s) INTERRUPTED
```
This gave users no indication of *why* the browser failed to start or
how to
fix it — they just saw a timeout.
Additionally, `_wait_for_cdp_url` used the deprecated
`asyncio.get_event_loop()` inside an `async def`, which should use
`asyncio.get_running_loop()` per Python 3.10+ best practice.
## Solution
- Added an optional `process: psutil.Process | None = None` parameter to
`_wait_for_cdp_url`. When provided, the polling loop checks on every
iteration whether the browser process is still alive. If it has exited,
a
`RuntimeError` is raised **immediately** (< 0.1 s instead of 30 s) with
a
message that points users toward the likely fixes:
`--no-sandbox` for Docker/headless environments, or `Xvfb` for headless
Linux without a display.
- `psutil.AccessDenied` is silently ignored so the CDP poll continues
normally on systems where process inspection is restricted.
- `_launch_browser` now passes `process=process` to `_wait_for_cdp_url`.
- Replaced `asyncio.get_event_loop().time()` with
`asyncio.get_running_loop().time()` throughout the method.
## Testing
The change is backward-compatible: `process` defaults to `None`, so all
existing callers that don't pass a process are unaffected. The only new
behaviour is for the error path (process exits before CDP is ready),
which
previously resulted in a 30-second hang followed by an uninformative
`TimeoutError`.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes#4471 (part 1) by detecting early browser process exit in
`_wait_for_cdp_url`, replacing the 30s CDP timeout with an immediate,
actionable error when Chromium fails to start on headless Linux.
- **Bug Fixes**
- Added optional `process: psutil.Process | None` to
`_wait_for_cdp_url`; raises a descriptive `RuntimeError` with hints
(`--no-sandbox`, `Xvfb`) when the browser exits, ignoring
`psutil.AccessDenied`.
- `_launch_browser` now passes the process; default remains `None` for
backward compatibility.
- **Refactors**
- Replaced `asyncio.get_event_loop().time()` with
`asyncio.get_running_loop().time()`.
<sup>Written for commit f4e2bb1f74.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/4599?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Summary
- include the existing bounded image context when the interactive node
is itself an `img`, not only when an image is below an interactive
parent
- keep the existing three-context and 100-descendant limits, with the
direct image counting toward the context limit without consuming
descendant budget
- normalize URL-parser-ignored controls before rejecting `data:`
sources, and omit oversized raw image-context attributes before
processing
## Why
Follow-up to #5541 (and its fix for #4312). That change starts traversal
at `node.children`, but production clickability detection can mark an
`img` itself interactive through click listeners, interactive
attributes/roles, icon heuristics, or pointer cursor. Because `src` is
not in `DEFAULT_INCLUDE_ATTRIBUTES`, an unlabeled directly clickable
image still serializes without the filename context that #5541
introduced for child images.
The same sanitizer now handles the direct node. During adversarial
review, URL preprocessing was also aligned with browser behavior so
obfuscated `data:` schemes using C0 controls cannot expose inline
payloads through `image_src`.
## Validation
- exact-current-main red/green harness: direct image context is empty
before and contains the sanitized filename after
- isolated serializer regressions for query/fragment stripping,
browser-normalized `data:` rejection, oversized raw sources, and
direct-plus-descendant context limits
- `python3 -m py_compile` for both changed files
- repository-configured Ruff rules for changed code (apart from the
unchanged current-main import-order baseline in the newly merged test
file)
- repository-configured `ruff format --check` for both changed files
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Exposes sanitized image context for directly clickable `img` elements,
not just images nested under interactive parents.
- Direct images now contribute their own `alt`, `title`, `aria-label`,
and `src` filename to the LLM DOM while preserving the existing
three-context and 100-descendant limits.
- Normalizes `data:` URLs by stripping C0 controls and spaces before
rejecting them, matching browser behavior so obfuscated schemes don't
leak inline payloads.
- Skips oversized image context attributes (>4096 chars) before scanning
or serializing them.
<sup>Written for commit 4e09955e57.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5586?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
Fix send_keys handling for literal plus characters.
Previously, send_keys(keys="+") was parsed as an empty key combination,
causing empty key events instead of inserting +. Shortcuts such as
Control++ were also parsed incorrectly.
This change:
Sends literal + through the character-input path.
Supports shortcuts like Control++.
Preserves existing shortcuts such as Control+a and special keys like
Enter.
Prevents empty key events from being dispatched.
Testing
Added regression tests for +, C++, and Control++.
Verified existing Control+a and Enter behavior.
Focused test suite: 4 passed.
Fixes#5569
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes `send_keys` handling so a literal `+` is sent as a character
instead of an empty key event, and shortcuts like `Control++` work
correctly. Previously, `send_keys(keys="+")` dispatched empty key events
and `Control++` was parsed incorrectly.
- `+` and text containing `+` (like `C++`) now go through the
character-input path.
- `Control++` is recognized as a shortcut with `+` as the main key.
- Existing shortcuts like `Control+a` and special keys like `Enter`
behave as before.
- Adds regression tests for `+`, `C++`, and `Control++`.
Fixes#5569.
<sup>Written for commit 8043fec5e6.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5582?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
Fixes#5486
`switch` returned a non-error `ActionResult` on both of its failure
paths (a stale/unknown `tab_id`, and a `SwitchTabEvent` that produced no
result), so callers had no way to tell a failed switch from a real one.
The false "Switched to tab #..." claim was written into
`long_term_memory`, so subsequent agent steps reasoned from a tab that
was never actually reached.
This raises `BrowserError` on both failure paths instead, following the
same convention `upload_file` already uses in this file. The error
message preserves the actual underlying cause (e.g. the stale tab_id)
rather than a generic string, so `ActionResult.error` carries actionable
information back to the agent.
## Changes
- `switch()` now raises `BrowserError` on both failure paths (stale
`tab_id`, and a missing `SwitchTabEvent` result), instead of returning a
success-shaped `ActionResult`.
- Added regression tests covering the failing `tab_id` case and
confirming the happy path is unaffected.
## Test plan
- [x] Verified against a real Chrome instance: failure path sets
`ActionResult.error` with the actual cause, happy path unaffected
- [x] New tests added to `tests/ci/browser/test_tabs.py`
- [x] `ruff check` / `ruff format --check` clean
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Reports failed tab switches as errors instead of silent successes.
Previously `switch()` returned a success-shaped `ActionResult` for a
stale/unknown `tab_id` or when `SwitchTabEvent` produced no result; now
both paths raise `BrowserError` with the real cause so callers see
`ActionResult.error` and no false success.
- Uses `event_result(raise_if_any=True)` so handler failures surface
their actual cause instead of a generic message.
- Success path is unchanged; it still returns "Switched to tab #…".
- Adds regression tests for a nonexistent `tab_id`, the "no result" path
via an `event_bus.dispatch` monkeypatch, and the happy path; uses a
static `_TabActionModel` to satisfy pyright.
- Migration: after calling `switch`, check `ActionResult.error` instead
of inferring success from memory strings.
<sup>Written for commit f3a6becbf0.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5500?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Problem
Fixes#5647. The DOM the agent sees only carried the static `value`
attribute. When JavaScript, autofill, or a framework binding fills a
field, the value lives in the element property, so pre-filled inputs
looked empty and the agent retyped over them or skipped them. Form
filling is one of the most common tasks, so this shows up a lot.
## Fix
`DOMSnapshot.captureSnapshot` already returns `inputValue`, `textValue`,
and `inputChecked`. This reads them once per document, keeps them on
`EnhancedSnapshotNode`, and surfaces the live value as the `value`
attribute for `input` and `textarea` nodes so the serializer and
`get_meaningful_text_for_llm` pick it up without further changes.
Password, file, and hidden inputs are skipped.
## Proof
`tests/ci/test_dom_live_input_value.py` loads a page whose fields are
filled by script and asserts the values reach `selector_map` and the LLM
representation, and that a password value does not.
- on `main`: `AssertionError: assert None == 'Ada Lovelace'`
- with this change: passes; `test_dom_visibility.py` and
`test_ax_name_matching.py` still pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01UV3AvcViJQzAV75u67XZCF
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes#5647 by showing the live value of pre-filled inputs to the agent.
The DOM snapshot only carried the static `value` attribute, so fields
filled by JavaScript, autofill, or framework bindings looked empty; now
the live value is surfaced for `input` and `textarea` nodes, and
checkboxes/radios show their live checked state. Password, file, hidden,
payment (`cc-*`), and one-time-code fields never expose their live
value.
**Details**
- Reads `inputValue`, `textValue`, and `inputChecked` from
`DOMSnapshot.captureSnapshot` and stores them on `EnhancedSnapshotNode`.
- Adds a regression test covering script-filled values, sensitive
exclusions, and checked state.
<sup>Written for commit 3b12a946cc.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5650?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
Review follow-up: password, file, hidden, payment (cc-*) and one-time-code
fields never get their live value stored on EnhancedSnapshotNode, so it
cannot leak through __json__. Checkbox and radio inputs now show their live
checked state as the checked attribute.
DOM attributes only carry the static value=... written in the HTML. When
JavaScript, autofill, or a framework fills a field, the value lives in the
element property, which DOMSnapshot exposes as inputValue/textValue. The
agent saw such fields as empty and retyped or skipped them (#5647).
Read inputValue, textValue, and inputChecked from the snapshot, keep them on
EnhancedSnapshotNode, and surface the live value as the value attribute for
input and textarea nodes. Password, file, and hidden inputs are left alone.
Fixes#5647
## Summary
- keep `top_p` and `seed` on completion requests instead of passing them
to `AsyncOpenAI`
- load only `OPENROUTER_API_KEY` for OpenRouter and fail clearly when it
is missing
- turn empty `choices` responses into provider errors and preserve their
status codes
## Tests
- `uv run pytest -q tests/ci/models/test_llm_openrouter.py
tests/ci/test_openrouter_token_cost.py`
- `uv run pre-commit run --files browser_use/llm/openrouter/chat.py
tests/ci/models/test_llm_openrouter.py`
Closes#5598
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes OpenRouter and Vercel AI Gateway client setup and response
handling so unsupported params stay on completion requests and
credential or response gaps surface as provider errors instead of
crashes. Closes#5598.
- `top_p` and `seed` go on completion requests, not to `AsyncOpenAI`.
- Only `OPENROUTER_API_KEY` is loaded; missing keys raise a 401 instead
of falling back to `OPENAI_API_KEY`.
- Empty `choices` raise a 502, and structured parse failures keep their
original status codes for both providers.
- Vercel AI Gateway now requires `AI_GATEWAY_API_KEY` or
`VERCEL_OIDC_TOKEN` and raises a 401 when missing.
<sup>Written for commit ca69ebdf89.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5599?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Why
`set(CHROME_DEFAULT_ARGS) - set(ignore_default_args)` makes Chrome's
default launch flags process-dependent. With `PYTHONHASHSEED=0`, the
first flag changes from `--disable-field-trial-config` to
`--disable-background-networking`.
## Fix
Filter the canonical list in place. Ignored flags are still removed, but
every remaining flag keeps its existing order.
## Proof
- Red: both order regressions fail on current `main` with
`PYTHONHASHSEED=0`.
- Green: both pass with seeds `0`, `1`, `2`, and `12345`.
- Existing security side-effect tests pass.
- All changed-file pre-commit hooks pass, including Ruff, format,
Pyright, and private-key detection.
- `git diff --check` passes.
## Customer impact
No API or flag membership changes. The patch only removes
nondeterministic reordering. Existing `disable_security` feature merging
remains covered.
This is the maintainer-owned replacement for the stalled contributor
implementations in #5470 and #5505; their branches were not edited.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes nondeterministic ordering of Chrome default launch args.
Previously the set difference in `get_args` could reorder flags
depending on `PYTHONHASHSEED`; now ignored flags are filtered from the
canonical list in place, with no flag membership or API behavior
changes. Adds regression tests covering default-order preservation and
ignored-flag removal.
<sup>Written for commit df2701e524.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5621?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
Storage state JSON is written with `encoding='utf-8'` and
`ensure_ascii=False`, but both file read paths currently use the
platform locale. On Windows, non-ASCII cookie or Web Storage values can
therefore fail to decode, and the watchdog continues without restoring
the expected session state.
This passes `encoding='utf-8'` to both the existing-state merge read and
the asynchronous startup load read. It does not change caller-selected
paths, permissions, backups, or dict-based loading.
The focused regressions verify the exact encoding argument, preserve a
non-ASCII value through load, and retain both existing Unicode and newly
captured cookies through merge/write.
Validation: 2 focused red-green tests; Ruff check/format; `py_compile`.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Reads persisted storage state as UTF-8 so non-ASCII cookies and Web
Storage values no longer fail to decode, especially on Windows, and
sessions restore correctly. Both the merge read and async startup load
now use `encoding='utf-8'`; caller-selected paths, permissions, backups,
and dict-based loading are unchanged.
- **Bug Fixes**
- Force UTF-8 when reading existing JSON during merge and during async
startup load.
- Add tests asserting UTF-8 reads and preserving Unicode values and
cookie merge behavior.
<sup>Written for commit fa780c8e50.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5359?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Summary
- preserve caller-provided `available_file_paths` ordering when
downloads are discovered
- append new downloads in discovery order while deduplicating repeated
paths
- add a regression test covering existing inputs, ordered downloads, and
duplicate download entries
## Problem
`Agent._update_available_file_paths()` converted both the existing file
list and the latest downloads to sets, then rebuilt
`available_file_paths` from their union. Set iteration order is not the
order supplied by the caller or the browser, so the
`<available_file_paths>` block exposed to the model could change order
across processes.
## Fix
Use a set only for membership checks. Keep the original list as the
output source of truth and append each unseen download in first-seen
order.
## Verification
- regression test: RED on `main`, GREEN with this change
- `uv run pytest -q tests/ci/test_agent_download_paths.py
tests/ci/test_beta_agent.py -x`: 202 passed, 1 skipped
- `uv run pyright browser_use/agent/service.py
tests/ci/test_agent_download_paths.py`: 0 errors
- Ruff lint and format checks passed
- full `tests/ci` reached 479 passed / 33 skipped before one
pre-existing module-identity assertion failed; that exact test passed
when rerun alone
AI assistance was used to help inspect the codebase, draft the
regression test, and validate the change.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes unstable ordering of `available_file_paths` by preserving the
caller's list order and appending new downloads in discovery order.
Prevents reordering across processes and removes duplicates.
- **Bug Fixes**
- Replaced set-union logic with list + membership checks to keep
original order.
- Append only unseen downloads in first-seen order; ignore duplicates.
- Added regression test covering input order, download order, and
duplicate entries.
<sup>Written for commit e785b615d2.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5453?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Summary
- match URL negation terms as whole words instead of arbitrary
substrings
- share the negation matcher across the standard and beta agent URL
extractors
- add regression coverage for both a false positive (`notable`) and an
explicit negation (`do not`)
## Problem
The direct-navigation URL extractors used substring checks for `not`. As
a result, normal prose such as `Open this notable site:
https://example.com` was treated as a negated instruction, so the agent
skipped its initial navigation action.
## Testing
- `python -m pytest -o addopts=''
tests/ci/test_beta_agent.py::test_beta_agent_exposes_task_helper_methods`
- `ruff check browser_use/utils.py browser_use/agent/service.py
browser_use/beta/service.py tests/ci/test_beta_agent.py`
- `ruff format --check browser_use/utils.py browser_use/agent/service.py
browser_use/beta/service.py tests/ci/test_beta_agent.py`
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Matches URL negations as whole words during direct-navigation
extraction, so prose like "notable" no longer causes false skips.
Previously the extractors substring-matched "not"; now both agents share
a word-boundary regex for never/not/don't (straight or curly
apostrophes) in the 20-character pre-URL context.
- Adds `URL_NEGATION_PATTERN` and `has_url_negation` in
`browser_use/utils.py`; used by `browser_use/agent/service.py` and
`browser_use/beta/service.py`.
- Removes per-extractor `excluded_words`; keeps the 20-character window;
injects the scheme after the negation check; updates debug messaging.
- Adds regression tests allowing "notable" and blocking "Do not open …"
for both agents.
<sup>Written for commit c05a826af8.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5493?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## What
Treat keys inside a schema `properties` map as user field names before
applying schema-keyword rules. This keeps fields named `description` or
`properties` from retaining dangling `$ref` values.
Fixes#5603
## Test
- `uv run pytest -q tests/ci/models/test_llm_schema_optimizer.py`
- `uv run pre-commit run --files browser_use/llm/schema.py
tests/ci/models/test_llm_schema_optimizer.py`
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes schema optimization so user fields named `description` or
`properties` no longer keep dangling `$ref` values.
- Treats keys inside a `properties` map as field names before applying
schema-keyword rules.
- Simplifies `title` skipping, which now only applies outside
`properties`.
- Adds coverage for nested and aliased fields named after schema
keywords.
<sup>Written for commit b2507091aa.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5605?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Why
The official `cloud` skill still routes new hosted-agent integrations
through
API v2 or v3. Browser Use's current Cloud quickstart uses API v4, so
agents that
load the skill miss the current run, session, and workspace flow.
## What changed
- add a focused API v4 reference for Python, TypeScript, and REST
- route new hosted-agent setup to v4 while keeping v2 and v3 available
for existing integrations
- document the current browser boundary: v4 REST, explicit v3 SDK
namespace, and explicit stop required
- mark the old quickstart as the legacy v2 path
## Verification
- Cloud skill validator passed
- all 12 routed references resolve
- three Python examples parse
- six documented REST routes match the current v4 OpenAPI spec
- repository pre-commit passed for all three changed files
- three skill-install CI tests passed
- `git diff --check` passed
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds an API v4 reference to the `cloud` skill and routes new
hosted-agent setups to v4, while keeping v2 and v3 available for
existing integrations.
- Documents runs, sessions, workspaces, and direct browser control with
Python, TypeScript, and REST examples.
- Notes that the browser-management SDK wrapper still uses the `v3`
namespace, and that browsers must be stopped explicitly because closing
CDP does not stop billing.
- Marks the old quickstart as the legacy v2 path.
- Adds a CI test that verifies the v4 reference's workspace file-listing
examples.
<sup>Written for commit 442455465b.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5591?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
Fixes#5596
`ChatGoogle(include_system_in_user=True)` is meant to prepend the system
text to the first user message. When that message has list content it
replaced it instead: the system text became the only part sent, and the
user's text and images were dropped.
That is the shape an agent run always produces. `use_vision` defaults to
`True`, `AgentMessagePrompt.get_user_message()` returns list content as
soon as there is a screenshot, and `MessageHistory.get_messages()` puts
the state message first among the user messages. So with this flag on,
every step sent Gemini the system prompt and nothing else, with no error
anywhere.
The cause was structural: the content-part conversion lived in the
`else` arm of the prepend branch, so taking the prepend branch skipped
it. The fix computes the system text first and then always runs the
conversion, folding the system text into the leading text only for `str`
content, which is what it already did.
Before, on `main`:
```
include_system_in_user=False user ["'<browser_state>the page and the task live'", 'image']
include_system_in_user=True user ["'You are a browser agent.'"]
```
After:
```
include_system_in_user=False user ["'<browser_state>the page and the task live'", 'image']
include_system_in_user=True user ["'You are a browser agent.'", "'<browser_state>the page and the task live'", 'image']
```
### Tests
Three tests in `tests/ci/models/test_llm_google.py`, real objects only:
- `test_include_system_in_user_keeps_list_content_parts`: text part and
image survive alongside the prepended system text.
- `test_include_system_in_user_string_content_is_merged_into_one_part`:
parity guard for the `str` path, which was already correct and must stay
a single merged part. This one passes on `main` too.
- `test_include_system_in_user_keeps_the_agent_state_message`: builds
the message through the real `AgentMessagePrompt` with `use_vision=True`
and asserts the task and screenshot reach the serialized contents.
Reverting the change in `browser_use/llm/google/serializer.py` and
rerunning:
```
FAILED tests/ci/models/test_llm_google.py::test_include_system_in_user_keeps_list_content_parts
E AssertionError: user text was dropped
assert '<browser_state>the page and the task live here</browser_state>' in 'You are a browser agent.'
FAILED tests/ci/models/test_llm_google.py::test_include_system_in_user_keeps_the_agent_state_message
E AssertionError: the task never reached the model
2 failed, 1 passed
```
With the fix, `pytest tests/ci/models/test_llm_google.py` gives 8
passed, 1 skipped (the skip is the live `gemini-3-flash-preview` test,
no API key here).
### Checks
`pre-commit run --files browser_use/llm/google/serializer.py
tests/ci/models/test_llm_google.py` passes every hook, including ruff
check, ruff format and pyright at the versions pinned in
`.pre-commit-config.yaml`.
Full suite, `pytest tests/ci -q -o addopts="" --timeout=120`: 2 failed,
1108 passed, 34 skipped on this branch, against 2 failed, 1105 passed,
34 skipped on `main` (the +3 are the new tests). The two failures are
the same `tests/ci/test_beta_agent.py` isinstance checks in both runs;
they are order-dependent and pass on their own, and they are unrelated
to this change.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes#5596 by keeping the first user message's content when
`include_system_in_user=True` is set on `ChatGoogle`. Previously, when
the message had list content (text plus images, as the agent always
produces with vision on), the system text replaced it and the user's
text and screenshot were dropped; now the system text is prepended and
the user's parts are preserved.
- Reorders the serializer so content conversion runs for both string and
list content, merging system text only into the leading text part.
- Adds three tests covering list content, string content parity, and the
agent's actual message shape.
<sup>Written for commit 6a2b0f4f6c.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5597?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
- Read BROWSER_USE_HEADLESS env var via default_factory in
BrowserProfile
- Preserve fallback to display detection when env var is unset
- Add unit tests covering env var parsing and overrides
Fixes#5420
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Honors `BROWSER_USE_HEADLESS` in `BrowserProfile` so the env var now
sets the default headless mode instead of being ignored. Unset values
still fall back to display detection, and explicit headless args still
override. Hosts with `BROWSER_USE_HEADLESS` set may see their default
browser mode change.
- `BrowserLaunchArgs.headless` now uses a `default_factory` that parses
the env var case-insensitively: `'0'`/`'false'`/`'no'`/`'off'`/`''` =>
False, any other non-empty value => True, unset => None.
- `BrowserSession` inherits the value via its `browser_profile`.
- Tests for `BROWSER_USE_HEADLESS` and `BROWSER_USE_DISABLE_EXTENSIONS`
are deduplicated with pytest parametrization, covering profile/session
propagation and explicit overrides.
<sup>Written for commit 34d7978da9.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5475?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Summary
- replace the stale five-free-task claim with the current $15 one-time
signup credit
- fix the surrounding grammar
## Test
- `pre-commit run --files CLOUD.md`
- `git diff --check`
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Corrects the Cloud signup credit in `CLOUD.md` from five free tasks to
the current $15 one-time signup credit (if eligible), and fixes the
surrounding grammar.
<sup>Written for commit b4e68f19b2.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5602?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Summary
- replace removed pre-3.0 subcommands in the remote-browser skill with
the current piped-Python workflow
- document named Browser Use Cloud daemons for isolated sandbox browser
work
- add a regression test so deleted CLI commands do not return to the
skill
## Why
`skills/remote-browser/SKILL.md` still starts with `browser-use open`
and `browser-use state`. The current CLI exits 2 for both commands and
tells users to use `new_tab()` and `page_info()` through stdin.
## Test
- `uv run --python 3.11 pytest -q
tests/ci/test_browser_use_skill_install_docs.py`
- `uv run --python 3.11 pre-commit run --files
skills/remote-browser/SKILL.md
tests/ci/test_browser_use_skill_install_docs.py`
- `uv run --python 3.11 python
/Users/magnus/.codex/skills/.system/skill-creator/scripts/quick_validate.py
skills/remote-browser`
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Migrates the remote-browser skill to the current Browser Use CLI 3.0
workflow, replacing removed pre-3.0 subcommands (`browser-use open`,
`browser-use state`, etc.) with the piped-Python interface via stdin
heredocs.
- Documents named Browser Use Cloud daemons for isolated browser
sessions.
- Adds a regression test blocking retired CLI commands from returning to
the skill.
<sup>Written for commit 7ed622f0de.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5583?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Why
Every checked-in raw Cloud example targets retired `/api/v1` routes that
now return 404. The folder also shows the old bearer header and legacy
request fields.
## What
- replace the five broken V1 samples with one minimal V4 create → status
→ result flow
- use the current `X-Browser-Use-API-Key` header and terminal run
statuses
- load `.env` as the setup guide promises
- link optional fields to the live V4 OpenAPI spec instead of
duplicating claims that can drift
## Verification
- `uv run ruff check examples/cloud/01_basic_task.py`
- `uv run ruff format --check examples/cloud/01_basic_task.py`
- `uv run python -m py_compile examples/cloud/01_basic_task.py`
- mocked create → running → completed → summary contract test
- `uv run pre-commit run --files examples/cloud/README.md
examples/cloud/env.example examples/cloud/01_basic_task.py`
- live route probe: the three documented V1 routes return 404;
`/api/v4/runs` returns the expected missing-API-key 401 without
credentials
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Replaces the retired V1 Cloud API examples with a single V4 flow so the
checked-in samples stop hitting endpoints that return 404.
- Drops the fast-mode, structured output, proxy, and search API examples
in favor of one create → status → result flow.
- Switches to the `X-Browser-Use-API-Key` header and terminal run
statuses.
- Cancels a run that exceeds the configurable wait limit and rejects
non-positive or non-finite `BROWSER_USE_RUN_TIMEOUT` values.
- Loads `.env` via `dotenv` as the README setup promises.
- Points optional fields to the live V4 OpenAPI spec instead of
repeating them in the README.
<sup>Written for commit 1359fb22cb.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5578?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Summary
- Record WebSocket message-handler drops that occur while an automatic
reconnect is in progress.
- Schedule another automatic reconnect after the in-flight attempt
finishes.
- Clear pending reconnect state during session reset.
- Add a focused regression test.
Fixes#5366
## Tests
- `uv run pre-commit run --all-files --show-diff-on-failure`
- `uv run pyright`
- `uv run pytest -vxs tests/ci/test_browser_session.py`
- `uv run pytest -vxs tests/ci` — 483 passed, 28 skipped before an
unrelated existing failure in
`tests/ci/test_beta_agent.py::test_beta_agent_constructor_type_hints_match_browser_use_core_params`.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes WebSocket drops that happen during an in-flight reconnect by
recording them and automatically retrying after the attempt finishes.
Improves session stability and clears pending reconnect state on reset.
Fixes#5366.
- **Bug Fixes**
- Record WS drops during reconnect via `_reconnect_pending`, then
auto-schedule a follow-up reconnect once the attempt ends.
- Clear `_reconnect_pending` on session reset and wake any waiters.
- Add a focused regression test that verifies a follow-up reconnect is
scheduled after a drop during reconnect.
<sup>Written for commit 281cbc8c74.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5373?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
Fixes#4312
Image-only clickable elements can be indistinguishable in the serialized
DOM when they have no text or accessible label. Include bounded
descendant image context on the interactive parent, using
alt/title/aria-label and a query-stripped image filename while ignoring
data URLs.
Validation:
- uv run pytest -q tests/ci/test_image_only_dom_representation.py
tests/ci/test_dom_paint_order_serialization.py
- uv run ruff check browser_use/dom/serializer/serializer.py
tests/ci/test_image_only_dom_representation.py
- uv run ruff format --check browser_use/dom/serializer/serializer.py
tests/ci/test_image_only_dom_representation.py
- uv run pre-commit run --files browser_use/dom/serializer/serializer.py
tests/ci/test_image_only_dom_representation.py
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes#4312 by exposing bounded descendant image context in the
serialized DOM for image-only interactive elements. Previously,
interactive parents without text or labels serialized without context;
now they carry image alt/title/aria-label and a query/fragment-stripped
filename, with traversal and allocation bounds.
- Add `image_alt`, `image_title`, `image_label`, and `image_src`
(query/fragment-stripped filename) to interactive parents; skip `data:`
and query-only sources; cap each value to 100 chars.
- Limit to three descendant images and at most 100 descendants; traverse
lazily without copying child lists to bound allocations.
- Keep paint-order serialization unchanged; add tests for filename
propagation, query/fragment stripping, data URL filtering, traversal
limits, and non-eager traversal.
<sup>Written for commit fa29b0e05d.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5541?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Summary
- centralize reasoning-model pattern matching for the LLM adapters
- ignore empty or whitespace-only patterns instead of treating every
model as a reasoning model
- preserve the existing matching behavior for non-empty patterns across
OpenAI, Azure, and Vercel
This prevents an empty `reasoning_models` entry from silently dropping
sampling parameters such as `temperature` and `frequency_penalty`.
## Testing
- `py -m uv run pytest -q tests/ci/models/test_llm_openai.py` (7 passed,
1 skipped)
- `py -m uv run pytest -q tests/ci/models/test_azure_responses_api.py -k
ShouldUseResponsesAPI` (6 passed)
- `py -m uv run pyright browser_use/llm/base.py
browser_use/llm/openai/chat.py browser_use/llm/azure/chat.py
browser_use/llm/vercel/chat.py tests/ci/models/test_llm_openai.py`
- `py -m uv run ruff check browser_use/llm/base.py
browser_use/llm/openai/chat.py browser_use/llm/azure/chat.py
browser_use/llm/vercel/chat.py tests/ci/models/test_llm_openai.py`
Fixes#5543
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Ignore empty and whitespace-only reasoning-model patterns so regular
models are no longer treated as reasoning models, which previously
stripped sampling params like `temperature` and `frequency_penalty`.
Matching is centralized in `is_reasoning_model`, and the `openai`,
`azure`, and `vercel` adapters all use it so only real patterns apply.
**Bug Fixes**
- Tests cover empty-pattern cases and verify reasoning params are only
set on true matches.
Fixes#5543.
<sup>Written for commit 56b76ddbad.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5545?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
`write_file` tells the model that PDF content is markdown, but after
#5502 only `#` / `##` / `###` headings were styled. Bold, italic, inline
code, and bullets printed as literal `**syntax**`.
Keep the #5502 escape-first behavior (raw ReportLab markup cannot be
restored — that is the parse bug). After `html.escape`, convert a small
markdown subset to RML. Underscores stay literal so `snake_case` names
are not italicized. `*` is only emphasis when it actually wraps a span —
`2 * 3 * 4` and globs are left alone. Fenced blocks skip inline
conversion so shell backticks survive.
Heading offsets are shared with `DocxFile` via `_split_heading`.
Fixes#5538
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Render markdown emphasis in PDF exports from `write_file` so PDFs match
the documented markdown contract. Previously only `#`/`##`/`###`
headings rendered; now bold, italic, inline code, and bullets format
correctly while we keep the escape-first fix to avoid `ReportLab` parse
issues.
- Convert `**bold**`, `*italic*`, `` `inline` ``, and list bullets
(`-`/`*`) to RML; code fences bypass inline conversion; underscores
never italicize.
- Keep glob tokens and arithmetic stars literal (e.g., `*.txt`,
`**/foo/**`, `2 * 3 * 4`); inline code protects emphasis markers.
- Restore inline code via collision-safe placeholders so user text
cannot be mistaken for stash tokens.
- Share heading parsing with `DocxFile` to align `#`/`##`/`###` levels;
add tests for emphasis, bullets, fences, globs, and edge cases.
<sup>Written for commit f168606cb9.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5540?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
BrowserSession.clear_cookies() sends Network.clearBrowserCookies on the
root CDP client, which always fails:
RuntimeError: {'code': -32601,
'message': "'Network.clearBrowserCookies' wasn't found"}
Network is a per-target domain and is not dispatchable without a session
attachment; the root client has none. Storage is browser-level, and the
cookies() getter two lines above already uses Storage.getCookies()
there.
Measured against a live CDP endpoint:
Network.clearBrowserCookies() REJECTED -32601
Storage.clearCookies() OK
Storage.clearCookies(session_id=...) OK
Network.clearBrowserCookies(session_id=...) OK
The private _cdp_clear_cookies() already uses Storage.clearCookies with
a session_id, though its docstring still refers to
Network.clearBrowserCookies
- this looks like the public method was missed when that one was fixed.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes `BrowserSession.clear_cookies()` so it actually clears cookies.
Previously it sent `Network.clearBrowserCookies` on the root CDP client,
which always fails because Network is a per-target domain. Now it uses
`Storage.clearCookies`, a browser-level command that matches the private
`_cdp_clear_cookies()` method.
<sup>Written for commit 2f573908ca.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5562?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Summary\n\n- bump Click from 8.3.1 to 8.3.3, the fixed release for
PYSEC-2026-2132\n- bump MCP from 1.26.0 to 1.28.1, the common fixed
floor for PYSEC-2026-3481, PYSEC-2026-3482, and PYSEC-2026-3483\n- bump
pypdf from 6.14.2 to 6.15.0, the fixed release for PYSEC-2026-3655 and
PYSEC-2026-3656\n- keep the change limited to the three exact runtime
pins\n\n## Why\n\nThe current exact pins prevent downstream resolvers
from selecting patched releases. Updating to the minimum common fixed
versions clears the complete advisory set without unrelated dependency
churn.\n\n## Validation\n\n- uv lock --dry-run --python 3.11 (resolved
the full 400-package graph)\n- pre-commit run --files pyproject.toml
--show-diff-on-failure\n- uv build --wheel\n- verified the built wheel
metadata contains click==8.3.3, mcp==1.28.1, and pypdf==6.15.0\n\nNo UI
changes; a screenshot is not applicable.\n\nFixes #5524
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Updates the exact `click` and `pypdf` pins to patched versions to clear
known vulnerabilities; `mcp` is already at the fixed release.
- `click`: 8.3.1 → 8.3.3 (fixes PYSEC-2026-2132).
- `pypdf`: 6.14.2 → 6.15.0 (fixes PYSEC-2026-3655/3656).
- Change is limited to these two pins; no functional or API changes.
- Validation: `uv lock --dry-run --python 3.11`, pre-commit on
`pyproject.toml`, and wheel build confirming the new pins in metadata.
<sup>Written for commit c04983c2b7.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5526?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Summary
- DeepSeek retired `deepseek-chat` / `deepseek-reasoner` on 2026-07-24.
The default model is now `deepseek-v4-flash`. Details:
https://api-docs.deepseek.com/news/news260424/
- Add a `thinking` flag for V4 (default `False`) and send
`extra_body.thinking` to explicitly disable server-side thinking (on by
default), matching the previous `deepseek-chat` behavior
- Update the example and skills docs to use the new model name
## Changes
- `browser_use/llm/deepseek/chat.py`: default `model=deepseek-v4-flash`;
add `thinking: bool = False` and `_request_kwargs()`
- `examples/models/deepseek-chat.py`: switch the example to
`deepseek-v4-flash`
- `skills/open-source/references/models.md`: sync the docs example
## Why
The old model IDs are no longer valid; without this update, the default
`ChatDeepSeek()` call may fail. `deepseek-v4-flash` enables thinking by
default, which changes latency and output shape, so we disable it by
default for compatibility.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Updates the DeepSeek default model to `deepseek-v4-flash` because
DeepSeek retired `deepseek-chat` and `deepseek-reasoner` on 2026-07-24.
Adds a `thinking` flag (default `False`) to keep the prior non-thinking
behavior.
**Changes**
- Sends `extra_body.thinking` to explicitly disable server-side
thinking, preserving prior latency and output shape; only applies to V4
model names.
- Extracts request-param construction into `_request_kwargs()` so the
constructor keeps working positionally.
- Updates the example script and models doc to use `deepseek-v4-flash`.
<sup>Written for commit 2b66d1f1c0.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5563?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Why
The Cloud tools guide still showed 12 commands removed by CLI 3.0,
including `browser-use open`, `state`, `click`, and `eval`.
This is already breaking downstream agent docs. [cli-printing-press
issue #4256](https://github.com/mvanhorn/cli-printing-press/issues/4256)
reproduced the same failure after its browser capture guide copied the
old command surface.
## What changed
- Replaced the removed preset commands with Python piped over stdin.
- Added the real Browser Use Cloud flow: `start_remote_daemon`, one
stable `BU_NAME`, and `stop_remote_daemon`.
- Used the current helpers: `new_tab`, `page_info`, `fill_input`,
`press_key`, `js`, and `capture_screenshot`.
- Called out the removed flags so agents do not translate the old
examples literally.
## Checks
- `uv run pytest tests/ci/test_browser_use_cli.py
tests/ci/test_browser_use_skill_install_docs.py -q` (`6 passed`)
- CLI 3.0 Cloud helper import check
- Legacy `browser-use open --help` rejection check
- Every documented Python block parsed with `ast.parse`
- No old preset command remains in the guide
- `git diff --check`
## What changed
The Cloud section now links to the rerunnable-scripts guide and says
what the feature does: save repeated data work, fetch live data on later
runs, and repair the script when a site changes.
The old pricing-page example is gone.
## Checks
- applicable pre-commit checks passed for `README.md`
- `git diff --check`
## Problem
`create_history_gif()` opens PIL Image objects via `Image.open()` in a
loop (one per screenshot) and in `_create_task_frame()`, but never
closes them. In long agent runs with many steps, dozens of large
screenshot images accumulate as unclosed file descriptors.
The `logo` image (line 127) is also never closed.
## Fix
- Close all images in the `images` list after the GIF is saved
- Close the `logo` after it's no longer needed
- Close the `template` in `_create_task_frame()` after extracting its
size
## Test plan
- [ ] Existing tests pass: `pytest tests/`
- [ ] Create a GIF from a multi-step agent run — should complete without
resource warnings
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Close images during GIF creation to prevent file descriptor leaks in
long agent runs. Ensures deterministic cleanup even if GIF saving fails,
without changing output.
- **Bug Fixes**
- Ensure deterministic cleanup: wrap GIF save in try/finally and close
all images (`images`, `logo`, and the original screenshot when an
overlay is created).
- Close `template` in `_create_task_frame()` after reading its size.
<sup>Written for commit 41b24ec55e.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5105?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Symptom
`MCPClient.mcp_action_wrapper()` calls `self.session.call_tool(...)`,
which returns an MCP-spec `CallToolResult` — a `CallToolResult` includes
an `isError: bool` field that signals an *application-level* tool
failure (e.g. a `read_file` tool returning `"File not found"`) even
though the RPC call itself succeeded. That field was never checked.
`_format_mcp_result()` unconditionally stringifies `result.content` and
the wrapper returns it as a success-shaped `ActionResult` with no
`error` field set — silently hiding tool-reported failures from the
agent (both the param-model and no-param wrapper variants have this
bug).
## Fix
Check `result.isError` right after formatting the content, at both call
sites, and route to the same `ActionResult(error=..., success=False)`
pattern this file already uses elsewhere (connection failures, RPC
exceptions) instead of always treating `content` as a successful result.
## Verification
- Added `tests/ci/test_mcp_client_error_result.py`:
- `test_mcp_tool_isError_true_is_surfaced_as_action_result_error` —
confirmed **red** against the pre-fix code (`ActionResult(success=None,
error=None, extracted_content='File not found: ...')` — looked like a
success), now **green**.
- `test_mcp_tool_isError_false_still_succeeds` — sanity check that
normal successful calls are unaffected.
- `uv run pytest tests/ci/test_mcp_client_error_result.py
tests/ci/security/test_mcp_allowed_domains.py
tests/ci/test_action_timeout.py tests/ci/test_rerun_ai_summary.py` — 121
passed, no regressions.
- `ruff check` + `ruff format --check` + `pyright` (basic mode) clean on
both changed files.
## AI disclosure
Bug found and fix implemented with Claude Code assistance; verified
(red→green tests, lint/type-check, diff review) by a human before
submission.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Surfaced MCP tool application-level errors by mapping
`CallToolResult.isError` to a failed `ActionResult`, so agents no longer
treat tool-reported failures as successes. Successful calls are
unchanged.
- **Bug Fixes**
- Check `result.isError` in both `mcp_action_wrapper` variants and
return `ActionResult(error=..., success=False)`.
- Format error messages with tool name plus extracted content.
- Added tests for `isError=True` and `isError=False` paths, including
parameterized tool errors.
<sup>Written for commit 28fe857d9f.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5235?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
Closes#5239.
## Why
The `browser-use --mcp` tool catalogue ships without any MCP
annotations, so clients that gate execution on hints treat every tool as
potentially destructive. As reported in #5239, Codex CLI with
`approval_policy=never` auto-cancels even `browser_get_state`, which
makes the server unusable in non-interactive runs.
## What
Adds `annotations=types.ToolAnnotations(readOnlyHint=True)` to the five
tools whose handlers only read state: `browser_get_state`,
`browser_get_html`, `browser_screenshot`, `browser_list_tabs`,
`browser_list_sessions`. I read each handler in
`browser_use/mcp/server.py` before annotating it.
Two deliberate choices, happy to adjust if you see them differently:
- `browser_extract_content` stays unannotated. It dispatches the
`extract` action through `Tools.act()` with a FileSystem handle and can
write extraction artifacts, so it is not provably read-only.
- A cold-start call to an annotated tool still lazy-launches the browser
session via `_execute_tool`. I kept the hint since that is idempotent
bootstrapping rather than a mutation of the page being inspected, and it
matches what the reporter validated with their proxy experiment. The one
exception is `browser_list_sessions`, which never launches anything.
The scope here is only the `--mcp` surface from the issue. The newer
`--cli-mcp` surface was left alone on purpose since that code is being
actively reworked.
## Demo
Regression test fails on main's catalogue and passes with the fix:
```
$ git stash -- browser_use/mcp/server.py # main's tool catalogue
$ uv run pytest -q tests/ci/test_mcp_tool_annotations.py
FAILED tests/ci/test_mcp_tool_annotations.py::test_read_only_tools_advertise_read_only_hint
$ git stash pop
$ uv run pytest -q tests/ci/test_mcp_tool_annotations.py
2 passed
```
`tools/list` after the fix advertises exactly these as read-only:
```
readOnly: ['browser_get_html', 'browser_get_state', 'browser_list_sessions', 'browser_list_tabs', 'browser_screenshot']
```
The new test pins the set in both directions: read-only tools must
advertise the hint, and no state-changing tool may ever carry it.
## Checks
Ran locally: `uv run pre-commit run --all-files`, `uv run pyright` (0
errors), `uv run pytest tests/ci/test_mcp_tool_annotations.py
tests/ci/security/test_mcp_allowed_domains.py` (5 passed).
---
quick note: I'm a college freshman trying my best to contribute for the
greater good :) I worked through this with Claude Code (we read each
tool handler together to decide the read-only set, and I ran the lint,
type check and test gates locally). apologies in advance if anything
looks off, if there are mistakes I would genuinely love to learn from
them.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Marks read-only MCP tools with `annotations.readOnlyHint=True` so
hint-gated clients (e.g., Codex CLI with `approval_policy=never`) don’t
auto-cancel safe calls. Fixes#5239 and adds a regression test to pin
the read-only set.
- **Bug Fixes**
- Annotated: `browser_get_state`, `browser_get_html`,
`browser_screenshot`, `browser_list_tabs`, `browser_list_sessions`.
- Left `browser_extract_content` unannotated (can write artifacts via
`extract`).
- Added `tests/ci/test_mcp_tool_annotations.py` to ensure read-only
tools advertise the hint and mutating tools never do.
- Scope limited to `--mcp`; `--cli-mcp` unchanged.
<sup>Written for commit d05aa1cfbc.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5246?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
## Problem
`PaintOrderRemover.calculate_paint_order()`
(`browser_use/dom/serializer/paint_order.py`) correctly computes, for
every node with layout paint-order and bounds info, whether it is fully
covered by another node painted on top of it (e.g. text underneath an
open modal, an off-canvas menu, a tab panel that's present-but-hidden
via overlapping z-index rather than `display:none`, a tooltip, etc). It
records the result on `SimplifiedNode.ignored_by_paint_order`.
That flag was only ever consulted in one place:
`_assign_interactive_indices_and_mark_new_nodes()`, which uses it to
avoid handing out a clickable index for a covered button. But
`DOMTreeSerializer.serialize_tree()` — the function that actually
renders the tree into the text string sent to the LLM every step (via
`SerializedDOMState.llm_representation()`, called from
`agent/prompts.py`, `agent/service.py`, `actor/page.py`,
`browser/session.py`) — never checked it for `TEXT_NODE`s. Text nodes
were gated only by their own CSS `is_visible`
(opacity/display/viewport), which stays `True` even when the element is
completely covered by something else painted on top.
**Concrete failure scenario**: a page has two overlapping text blocks at
the same screen position — e.g. content sitting underneath an open
modal, where the modal covers it via z-index/position rather than
`display:none`. Both text nodes are individually `is_visible=True`
(they're not `display:none` and are inside the viewport), but
`PaintOrderRemover` correctly marks the underneath one
`ignored_by_paint_order=True`. Before this fix, `serialize_tree()`
printed *both* lines of text into the LLM's DOM snapshot, even though
only the top one is actually visible on screen — misleading the model
into "reading" page content that isn't really there, on any page using
overlap-based hide/show (modals, dropdowns, carousels, tab panels).
## Fix
Check `node.ignored_by_paint_order` alongside `is_visible` in the
`TEXT_NODE` branch of `serialize_tree()`, matching the same acceptance
check already used for interactive-index assignment.
## Testing
- Added `tests/ci/test_dom_paint_order_serialization.py`, which
constructs two overlapping `EnhancedDOMTreeNode` TEXT_NODEs directly (no
browser needed, following the existing `tests/ci/test_dom_visibility.py`
pattern), runs the real `PaintOrderRemover` against them, and asserts
the covered text is excluded from `DOMTreeSerializer.serialize_tree()`'s
output while the covering text is still included.
- Confirmed red→green: reverting only `serializer.py` reproduces the
exact leak (`assert 'HIDDEN BEHIND MODAL' not in ...` fails, both lines
present); reapplying passes.
- Full fast test suite (`tests/ci/` minus files that construct a live
`BrowserSession`): 493 passed, 7 skipped (unrelated, need provider API
keys) — matches pre-change baseline.
- `ruff check`, `ruff format --check`, `pyright`: all clean.
- xref: checked open PRs touching
`paint_order`/`ignored_by_paint_order`/`serialize_tree` — #5159
("perf(dom): speed up paint-order filtering") only touches
`paint_order.py`'s computation logic, not `serializer.py`'s consumption
of the flag at the TEXT_NODE site, so this is complementary, not a
duplicate.
## AI-Generated disclosure
Found via an AI-assisted code review pass (Claude Code) over
`browser_use/dom/`. I personally traced `ignored_by_paint_order`'s
single existing call site, confirmed the gap at the TEXT_NODE branch,
reproduced the leak with a standalone construction (no live browser
needed), verified the fix, and ran the fast test suite plus
lint/format/typecheck before submitting.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Prevents text hidden behind overlays from reaching the LLM by skipping
paint‑order‑occluded `TEXT_NODE`s in the DOM snapshot. The snapshot now
matches on-screen content for modals, dropdowns, and tab panels.
- **Bug Fixes**
- In `DOMTreeSerializer.serialize_tree()`, include `TEXT_NODE`s only
when `is_visible` and not `ignored_by_paint_order`.
- Align with interactive-index logic to avoid leaking covered content.
- Add `tests/ci/test_dom_paint_order_serialization.py` to verify covered
text is excluded and top-layer text remains.
<sup>Written for commit 561387adcc.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browser-use/browser-use/pull/5225?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->