Commit Graph

9859 Commits

Author SHA1 Message Date
Saurav Panda ae52ce8dcd fix: satisfy pyright in profile copy test 2026-05-09 11:40:51 -07:00
duyua9 e89f3aaae2 fix: handle locked Chrome profile copies 2026-05-09 23:17:17 +08:00
Saurav Panda e76334643b fix(BrowserError): drop trailing 'during: None' when no event is attached (#4787)
## Problem

`BrowserError.__str__` unconditionally appends `during:
<while_handling_event>`, so any error with `details` set but no `event`
attached renders as:

```
Text not found: "IMI" ({'text': 'IMI'}) during: None
```

The `during: None` tail is logging noise. The biggest offender is
`default_action_watchdog.on_ScrollToTextEvent` (`raise
BrowserError(f'Text not found: ...', details={'text': event.text})` — no
event passed), but every other call site that passes `details=` without
`event=` hits the same path.

**Production volume (last 24h, Datadog us5):**
- 1,600 occurrences matching `status:error env:production "'}) during:
None"` ([log
query](https://us5.datadoghq.com/logs?from_ts=1777910480700&live=false&query=status%3Aerror+env%3Aproduction+%22%27%7D%29+during%3A+None%22&stream_sort=desc&to_ts=1777996880700))
- Sample line:
  ```
🚌 [DefaultActionWatchdog.on_ScrollToTextEvent(#64b7)] ❌ Failed (0.08s):
BrowserError: Text not found: "IMI" ({'text': 'IMI'}) during: None
  ```

## Fix

Build the string from whichever parts are actually populated, instead of
two mutually-exclusive branches that always print one of them. Behavior
matrix:

| details | while_handling_event | Before | After |
| --- | --- | --- | --- |
| set | None | \`msg ({...}) during: None\` | \`msg ({...})\` |
| set | set | \`msg ({...}) during: <event>\` | \`msg ({...}) during:
<event>\` |
| None | set | \`msg (while handling: <event>)\` | \`msg during:
<event>\` |
| None | None | \`msg\` | \`msg\` |

The only behavior change for clean cases is a small wording tweak in the
\`details=None, event=set\` row (\`while handling:\` → \`during:\`) —
keeping the prefix consistent across all permutations.

## Test plan

- [ ] No new test added (string-only change); existing tests should
still pass
- [ ] Manual: run an agent task that triggers \`ScrollToText\` against a
page that doesn't contain the text; verify no \`during: None\` in logs
- [ ] After merge + deploy, the same Datadog query should drop to ~0

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Removes the noisy "during: None" suffix from `BrowserError` messages
when no event is attached, making logs cleaner and easier to scan.

- **Bug Fixes**
- Build the error message from only the populated parts; omit "during"
when no event is present.
- Standardize the label to "during:" when an event exists (was "while
handling:"); expected to eliminate ~1.6k noisy log lines/day.

<sup>Written for commit c16bd93d12.
Summary will update on new commits.</sup>

<!-- End of auto-generated description by cubic. -->
2026-05-08 16:19:36 -07:00
Saurav Panda c16bd93d12 Merge branch 'main' into fix/browser-error-str-during-none-20260505 2026-05-08 16:14:34 -07:00
Saurav Panda f0b4d7c51f fix: survive AX tree iframe detach in DOM extraction (#4778) (#4797)
## Summary

Fix #4778: `_get_ax_tree_for_all_frames` called `asyncio.gather` without
`return_exceptions=True`, so a single iframe detaching between
`Page.getFrameTree` and `Accessibility.getFullAXTree` (TOCTOU race
common with ad/widget iframes) raised and discarded AX data from every
other frame including the main document. The agent then sees an empty
DOM and burns retries. Reporter confirmed on 7/10 real-world sites with
full repro.

Now passes `return_exceptions=True` and skips dead frames in the merge;
surviving frames return AX nodes as before.

## Notes on scope

This PR originally also included a fix for #4484 (`step_interval`
field). On review, Codex pointed out that the existing `previous_end −
previous_start` formula — while a literal misnomer — is load-bearing:
`rerun_history` uses the saved value as a heuristic delay before
replaying each step, giving slow pages time to settle. Switching to a
literal "gap between steps" (`current_start − previous_end` ≈ 0, because
`step_start_time` is set at the top of `step()` before DOM capture and
LLM) would silently regress replay on slow pages. #4484 needs a more
deliberate redesign and is dropped from this PR.

## Test plan

- [x] `uv run pytest tests/ci` — 789 passed, 29 skipped (full local
suite)
- [x] `uv run pyright browser_use/dom/service.py` — clean
- [x] `tests/ci/browser/test_dom_serializer.py` (cross-origin iframe +
shadow DOM coverage) — 3/3 pass

Closes #4778
2026-05-06 13:44:15 -07:00
Saurav Panda 20e11bcb89 fix: propagate root-frame AX failures, only skip child detaches
Codex flagged that the previous catch-all skipped root-frame failures
too, masking a stale main-frame id as a successful (but empty) AX tree
and breaking interactiveness/form-value serialization that depend on
node.ax_node. Re-raise on the root frame so the retry/empty-DOM path
runs; continue to skip detached child iframes.
2026-05-06 13:35:39 -07:00
Saurav Panda 3a2b93cd05 scope down: drop #4484 step_interval fix, keep AX tree fix only
Codex review noted the original `previous_end - previous_start` formula,
while semantically misnamed, is load-bearing for replay correctness:
rerun_history uses it as a heuristic delay before replaying each step,
giving slow pages time to settle. Switching to the literal "gap" between
steps (~0 because step_start_time is set at the top of step()) silently
regresses replay on slow pages. #4484 needs a more careful redesign.
2026-05-06 12:15:58 -07:00
Saurav Panda 6c57fe75eb fix: correct step_interval gap calc and survive AX tree iframe detach
- StepMetadata.step_interval was computing the previous step's duration
  instead of the gap between steps. Use current_start - previous_end so
  rerun_history's "saved interval includes LLM time" comment actually
  holds (closes #4484). Existing test encoded the bug as expected; updated
  to assert gap semantics.

- _get_ax_tree_for_all_frames called asyncio.gather without
  return_exceptions=True, so a single iframe detaching between
  Page.getFrameTree and Accessibility.getFullAXTree (TOCTOU race common
  with ad/widget iframes) discarded AX data from every other frame
  including the main document, leaving the agent blind. Now skips dead
  frames and merges surviving trees (closes #4778).
2026-05-06 12:08:42 -07:00
Larsen Cundric 49fb16b693 fix(BrowserError): drop trailing 'during: None' when no event is attached
BrowserError.__str__ unconditionally appended 'during: <while_handling_event>',
so any error with details set but no event (e.g. ScrollToText 'Text not found',
click failures raised from action handlers) renders as

    Text not found: "X" ({'text': 'X'}) during: None

The 'during: None' tail is logging noise that surfaces ~1.6k times/day in
prod (Datadog: status:error env:production '"\'\}) during: None"', 24h).

Build the string from the parts that are actually populated, instead of
two mutually-exclusive branches that always print one of them.
2026-05-05 16:04:59 +00:00
Saurav Panda 8fc4f34a66 fix: preserve partial action results when mid-batch action fails (#4770)
## Problem
When `multi_act()` executes a batch of N actions and action K (where 0 <
K < N) raises an exception, the partial results from successfully
executed actions 1..K-1 are silently lost. The exception propagates
through `_execute_actions()` → `_handle_step_error()`, which creates a
single generic `ActionResult(error=error_msg)` that overwrites any trace
of the earlier successes.

The LLM receives only the generic error and loses visibility into which
specific actions completed before the failure — even though those
actions had real side effects (clicks, form fills, navigation).

## Fix
Instead of re-raising the exception, append an error `ActionResult` to
the accumulated `results` list and return it. This path is already
supported downstream:

- `_execute_actions()` (line 1205): `self.state.last_result` is set from
the return value
- `_post_process()` (lines 1221-1222): explicitly handles multi-action
errors via loop detection and replan nudges
- `_finalize()`: saves all results to history for the LLM to see

**Change**: `raise e` → `results.append(ActionResult(error=str(e)));
return results`

## Impact
- The agent now sees exactly which actions succeeded and which failed
- No change to the success path
- Error handling remains correct: single-action errors still increment
`consecutive_failures`, multi-action errors are handled by loop
detection

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Preserves partial results in `multi_act()` when a mid-batch action fails
by returning accumulated successes plus a trailing error `ActionResult`.
Keeps stop/pause and reconnect/shutdown behavior by re-raising
`InterruptedError` and connection-like errors.

- **Bug Fixes**
- For non-control-flow and non-connection exceptions, log, append
`ActionResult(error="<Exception>: <msg>")`, and return accumulated
results.
- Re-raise `InterruptedError`; `asyncio.CancelledError` doesn’t reach
this handler; re-raise connection-like errors so `_handle_step_error`
can run reconnect/shutdown.
- Fixed `except` block indentation to match the inner `try` (prevented a
SyntaxError).

<sup>Written for commit 54322e6a8b.
Summary will update on new commits.</sup>

<!-- End of auto-generated description by cubic. -->
2026-05-02 19:02:19 -07:00
Saurav Panda 54322e6a8b fix: correct except indentation and preserve connection-error handling
- Re-indent the except block to match the inner try (was a SyntaxError)
- Drop asyncio.CancelledError from the isinstance check (it's BaseException,
  never reaches except Exception)
- Re-raise on _is_connection_like_error so _handle_step_error can run its
  reconnect / browser-closed shutdown logic
- Include exception class name in the preserved ActionResult error
2026-05-02 17:59:04 -07:00
Saurav Panda 45f53f47e4 Merge branch 'main' into fix/preserve-partial-results-on-action-error 2026-05-02 17:55:03 -07:00
Saurav Panda 8c7d333ec3 fix: remove dead code 'type' from second elif branch in optimize_schema() (#4704)
Closes #4703

## Problem

In `browser_use/llm/schema.py`, the `optimize_schema()` function has a
dead code issue. The `'type'` key is first handled by an early `elif key
== 'type':` branch, but then it also appears inside a later `elif key in
['type', 'required', ...]` block. Because Python `elif` chains are
exclusive, the `'type'` entry in the second list is **never reached**.

```python
# This catches 'type' FIRST:
elif key == 'type':
    optimized[key] = value if not isinstance(value, (dict, list)) else optimize_schema(value, defs_lookup)

# ...

# 'type' here is dead code — never reached:
elif key in [
    'type',     # <-- unreachable dead code
    'required',
    ...
]:
```

## Fix

Remove `'type'` from the second `elif key in [...]` list to eliminate
dead code and clarify intent.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Removed dead code in optimize_schema() by removing `type` from the later
`elif` list, since `type` is already handled earlier. No functional
change; clarifies intent and removes unreachable logic.

<sup>Written for commit 6ccae6980e.
Summary will update on new commits.</sup>

<!-- End of auto-generated description by cubic. -->
2026-05-01 15:49:24 -07:00
Saurav Panda 6ccae6980e Merge branch 'main' into fix/optimize-schema-dead-code 2026-05-01 15:46:10 -07:00
Saurav Panda 8653e59419 Find more recent versions of Playwright-installed Chromium for Mac/ARM users (#4680)
Users who have Mac devices with ARM chips may have more recent versions
of Chromium installed in
`~/Library/Caches/ms-playwright/chromium-*/chrome-mac-arm64/`, but the
current code only searches in
`~/Library/Caches/ms-playwright/chromium-*/chrome-mac/`. As a result,
Browser Use will use a significantly out-of-date browser when a newer
version is available.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Improve Chromium detection on macOS by searching both `chrome-mac` and
`chrome-mac-arm64` in the Playwright cache. Mac/ARM users now run the
latest Playwright-installed Chromium instead of an outdated build.

- **Bug Fixes**
- Expanded search to
`~/Library/Caches/ms-playwright/chromium-*/chrome-mac*/Chromium.app/Contents/MacOS/Chromium`
to include `chrome-mac-arm64`.

<sup>Written for commit 98a86bead7.
Summary will update on new commits.</sup>

<!-- End of auto-generated description by cubic. -->
2026-05-01 15:45:57 -07:00
Saurav Panda 98a86bead7 Merge branch 'main' into main 2026-05-01 15:43:05 -07:00
Saurav Panda 886bf9d28e Merge branch 'main' into fix/optimize-schema-dead-code 2026-05-01 15:40:09 -07:00
Tony fd7eeeb15f fix: re-raise control-flow exceptions (InterruptedError, CancelledError)
PHclaw pointed out that _check_stop_or_pause() raises InterruptedError
inside the multi_act loop. Converting it to an ActionResult would prevent
the agent from properly stopping/pausing between batched actions.

Re-raise InterruptedError and asyncio.CancelledError before wrapping
other exceptions as ActionResult with partial results.
2026-04-30 16:00:44 +08:00
Tony bca433ccb2 fix: preserve partial action results when mid-batch action fails
When multi_act() executes a batch of actions and one fails mid-way,
the partial results from successfully executed earlier actions were
discarded by re-raising the exception. The agent lost visibility into
which actions completed before the failure.

Return the accumulated results with an error ActionResult appended,
which aligns with the existing post_process logic that explicitly
handles multi-action errors via loop detection and replan nudges
(lines 1221-1222).
2026-04-30 16:00:44 +08:00
Alexander Yue d19ec6ef20 docs: update issue template setup and docs links (#4746)
## Summary

Updates two GitHub issue template fields for consistency with the
current project docs and setup guidance.

## Changes

- Update the feature request template to use `uv pip show browser-use`
instead of `pip show browser-use`
- Refresh the docs issue template placeholder URL to the current Browser
all-parameters page:

`https://docs.browser-use.com/open-source/customize/browser/all-parameters`

## Validation

- Ran `git diff --check`
- Parsed both edited YAML files successfully


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Update GitHub issue templates for accuracy. Feature requests now use `uv
pip show browser-use` for version checks, and the docs issue placeholder
links to the current all-parameters page.

<sup>Written for commit 88d61e8b62.
Summary will update on new commits.</sup>

<!-- End of auto-generated description by cubic. -->
2026-04-26 09:59:07 -07:00
Ankit Kotnala 88d61e8b62 docs: update issue template setup and docs links 2026-04-26 21:49:57 +05:30
Saurav Panda f3878b0e07 fix(element): redact sensitive values in fill() debug logging (#4736)
## Summary
- Redact sensitive form field values (passwords, etc.) from DEBUG
logging in `fill()` method

## Why
The `fill()` method in `browser_use/actor/element.py` was logging the
full value being typed at DEBUG level:
```
logger.debug(f'Typing text character by character: "{value}"')
```
This could expose passwords and other sensitive data in debug logs when
users have DEBUG logging enabled.

## Fix
Changed to:
```
logger.debug(f'Typing text character by character: "[REDACTED {len(value)} chars]"')
```
This preserves debug utility (knowing that typing is happening and
roughly how much) while avoiding exposure of sensitive data.

## Validation
- [x] pyright type check passes (0 errors)
- [x] ruff lint passes (0 errors)

## Related
Issue #4721 (security advisory - supply chain risk in init command also
noted in same issue)

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Redacts sensitive values from `fill()` debug logs to prevent leaking
passwords or secrets, while keeping useful context. Also cleans up
schema optimization by removing an unreachable `'type'` key check.

- **Bug Fixes**
- `browser_use/actor/element.py`: Log `"[REDACTED {len(value)} chars]"`
instead of the actual text in `fill()` DEBUG logs.
- `browser_use/llm/schema.py`: Remove dead `'type'` entry from
validation fields; it’s already handled earlier.

<sup>Written for commit 99e9a455e7.
Summary will update on new commits.</sup>

<!-- End of auto-generated description by cubic. -->
2026-04-24 18:07:59 -07:00
Will-hxw 99e9a455e7 fix(element): redact sensitive values in fill() debug logging
Prevent passwords and other sensitive form data from appearing in debug
logs by showing "[REDACTED N chars]" instead of the actual value.

Fixes issue #4721 (security advisory)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 23:19:31 +08:00
Will-hxw c4f712b90b fix(schema): remove unreachable 'type' from validation fields list
The 'type' key is already handled by an earlier elif branch
(elif key == 'type'), making its presence in the later elif key in [...]
block dead code. Remove it to eliminate confusion.

Fixes #4703
2026-04-23 21:59:48 +08:00
Cocoon-Break e253f55024 Merge branch 'main' into fix/optimize-schema-dead-code 2026-04-22 10:42:16 +08:00
Saurav Panda 1da2d3ff28 docs: fix malformed HTML attributes in README picture/img tags (#4712)
## Summary

Three HTML attribute-quoting typos in `README.md`:

- Line 2 — trailing `""` on the light-mode `<source srcset=...>` in the
top logo `<picture>`.
- Line 9 — same trailing `""` on the light-mode `<source srcset=...>`
for the hero image.
- Line 33 — missing closing `"` on `<img width="4 height="1" alt="">`
between the Twitter and Discord badges (was: `width="4 height="1"`, now:
`width="4" height="1"`).

Diff is 3 insertions / 3 deletions, one file, no code paths touched.

### Why

GitHub's markdown renderer tolerates these today, but:

- Strict HTML parsers / README mirrors (pypi page, docs sites, some
RSS/embed previews) can choke on the stray quotes or run attributes
together.
- `width="4 height="1"` is parsed as `width="4 height="` with a stray
`1"` token — the `height` attribute is effectively silently dropped.
- Costs nothing to have the raw source be valid HTML.

Pure docs fix, no behavior change, no new deps.

### Test plan

- [x] `git diff --stat` → 1 file, 3/3.
- [x] Visual inspection of rendered README on fork still looks
identical.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fix three malformed HTML attributes in `README.md` to make the HTML
valid across strict renderers and mirrors. Removed trailing quotes from
two light‑mode `<source srcset>` tags and fixed a missing quote in an
`<img>` tag’s `width`/`height` attributes; no behavior change.

<sup>Written for commit eb12fb9df6.
Summary will update on new commits.</sup>

<!-- End of auto-generated description by cubic. -->

---
_Part of open-source blockchain work from
[kcolbchain.com](https://kcolbchain.com) — maintained by [Abhishek
Krishna](https://abhishekkrishna.com). PR opened via [kcolbchain
contrib-bot](https://github.com/kcolbchain/kcolbchain.github.io/blob/master/deploy/contrib-bot/README.md)._
2026-04-20 19:08:44 -07:00
Saurav Panda eb12fb9df6 Merge branch 'main' into contrib-bot/2026-04-21-browser-use 2026-04-20 19:02:39 -07:00
Saurav Panda 981e747707 Bump aiohttp to 3.13.4 (CVE-2026-34515) (#4715)
## Summary
- Bumps pinned `aiohttp` from `3.13.3` to `3.13.4` to patch
[CVE-2026-34515](https://github.com/aio-libs/aiohttp/security/advisories/GHSA-p998-jp59-783m)
(NTLMv2 credential leak via aiohttp's Windows static resource handler).
- **Not exploitable in this codebase.** aiohttp is used only as a client
(CDP polling in
`browser_use/browser/watchdogs/local_browser_watchdog.py:410`, plus two
examples). There is no `aiohttp.web` server, no `add_static`, no
Windows-only server code — none of the vulnerable surface is reached.
- Bumping purely to clear Dependabot alert #29. Safe patch-level
upgrade.

## Test plan
- [x] `uv sync --frozen` resolves cleanly
- [x] `uv run python -c "import aiohttp; import aiohttp.web"` on 3.13.4
- [ ] CI green

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Bumps `aiohttp` from 3.13.3 to 3.13.4 to patch CVE-2026-34515 in the
Windows static resource handler. We only use `aiohttp` as a client, so
the vuln isn’t reachable; this is a safe patch to clear the security
alert.

<sup>Written for commit 8e9c3488de.
Summary will update on new commits.</sup>

<!-- End of auto-generated description by cubic. -->
2026-04-20 19:00:26 -07:00
Saurav Panda 8e9c3488de Bump aiohttp 3.13.3 -> 3.13.4 to patch CVE-2026-34515
GHSA-p998-jp59-783m: aiohttp's static resource handler on Windows
can leak NTLMv2 credentials via UNC path traversal. Fixed in 3.13.4.

browser-use only uses aiohttp as a client (local CDP polling in
watchdogs/local_browser_watchdog.py, plus examples) — no web.Application
or add_static — so the vuln is not reachable here. Bump is prophylactic
to clear the Dependabot alert.
2026-04-20 18:52:26 -07:00
Saurav Panda 66e577d8b6 Bump pypdf to 6.10.2 (CVE-2026-40260) (#4714)
## Summary
- Bumps pinned `pypdf` from `6.9.1` to `6.10.2` to patch
[CVE-2026-40260](https://github.com/py-pdf/pypdf/security/advisories)
(XMP metadata XML entity expansion / billion-laughs RAM exhaustion).
- pypdf < 6.10.0 did not restrict recursive entity declarations in XMP
metadata DTDs, so a small crafted PDF could allocate gigabytes of memory
when opened.
- Call site: `browser_use/filesystem/file_system.py:549` uses
`pypdf.PdfReader` on PDFs the agent has downloaded — i.e. reachable from
attacker-controlled content, which makes this more than cosmetic.

## Test plan
- [x] `uv sync --frozen` resolves cleanly
- [x] `uv run python -c "import pypdf; pypdf.PdfReader"` on 6.10.2
- [x] `tests/ci/test_file_system_*.py` (24 passed)
- [ ] CI green

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Update `pypdf` to 6.10.2 to fix CVE-2026-40260 (XMP XML entity expansion
“billion laughs” DoS). This secures our `PdfReader` usage in
`browser_use/filesystem/file_system.py` when opening agent-downloaded
PDFs.

- **Dependencies**
  - Bump `pypdf` from `6.9.1` to `6.10.2`.

<sup>Written for commit 74ccf0ebd6.
Summary will update on new commits.</sup>

<!-- End of auto-generated description by cubic. -->
2026-04-20 18:50:17 -07:00
Saurav Panda eca52101a9 Merge branch 'main' into fix/pypdf-cve-2026-40260 2026-04-20 18:45:56 -07:00
Saurav Panda 74ccf0ebd6 Bump pypdf 6.9.1 -> 6.10.2 to patch CVE-2026-40260
pypdf < 6.10.0 did not restrict recursive XML entity expansion when
parsing XMP metadata, allowing a crafted PDF to trigger a "billion
laughs"-style RAM exhaustion via PdfReader. Fixed upstream in 6.10.0.
Bumps to latest patch (6.10.2).

Relevant call site: browser_use/filesystem/file_system.py uses
pypdf.PdfReader on agent-downloaded PDFs, which is reachable from
attacker-controlled content.
2026-04-20 18:45:21 -07:00
Saurav Panda 895a0543ce Bump pillow to 12.2.0 (CVE-2026-40192) (#4713)
## Summary
- Bumps pinned `pillow` from `12.1.1` to `12.2.0` to patch
[CVE-2026-40192](https://github.com/python-pillow/Pillow/security/advisories/GHSA-whj4-6x5x-4v2j)
(FITS GZIP decompression bomb).
- Pillow 10.3.0-12.1.1 did not bound GZIP-compressed reads when decoding
FITS images, enabling a memory-exhaustion DoS via a crafted FITS file.
Fixed upstream in 12.2.0.
- Practical exposure in this repo is minimal — all `Image.open` call
sites operate on PNG screenshot bytes from CDP or bundled static assets,
no FITS input path — but the pinned version was flagged by Dependabot
and the bump is a safe patch-level upgrade.

## Test plan
- [x] `uv sync --frozen` resolves cleanly
- [x] `uv run python -c "import PIL; from PIL import Image, ImageDraw,
ImageFont"` succeeds on 12.2.0
- [ ] CI (`tests/ci`) green

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Bumps `pillow` from 12.1.1 to 12.2.0 to patch CVE-2026-40192 (FITS GZIP
decompression bomb) and clear the security alert. No functional changes;
the app does not process FITS images.

<sup>Written for commit e7b0caac9f.
Summary will update on new commits.</sup>

<!-- End of auto-generated description by cubic. -->
2026-04-20 18:43:22 -07:00
Saurav Panda e7b0caac9f Bump pillow 12.1.1 -> 12.2.0 to patch CVE-2026-40192
GHSA-whj4-6x5x-4v2j: FITS GZIP decompression bomb in Pillow < 12.2.0.
Pillow 10.3.0-12.1.1 did not bound GZIP-compressed reads when decoding
FITS images, enabling a memory-exhaustion DoS via a crafted FITS file.
Fixed upstream in 12.2.0.
2026-04-20 18:39:05 -07:00
Saurav Panda d1690e510a fix(tools): enforce per-action timeout to prevent hung event handlers (#4711)
## Summary
- Wraps `registry.execute_action` in `asyncio.wait_for` with a
configurable per-action wall-clock cap (default 90s, override via
`BROWSER_USE_ACTION_TIMEOUT_S` env var or
`tools.act(action_timeout=...)`)
- On timeout, the action returns `ActionResult(error=...)` so the agent
records the step and recovers, instead of hanging silently

## The bug
Individual CDP calls like `Page.navigate()` have their own 20s timeouts
(`session.py:988`), but the surrounding event-bus plumbing — `await
event` and `await event.event_result(...)` — has none. When a cloud
browser's CDP WebSocket goes silent mid-session (common failure mode
against remote browser services: handshake completes, `/json/version`
returns 200, then the WebSocket stalls), every subsequent action
dispatched through `event_bus` hangs indefinitely.

Flow when this happens:
1. `agent._execute_initial_actions()` → `multi_act()` → navigate handler
dispatches `NavigateToUrlEvent`
2. `await event` / `event_result()` wait on a handler that can never
complete because CDP is silent
3. `tools.act()` doesn't raise, doesn't return — it hangs
4. The outer agent watchdog eventually calls `agent.stop()`
5. `_execute_initial_actions` exits via `InterruptedError` (silently
swallowed at `service.py:2557-2559`)
6. Main loop sees `state.stopped=True`, exits with 0 history entries →
**empty trace**

## Real-world evidence
A 170k-task collector run produced:
- **1,090 empty-history traces** (21% of all outputs)
- Of the 927 with duration timing, **100% hit the 240s outer watchdog**
— median 582s, max 2214s (~37 min)
- Cloud HTTP layer was clean throughout: all 200/201, 0 non-2xx
responses
- Concurrency was not the cause — fail rate actually increased at lower
parallelism, confirming the hang is per-session not back-pressure

Sample timeline from one of these failures (cloud session `78cc5c3e`):
```
14:48:47  Cloud browser created successfully
14:48:47  HTTP GET /json/version 200 OK
14:48:47  Connecting to wss://78cc5c3e...cdp4.browser-use.com/devtools/browser/...
(5 minutes of total silence)
14:53:53  Stopping cloud browser session (triggered by outer watchdog)
```

## The fix
Default cap is **90s**, which sits comfortably above `Page.navigate`'s
20s + lifecycle wait's 8s + internal CDP calls, but well below any
reasonable outer agent watchdog.

Configurable via:
- `BROWSER_USE_ACTION_TIMEOUT_S` env var (process-wide default)
- `tools.act(action_timeout=...)` parameter (per-call override)

## Test plan
- [x] New `tests/ci/test_action_timeout.py` — stubs
`registry.execute_action` with a coroutine that sleeps 30s, asserts
`tools.act()` returns within the 0.5s cap with an
`ActionResult(error=...)` containing `"timed out"`
- [x] New test also covers the fast-handler path — ensures no regression
when actions complete normally
- [x] Existing `test_multi_act_guards.py` and
`test_action_blank_page.py` tests still pass (all 15 green locally)
- [ ] Full CI suite

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Add per-action, per-step, and per-CDP-request timeouts to prevent hangs
and empty traces. Defaults are 180s for actions/steps and 60s for CDP;
both are configurable.

- **Bug Fixes**
- Tools: wrap `registry.execute_action` with `asyncio.wait_for`; default
180s via `BROWSER_USE_ACTION_TIMEOUT_S` or
`tools.act(action_timeout=...)`. Validate env and per-call override
values (empty/non-numeric/non-finite/<=0) with fallback; on timeout
return `ActionResult(error=...)`.
- Agent: wrap `_execute_initial_actions()` with `asyncio.wait_for` using
`settings.step_timeout`; on timeout log, set `state.last_result`,
increment `state.consecutive_failures`, and continue to the main loop.
- Browser: add `TimeoutWrappedCDPClient` around
`cdp_use.CDPClient.send_raw` with `asyncio.wait_for` (default 60s via
`BROWSER_USE_CDP_TIMEOUT_S` or constructor). Validate env/constructor
values (non-finite/<=0). Use in connect/reconnect; raise `TimeoutError`
on unresponsive CDP calls.
- Tests: add `tests/ci/test_action_timeout.py` (hung/fast paths and
invalid `action_timeout` override) and `tests/ci/test_cdp_timeout.py`
(real `send_raw` path). Update `tests/ci/browser/test_cdp_headers.py` to
patch `TimeoutWrappedCDPClient`.

<sup>Written for commit 32416bb48c.
Summary will update on new commits.</sup>

<!-- End of auto-generated description by cubic. -->
2026-04-20 17:59:15 -07:00
abhicris 7a49139d21 docs: fix malformed HTML attributes in README picture/img tags
Three attribute quoting typos in README.md:
- Line 2: trailing `""` on the light-mode `<source srcset=...>` in the
  top logo picture block.
- Line 9: same trailing `""` on the light-mode `<source srcset=...>`
  for the hero image.
- Line 33: missing closing `"` on `<img width="4 height="1" alt="">`
  between the Twitter and Discord badges.

GitHub tolerates these today, but they break stricter HTML parsers and
look off in raw source / mirrors. Pure docs fix, no behavior change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-21 06:27:40 +05:30
Saurav Panda 32416bb48c address codex review: validate tools.act(action_timeout=...) override
P2 codex comment on 9a09c4d7: the public `action_timeout` parameter on
tools.act() skipped the same defensive validation that the env-var path
already had. Passing nan made every action time out instantly; inf /
<=0 disabled the guard entirely. Either mode silently defeats the safety
this module exists to provide, especially for callers sourcing timeouts
from runtime config.

Extracted _coerce_valid_action_timeout() (pairs with _parse_env_action_
timeout) and routed the override through it. None / nan / inf /
non-positive all fall back to the env-derived default with a warning.

New test_act_rejects_invalid_action_timeout_override asserts the
fallback by passing bad values and verifying the fast handler actually
executes to completion (which wouldn't happen if nan → immediate
timeout or if inf → hang would leak through).
2026-04-20 17:55:59 -07:00
Saurav Panda 847ad78365 address cubic review: validate constructor timeout + exercise real send_raw
Two P2 comments from cubic on 9a09c4d7:

1. TimeoutWrappedCDPClient.__init__ trusted its cdp_request_timeout_s arg
   blindly. nan / inf / <=0 would either make every CDP call time out
   immediately (nan) or disable the guard (inf / <=0) — same defensive
   gap we already fixed for the env-var path. Extracted _coerce_valid_
   timeout() that mirrors _parse_env_cdp_timeout's validation; constructor
   now routes through it, so both entry points are equally safe.

2. test_send_raw_times_out_on_silent_server used an inline copy of the
   wrapper logic rather than the real TimeoutWrappedCDPClient.send_raw.
   A regression in the production method — e.g. accidentally removing
   the asyncio.wait_for — would not fail the test. Rewrote to:
   - Construct via __new__ (skip CDPClient.__init__'s WebSocket setup)
   - unittest.mock.patch the parent CDPClient.send_raw with a hanging
     coroutine
   - Call the real TimeoutWrappedCDPClient.send_raw, which does
     super().send_raw(...) → our patched stub
   - Assert it raises TimeoutError within the cap

Also added test_send_raw_passes_through_when_fast (fast-path regression
guard) and test_constructor_rejects_invalid_timeout (validation for
fix #1). All 14 tests in the timeout suite pass locally.
2026-04-20 17:54:37 -07:00
Saurav Panda 44e7b1f082 fix(tests): patch TimeoutWrappedCDPClient at its new construction site
Earlier commit 9a09c4d7 swapped the CDPClient construction in
browser_use/browser/session.py from the raw cdp_use.CDPClient to our
TimeoutWrappedCDPClient subclass. test_cdp_headers.py patches the
CDPClient symbol in session.py's namespace to assert headers/User-Agent
propagate — but since the code now instantiates TimeoutWrappedCDPClient,
the patch no longer intercepts the call and the mock.assert_called_once
check fails with 'Called 0 times'.

Point the patches at session.TimeoutWrappedCDPClient instead so the
assertions match what the code actually constructs. Header propagation
still works end-to-end because TimeoutWrappedCDPClient forwards
*args/**kwargs to super().__init__.
2026-04-20 17:45:35 -07:00
Saurav Panda 9a09c4d7dc fix(cdp): timeout-wrap CDPClient.send_raw to break silent WebSocket hangs
cdp_use.CDPClient.send_raw awaits a future that only resolves when the
browser sends a response with a matching message id. There is no timeout
on that await. Against the cloud browser service, the failure mode we
observed is: WebSocket stays alive at the TCP/keepalive layer (proxy
keeps pong-ing our pings), but the browser upstream is dead / unhealthy
and never sends any CDP response. send_raw's future never resolves, and
every higher-level timeout in browser-use (session.start's 15s connect
guard, agent.step_timeout, tools.act's action timeout) relies on
eventually getting a response — so they all wait forever too.

Evidence from a 170k-task collector run: 1,090 empty-history traces,
100% hit the 240s outer watchdog, median duration 582s, max 2214s, with
cloud HTTP layer clean throughout (all 200/201). One sample showed
/json/version returning 200 OK and then 5 minutes of total silence on
the WebSocket before forced stop — classic silent-hang.

Fix: add TimeoutWrappedCDPClient, a thin subclass of cdp_use.CDPClient
that wraps send_raw in asyncio.wait_for(timeout=cdp_request_timeout_s).
Any CDP method that doesn't respond within the cap raises plain
TimeoutError, which propagates through existing `except TimeoutError`
handlers in session.py / tools/service.py. Uses the same defensive env
parse pattern as BROWSER_USE_ACTION_TIMEOUT_S — rejects empty /
non-numeric / nan / inf / non-positive values with a warning fallback.

Default is 60s: generous for slow operations like Page.captureScreenshot
or Page.printToPDF on heavy pages, but well below the 180s step timeout
and any typical outer watchdog. Override via BROWSER_USE_CDP_TIMEOUT_S.

Wired into both CDPClient construction sites in session.py (initial
connect + reconnect path). All 17 existing real-browser tests
(test_action_blank_page, test_multi_act_guards) still pass.
2026-04-20 17:40:32 -07:00
Saurav Panda a9a7201d8d Merge branch 'main' into fix/action-timeout-hang 2026-04-20 17:02:08 -07:00
Saurav Panda a97ba48345 fix(agent): timeout _execute_initial_actions so empty-history hangs cannot happen
The main execution loop already wraps _execute_step with asyncio.wait_for
using settings.step_timeout (default 180s). But _execute_initial_actions,
which runs before the main loop, is unwrapped — if it hangs (e.g. the
first navigate stalls on a silent CDP WebSocket before the per-action
timeout can catch it), the agent blocks indefinitely without ever
entering the main loop. No step gets recorded, history stays empty, and
any outer watchdog eventually kills the run with zero diagnostic data.

Wrap _execute_initial_actions with the same step_timeout. On timeout,
record the failure in state.last_result / consecutive_failures and fall
through to the main execution loop so the agent can still attempt to
recover. InterruptedError (from an interrupting callback) is still
swallowed silently — same contract as before.

Paired with the per-action asyncio.wait_for added in tools/service.py,
this closes the last unprotected path in the pre-main-loop flow.
2026-04-20 16:39:22 -07:00
Saurav Panda 8ad6f896ae Add record start/stop CLI command for session video capture (#4710)
Closes #4533.

## Summary

Adds `browser-use record start <path>` / `record stop` / `record status`
to capture the current session as an MP4 via CDP screencasting — all the
underlying machinery (`Page.startScreencast`, `VideoRecorderService`)
already existed in the repo; this just exposes it on the CLI.

- `RecordingWatchdog` gains a public `start_recording(path, size?,
framerate?)` / `stop_recording() -> Path` / `is_recording` API. The
existing `BrowserConnectedEvent`/`BrowserStopEvent` handler is
refactored to use it, so profile-driven recording
(`record_video_dir=...`) is unchanged.
- New `record` subcommand wired through argparse (`skill_cli/main.py`),
the daemon dispatch allowlist, and `skill_cli/commands/browser.py`.
Works with `--session NAME` via the existing named-daemon
infrastructure. `record stop` prints the saved file path so it can be
captured programmatically (as requested in the issue).
- `CLIBrowserSession` intentionally skips watchdogs; the handler lazily
attaches `RecordingWatchdog` on first `record start` so non-recording
sessions pay no cost.
- Output is `.mp4` (libx264) — matches the existing encoder. Gated
behind the existing `browser-use[video]` optional extra; the CLI returns
a helpful error if deps are missing.

## Example

```bash
browser-use --session demo record start /tmp/demo.mp4
browser-use --session demo open https://example.com
browser-use --session demo click 3
browser-use --session demo record stop
# /tmp/demo.mp4
```

## Test plan

- [x] `uv run pytest -vxs tests/ci/test_action_record.py` — 6 new tests,
all pass (~23s). Covers: full start/stop cycle against a real headless
browser (produces a decodable MP4), double-start rejection,
stop-without-start returns None, profile-driven flow unchanged, argparse
parsing, dispatch registration.
- [x] `uv run pyright` on changed files — clean.
- [x] `uv run ruff check` / `ruff format` — clean.
- [x] Live end-to-end CLI smoke test: `record start` → `open` → `record
stop` produced a valid ~11 KB MP4.
- [ ] CI green.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds `record start/stop/status` to the `browser-use` CLI to capture the
current session as an `.mp4` via CDP screencasting, with simple
start/stop APIs on `RecordingWatchdog` and reliable shutdown that
finalizes recordings.

- **New Features**
- `browser-use record start <path>`, `stop`, and `status`; `start`
supports `--framerate`, `stop` prints the saved path, and `status`
returns path, framerate, and size.
- Works with `--session NAME`; lazily attaches `RecordingWatchdog` so
non-recording sessions have no overhead.
- Outputs `.mp4` (libx264) via the existing encoder; gated behind
`browser-use[video]` with a clear error if missing.

- **Bug Fixes**
- `on_BrowserConnectedEvent` degrades gracefully when recording cannot
start (e.g., missing `browser-use[video]` or undetectable viewport) so
sessions still launch with `record_video_dir` set.
- Daemon shutdown now awaits `stop_recording()` (no timeout) and
finalizes any in-progress recording, preventing truncated MP4s.

<sup>Written for commit 44f7ead5cd.
Summary will update on new commits.</sup>

<!-- End of auto-generated description by cubic. -->
2026-04-20 15:49:35 -07:00
Saurav Panda d2985dcab9 review: reject non-finite timeouts + restore module after reload tests
Two more issues from automated review on #4711:

1. (P2, Codex) float() accepts 'nan' and 'inf' — both parse successfully
   and bypass the fallback path. 'nan' makes asyncio.wait_for time out
   immediately for every action; 'inf' effectively disables the hang
   guard. Extracted the parse into _parse_env_action_timeout() which
   rejects non-finite and non-positive values (including 0 and negatives)
   with a warning + fallback.

2. (P2, Cubic) The previous reload test left browser_use.tools.service
   pinned at _DEFAULT_ACTION_TIMEOUT_S=45.0 (the last monkeypatch value),
   which would leak into any later test in the same worker. Added a
   _restore_service_module fixture that pops the env var and reloads
   cleanly on teardown.

Expanded test coverage to include 'nan', 'NaN', 'inf', '-inf', '0', '-5'
alongside the existing '' / 'abc' cases — all fall back to 180s.
2026-04-20 15:45:36 -07:00
Saurav Panda 44f7ead5cd Drop timeout on recording finalize in daemon shutdown
`asyncio.wait_for(stop_recording(), timeout=5.0)` could expire while the
ffmpeg encoder was still flushing, leading the daemon's subsequent
`os._exit(0)` to kill the executor thread mid-write and leave the exact
truncated MP4 this hook was meant to prevent. `stop_recording()` already
offloads the blocking close to an executor, so awaiting it directly is
safe — and if it genuinely hangs, a stuck daemon is a clearer failure
signal than silent video corruption.

Verified end-to-end: start recording → `open` → `close` (no explicit
`record stop`) now produces a decodable MP4 with the captured frames.
2026-04-20 15:38:34 -07:00
Saurav Panda 1488a39b7f address PR review: raise default cap + tolerate bad env values
Two issues flagged by automated review on #4711:

1. (P1, Codex) The 90s default was *below* the extract action's intentional
   120s page_extraction_llm.ainvoke timeout (tools/service.py:1096,1172).
   Slow-but-valid extractions against large pages would be truncated into
   timeout errors — a regression. Raised default to 180s, which sits above
   that 120s inner cap with grace.

2. (P2, Cubic + Codex) float(os.getenv('BROWSER_USE_ACTION_TIMEOUT_S', '90'))
   ran at import time. An empty or non-numeric value (common with env
   templating) raised ValueError and prevented browser_use.tools.service
   from importing at all — turning a config typo into a process-wide
   startup failure. Wrapped in try/except with a warning and fallback to
   the hardcoded 180s default.

Tests:
- test_default_action_timeout_accommodates_extract_action — pins the
  default >= 150s so future edits can't silently regress extract.
- test_malformed_env_timeout_does_not_break_import — reloads the module
  with empty / non-numeric env values and asserts it falls back cleanly,
  plus verifies a valid numeric env value still takes effect.
2026-04-20 15:36:15 -07:00
Saurav Panda ce81ada89a fix(tools): enforce per-action timeout to prevent hung event handlers
Individual CDP calls like Page.navigate() have their own 20s timeouts, but
the surrounding event-bus plumbing (await event, event_result()) does not.
When a cloud browser's CDP WebSocket goes silent mid-session, agent handlers
hang indefinitely — agents never emit a step, any outer watchdog eventually
fires, and the run returns with zero history.

Observed in practice: a 170k-task collector run produced 1,090 empty-history
traces (21% of output). 100% hit the 240s outer watchdog; median 582s, max
2214s. Cloud HTTP layer was clean (all 200/201) — hang was entirely in CDP.

Wrap registry.execute_action in asyncio.wait_for with a configurable per-
action cap (default 90s, BROWSER_USE_ACTION_TIMEOUT_S env var or
tools.act(action_timeout=...)). On timeout, the action returns
ActionResult(error=...) so the agent can record the step and recover.

New tests/ci/test_action_timeout.py covers both hung and fast handlers.
Existing tools.act tests (test_multi_act_guards, test_action_blank_page)
still pass.
2026-04-20 15:22:30 -07:00
Saurav Panda 132756dabb Address PR review feedback for record start/stop
- `on_BrowserConnectedEvent` now catches `RuntimeError` from
  `start_recording()` so sessions with `record_video_dir` configured but
  missing `[video]` extras (or a viewport that can't be sized) keep
  starting — prior graceful-degradation behavior is restored.
- Lazy `RecordingWatchdog` in the CLI handler now calls
  `attach_to_session()`, so `AgentFocusChangedEvent` / `BrowserStopEvent`
  handlers are wired correctly if the session dispatches them.
- Daemon shutdown finalizes any in-progress recording before tearing the
  browser down, preventing truncated MP4s on `close`, idle timeout, or
  signal-driven exit.
- Added regression test that monkeypatches `start_recording` to raise and
  asserts `on_BrowserConnectedEvent` swallows it without breaking startup.
2026-04-20 15:11:49 -07:00
Saurav Panda b1d933258c Add record start/stop CLI command for session video capture
Closes #4533.

- `RecordingWatchdog` gains public `start_recording(path, size?, framerate?)`,
  `stop_recording() -> Path`, and `is_recording`; the existing
  `BrowserConnectedEvent`/`BrowserStopEvent` path is refactored to use them,
  so profile-driven recording behavior is unchanged.
- `browser-use record start <path>` / `record stop` / `record status`
  subcommands wired through argparse, daemon dispatch, and the browser
  command handler. `record stop` prints the saved file path so it can be
  captured programmatically, matching the issue's requested UX. Works with
  `--session NAME` via the existing named-daemon infrastructure.
- The CLI's `CLIBrowserSession` intentionally skips watchdogs; the handler
  lazily instantiates `RecordingWatchdog` on first `record start` so CLI
  recording doesn't pay the watchdog-setup cost for non-recording sessions.
- Output format is `.mp4` (libx264) since that's what the existing
  `VideoRecorderService` encodes; optional dependency gate is unchanged
  (`pip install "browser-use[video]"`).
- New `tests/ci/test_action_record.py` exercises the full stack against a
  real headless browser + `pytest-httpserver`, verifying decodable MP4
  output, double-start rejection, stop-without-start no-op, that the
  existing `profile.record_video_dir` flow still works, and the argparse /
  dispatch wiring.
2026-04-20 14:50:52 -07:00
Cocoon-Break 8442adae5f fix: remove dead code '\''type'\'' from second elif branch in optimize_schema() (Closes #4703)
The '\''type'\'' key is already handled by the earlier  branch,
making it unreachable in the later  block.
Remove it from the list to eliminate the dead code.

Signed-off-by: Cocoon-Break <54054995+kuishou68@users.noreply.github.com>
2026-04-19 09:39:43 +08:00