10299 Commits
Author SHA1 Message Date
MagMueller b1028339e9 Merge remote-tracking branch 'origin/main' into HEAD 2026-09-04 23:59:51 -07:00
Magnus Müller d5453ae80e fix(llm): resolve DeepSeek and Cerebras API keys explicitly (#5672)
## 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. -->
2026-09-04 23:49:49 -07:00
Growth Radar QA 24f60f71bb Merge remote-tracking branch 'origin/main' into HEAD 2026-09-04 23:43:34 -07:00
Magnus Müller 1706dd4e37 fix(dom): match hidden code styles case-insensitively (#5640)
## 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. -->
2026-09-04 23:36:45 -07:00
Magnus Müller c9b3e5507a Merge branch 'main' into fix/hidden-code-style-case 2026-09-04 23:34:04 -07:00
Magnus Müller 2b696cbdc5 fix(aws): support query strings in Bedrock image URLs (#5676)
## 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.
2026-09-04 23:32:30 -07:00
Magnus Müller d283202160 Merge branch 'main' into fix/bedrock-image-url-query 2026-09-04 23:30:34 -07:00
Magnus Müller 9b8dccce92 fix(llm): remap retired Mistral pixtral-large alias (#5673)
## 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. -->
2026-09-04 23:23:09 -07:00
Magnus Müller 10729fbd68 Merge branch 'main' into fix/mistral-retired-pixtral-alias 2026-09-04 23:20:39 -07:00
Magnus Müller cfe10a2358 fix(llm): stop dropping system messages in the Gemini request (#5664)
Fixes #5628
Fixes #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. -->
2026-09-04 18:54:55 -07:00
Magnus Müller 007d63516c Merge branch 'main' into fix/5624-5628-google-system-messages 2026-09-04 18:53:22 -07:00
Magnus Müller e8fbd19f44 fix(anthropic): treat data URL scheme and media type case-insensitively (#5636)
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. -->
2026-09-04 17:50:32 -07:00
Magnus Müller 72fc5d2a7d Merge remote-tracking branch 'origin/main' into pr5636 2026-09-04 17:47:55 -07:00
Magnus Müller 20aad72c73 Merge remote-tracking branch 'origin/main' into pr5640 2026-09-04 17:40:58 -07:00
MagMueller da5be5cc75 fix: align beta cloud event proxy model property 2026-09-04 17:18:42 -07:00
MagMueller dee4a59725 fix: use canonical model in cloud task events 2026-09-04 17:18:42 -07:00
Mohil-Ahuja eae129ac51 test: build the dynamic action via model_validate for static analysis 2026-09-04 16:25:41 -07:00
Mohil-Ahuja 63d13371fb fix(agent): redact sensitive data nested below list boundaries in saved history
AgentHistory._filter_sensitive_data_from_dict only recursed one level into
lists: a string or dict directly inside a list was filtered, but a list inside
a list (or any deeper container) was returned untouched. An action parameter
shaped like {"input": {"rows": [["token-123"]]}} was therefore written to the
history file with the secret intact.

Replace the hand-rolled one-level walk with a single recursive value
dispatcher that handles str/dict/list/tuple, mirroring how
Registry._replace_sensitive_data walks params on the injection side, so the
redaction and injection paths cover the same container shapes.

Fixes #5623
2026-09-04 16:25:41 -07:00
Magnus Müller 8600af0d4f filesystem: write_file can create tiny binary images (png/gif/jpg/webp) for upload flows (#5280)
## 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. -->
2026-09-04 12:53:24 -07:00
MagMueller d828d83d7a docs(tools): list jpeg in binary image support 2026-09-04 12:52:09 -07:00
MagMueller aaeded1c0a test: narrow restored file type for pyright (Base64BinaryFile); ruff format 2026-09-04 12:51:18 -07:00
MagMueller 9c2641832a binary write: validate magic bytes + no ghost file on failed write (cubic review)
- Reject valid base64 that isn't actually an image (e.g. aGVsbG8= -> b'hello'),
  and reject correct image bytes under the wrong extension (magic mismatch), so
  upload flows never receive a corrupt file.
- Register a new file in self.files only after a successful write, so a rejected
  binary write leaves no ghost entry in list_files()/state.
- Binary files cannot be appended (base64 fragments would corrupt).
- Strengthen tests: content-integrity round-trip, non-image base64, wrong-magic,
  and no-ghost assertions.
2026-09-04 12:51:18 -07:00
MagMueller 24dc68f9ea test: update filesystem validation for supported image extensions 2026-09-04 12:51:18 -07:00
MagMuellerandClaude Fable 5 257bcfd5f2 filesystem: let write_file create tiny binary images (png/gif/jpg/webp) from base64
Upload-validation flows were unwinnable: the agent's file tools are text-only, so
'upload a photo/gif' tasks could never be attempted. write_file now accepts small
image extensions where content is the base64 of a valid image (a 1x1 PNG is ~92
base64 chars, ~23 tokens); the bytes are decoded and written to disk so the file
is a real, uploadable image.

- Base64BinaryFile writes decoded BYTES (not base64 text) to disk; read() returns
  a '[binary png file, N bytes]' stub so base64 never enters the prompt (describe()
  runs every step).
- Strict base64 decode: invalid content is rejected with an error and no file is
  created, so a corrupt 'image' can't slip through upload's file_size>0 check.
- Registered in _file_types (so upload_file.get_file resolves it by basename) and
  from_state (so restored sessions keep it); pruned from UNSUPPORTED_BINARY_EXTENSIONS.
- Text write_file, PDF/DOCX rendering, and other binary rejections unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-04 12:51:18 -07:00
Aabid Mohamed 04112b8691 Merge branch 'main' into docs/pzero-supported-models 2026-09-04 21:57:54 +05:30
Vedanth Bora 5969f0912b fix: support query strings in Bedrock image URLs 2026-09-04 18:04:53 +05:30
nina-mir 2d86d6f584 test(llm): cover DeepSeek and Cerebras API key resolution 2026-09-04 00:56:53 -07:00
nina-mir 61a540307f fix(llm): resolve DeepSeek and Cerebras API keys explicitly
ChatDeepSeek and ChatCerebras pass api_key=None to AsyncOpenAI while
pointing at non-OpenAI base_urls, so the SDK falls back to OPENAI_API_KEY
and authenticates api.deepseek.com and api.cerebras.ai with it.

Resolve each provider's documented env var explicitly and raise when
absent, matching the pattern applied to ChatOpenRouter and ChatVercel
in #5599. models.md already documented DEEPSEEK_API_KEY and
CEREBRAS_API_KEY; neither adapter read them.
2026-09-04 00:56:53 -07:00
nina-mir b60d718a4b fix(llm): remap retired Mistral pixtral-large alias 2026-09-03 23:00:51 -07:00
UniversePeak 27b9b2347d test(cli): compare module and cli entrypoints 2026-09-04 13:55:58 +08:00
UniversePeak f363b81b66 fix(cli): support module invocation on Windows 2026-09-04 13:55:58 +08:00
Magnus Müller fe5ad35309 chore: pin publish workflow toolchain (#5669)
## 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. -->
2026-09-03 21:06:47 -07:00
MagMueller 68f9a0992d chore: pin publish workflow actions by SHA 2026-09-03 20:32:51 -07:00
MagMueller 8db8283af3 chore: pin uv in publish workflow 2026-09-03 20:32:51 -07:00
Magnus Müller 5c892e013a chore: pin release dependencies for 0.13.10 (#5670)
## 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. -->
0.13.10
2026-09-03 20:28:40 -07:00
MagMueller 743f630923 test: satisfy MCP handler context typing 2026-09-03 20:24:29 -07:00
MagMueller afbc714a60 fix: report unknown MCP tools as errors 2026-09-03 20:23:43 -07:00
MagMueller b1ae95ec81 chore: pin release dependencies for 0.13.10 2026-09-03 20:17:35 -07:00
Magnus Müller 64c1f46e33 chore: release 0.13.9 with Browser Harness 0.1.12 (#5666)
## 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. -->
0.13.9
2026-09-03 18:29:16 -07:00
MagMueller 0e0983e30e Release 0.13.9 with Browser Harness 0.1.12 2026-09-03 18:26:02 -07:00
Mohil-Ahuja 7dcb968fcb fix(llm): only merge system text into the first serialized user message
Dropping the `not formatted_messages` guard fixed the assistant-first case but
went too far: for a `user -> system -> user` ordering the system text was
merged into the second user message, where the documented behaviour is to
prepend to the *first* user message and otherwise fall back to the separate
system instruction.

Track whether a user message has already been serialized and merge only while
none has. An earlier assistant turn still does not disqualify the first user
message, but a user turn does, and the leftover text now reaches the caller as
the system instruction instead of being silently relocated.
2026-09-04 01:05:05 +05:30
Mohil-Ahuja 9d410fec3d test: assert on serialized contents through a narrowing helper 2026-09-04 00:41:12 +05:30
Mohil-Ahuja 01f32db445 fix(llm): stop dropping system messages in the Gemini request
GoogleMessageSerializer.serialize_messages() lost system instructions in two
different ways:

- With include_system_in_user=False, each system message assigned to the same
  `system_message` variable, so only the last one reached the request.
- With include_system_in_user=True, the collected system text was prepended
  only when the first user message was also the first formatted message. If an
  assistant turn came first, the text was neither prepended nor returned as
  the system instruction, and disappeared entirely.

Collect every system message into `system_parts` regardless of the flag, drop
the `not formatted_messages` guard so the text lands on the first *user*
message whatever precedes it, and join whatever is left over into the returned
system instruction. That last part also covers a conversation with no user
message at all, which previously discarded the system text.

A single system message still round-trips to the identical instruction string.

Fixes #5628
Fixes #5624
2026-09-04 00:05:03 +05:30
Magnus Müller 5b50d1f511 fix(browser): restore session handlers after event bus reset (#5230)
## 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. -->
2026-09-03 11:10:13 -07:00
Magnus Müller 780274dc69 Merge branch 'main' into fix/session-handler-registration 2026-09-03 11:08:54 -07:00
Magnus Müller 89637449ab fix: detect browser process exit early in _wait_for_cdp_url (#4599)
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. -->
2026-09-03 11:07:43 -07:00
Magnus Müller f4e2bb1f74 Merge branch 'main' into fix/issue-4471-fail-fast-browser-startup 2026-09-03 11:06:16 -07:00
Magnus Müller 1346f8caf4 Merge branch 'main' into fix/session-handler-registration 2026-09-03 11:06:15 -07:00
Magnus Müller 102cdb4f99 fix(dom): expose image context for directly clickable images (#5586)
## 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. -->
2026-09-03 11:04:41 -07:00
Magnus Müller 57f60e4cf6 Merge branch 'main' into fix/issue-4471-fail-fast-browser-startup 2026-09-03 11:03:22 -07:00