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>
- event_result(raise_if_any=True) so a SwitchTabEvent handler failure
surfaces its actual cause through the except block instead of
becoming a generic 'produced no result' message (only one handler
is ever registered for SwitchTabEvent, so this is safe).
- Test file used a dynamically-built ActionModel that pyright can't
see the fields of; switched to a statically declared _TabActionModel
subclass, matching the pattern used elsewhere in this file.
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.
Raise 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 instead of a generic string, so
ActionResult.error carries actionable information back to the agent.
Adds regression tests covering the failing tab_id case and confirming
the happy path is unaffected.
`browser_use/tools/views.py:57` reads
# Backward compatibility alias
SearchAction = SearchAction
which is a no-op self-assignment, so the pre-rename name is not exported.
`SearchGoogleAction` (present through 0.7.x) has been unimportable since
`SearchAction` was introduced in 0.9.0 — `from browser_use.tools.views
import SearchGoogleAction` raises ImportError.
The sibling alias two blocks down does it correctly
(`GoToUrlAction = NavigateAction`), and the test suite still refers to the
old name in a comment (`# Test SearchGoogleAction`), so the alias was
clearly intended.
Also add a regression test asserting both aliases resolve to their current
classes.
ActionRegistry._match_domains returned True when the URL was empty (`not url`),
so a domain-restricted action was offered on a blank/unknown page URL. This
failed open and was inconsistent with the `page_url is None` path, which hides
restricted actions. An empty `page_url` is reachable (a freshly created
about:blank target has `target.url == ''`), so a domain-scoped action could be
exposed on the wrong context.
Only unrestricted actions (`domains is None`) short-circuit to available; a
restricted action with an unknown/empty URL now fails closed.
Add regression tests.
An unconditional re-raise sent BrowserErrors without long_term_memory (e.g.
upload_file's failure paths) into Tools.act's handle_browser_error, which
re-raises exactly those — escaping act() as an exception where callers
previously got a recoverable ActionResult(error=...).
Guard the bypass on long_term_memory being present (the exact condition
handle_browser_error formats without re-raising; short_term_memory alone
would still re-raise), and flatten plain BrowserErrors to RuntimeError as
before. Regression test covers the plain-BrowserError path through
tools.act.
Registry.execute_action's catch-all handler flattened BrowserError into a
generic 'Error executing action ...' RuntimeError, destroying the structured
short_term_memory/long_term_memory the error carries to steer the LLM's next
action (e.g. the list of available dropdown options when clicking a select).
The 'except BrowserError' branch in Tools.act that formats those memories
into an ActionResult was dead code for any action that let a BrowserError
propagate (upload_file, dropdown_options via event_result, extraction
handlers).
Re-raise BrowserError before the generic handlers so handle_browser_error
becomes the single formatting point again.
A flex item defaults to min-width:auto and won't shrink below its content,
so a long/unbroken URL in the default footer template overflowed and pushed
the page / totalPages span off the printable area. Give the url span
min-width:0 + overflow ellipsis so it truncates, and flex-shrink:0 on the
page-count span so it always renders. Adds a long-URL regression test.
The save_as_pdf action now renders page metadata into the PDF margins by
default, matching Chrome's Print dialog: the date in the header and the page
URL plus page numbers in the footer.
- Add display_header_footer (default True), header_template, and
footer_template params to SaveAsPdfAction.
- Pass displayHeaderFooter + header/footer templates and explicit margins to
CDP Page.printToPDF so the metadata has room to render (Chrome clips it
otherwise, and defaults header/footer font-size to 0px).
- Default templates show the date / URL + page numbers; callers can override
with custom HTML or disable entirely for a clean PDF.
- fable for coord clicks
- explicit support
- model refusal response
- fable pricing with 5m/1h cache writes and inference geo multiplier
- anthropic fallback, thinking, output config, and inference geo request support
Drop the running narrative; the function name + the one-line why is
enough. Error message stays actionable but no longer reiterates the
threat model.
No behavior change; 6/6 tests pass.
SecurityWatchdog enforces prohibited_domains as a navigation restriction
alongside allowed_domains. Without prohibited_domains in the evaluate()
guard, a profile using only the deny-list could still load an allowed page
and have the agent fetch() into a blocked domain via JS.
Add prohibited_domains to the restriction check.
The agent's evaluate() action called Runtime.evaluate directly through
CDP. SecurityWatchdog only subscribes to navigation events, so JS
running inside an already-allowed page could fetch() arbitrary internal
URLs, read cookies and localStorage from any allowed origin's context,
and otherwise act as if allowed_domains / block_ip_addresses weren't
configured.
When a profile has allowed_domains or block_ip_addresses set, the
operator has signalled the agent is constrained — exposing an unmediated
JS evaluation primitive contradicts that signal. Refuse evaluate()
outright on such profiles; agents that need JS can run on an unrestricted
profile.
Empty allowed_domains=[] is treated as 'no restriction' elsewhere in the
codebase (e.g. SecurityWatchdog); evaluate() behaves consistently and
does not refuse in that case.
Addresses codex review on #4865.
For remote (`is_local=False`) sessions, `params.path` is meant to reference
a file on the remote machine. A coincidental basename collision with a
local FileSystem-managed file (e.g. `/tmp/note.md` colliding with a local
managed `note.md`) would silently rewrite the upload to point at the local
file, uploading the wrong file with no indication to the agent.
Gate the FileSystem rewrite on `browser_session.is_local`. On remote
sessions, fall through to the existing pass-through branch that allows
remote-accessible absolute paths.
Add a regression test that exercises the remote-session basename-collision
case and asserts the local FileSystem path never appears in the resolved
upload path.
GHSA-j9hj-92j8-jv9h.
The `upload_file` action constructed the absolute upload path by joining
`file_system.get_dir()` with the agent-controlled `params.path`. Because
`FileSystem.get_file()` matches by basename (`os.path.basename` first), an
agent-controlled path like `../note.md` would:
1. Pass `get_file()` lookup if a file named `note.md` exists in the FileSystem.
2. Be naively joined to `data_dir`, producing `data_dir/../note.md` — which
resolves to a sibling file outside the FileSystem directory.
3. Be uploaded to the browser as the resolved (escaped) file, surfacing
arbitrary contents from outside `browseruse_agent_data` to whatever file
input was targeted.
Use the FileSystem-owned `file_obj.full_name` for the join. Add a
`os.path.realpath` containment check as defense in depth; if it ever resolves
outside `data_dir`, refuse the upload.
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).
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.
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.
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.
- AGI-569: after any click that opens a new tab, automatically dispatch
SwitchTabEvent so the agent lands on the new page immediately instead
of requiring a manual switch step (~877 occurrences)
- AGI-548: for <input type="checkbox/radio">, capture checked state
before the CDP mouse click and verify it toggled afterward; if
unchanged (custom-styled or shadow-DOM-backed inputs), fall back to
JS element.click() and report the final checked state in metadata
(~1,241 occurrences)
- Add `browser-use upload <index> <path>` command for uploading files to
file input elements via the CLI
- Extract find_file_input_near_element from nested closures in tools/service.py
to a reusable method on BrowserSession, deduplicating two copies
- Add BrowserWrapper.upload() for the Python REPL
- Resolve file paths to absolute on the client side before sending to daemon
- Update SKILL.md files and README with upload command docs