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
- restricted read_long_content file access to only paths in available_file_paths or browser_session.download_files to mitigate injection
- made PDF page selection truncate pages that exceed char budget instead of dropping them entirely
- fixed truncation hint in file_system.py to reference read_long_context instead of search_page