Commit Graph

1044 Commits

Author SHA1 Message Date
MagMueller 90a052371d Move user request before agent history 2026-05-23 09:48:15 -07:00
Saurav Panda ac5ff6dd15 Merge branch 'main' into prompt-cache/gemini-cached-content 2026-05-22 18:32:45 -07:00
Saurav Panda b06b47a23a Merge branch 'main' into prompt-cache/relocate-per-step-metadata 2026-05-22 18:11:00 -07:00
Saurav Panda a3bae60ed2 agent(prompts): move per-step metadata out of <agent_state> into a tail block
The step counter (Step N maximum:M) and datetime.now() were rendered
inside <agent_state>, ahead of <browser_state> in the user message.
The cache miss already happens at the <agent_state> boundary today, so
this isn't a live cache regression — but the layout meant that any
future move of more-stable agent_state fields into the system prompt
would still leave per-step varying bytes in the middle of the prefix,
silently capping how far the cache could extend.

Pull both fields into a new _get_step_meta_description() and append it
at the very end of get_user_message(), after <agent_state>,
<browser_state>, <read_state>, <page_specific_actions>, and unavailable-
skills info. Everything above this tail block is now eligible to be
treated as the cacheable region.

Adds regression tests that lock the layout:
- <step_info> must appear after <agent_state> and <browser_state>
- <step_info> must not leak back into <agent_state>
- bytes before <step_info> must be identical across two different step
  numbers (the step counter must not be in the prefix)
2026-05-22 17:58:13 -07:00
Saurav Panda 8236cc7506 agent(history): freeze HistoryItem + lock byte-prefix property in tests
For Gemini's implicit cache (and similar provider caches) to actually
hit step over step, the rendered transcript for steps 1..N-1 must be
byte-identical at step N and at step N+1. The agent already appends
HistoryItems immutably in practice, but nothing in the type prevented
a future caller from mutating one in place — which would silently kill
the cache from that byte onward.

Mark HistoryItem frozen so any mutation now fails loud at runtime
rather than slowly burning input tokens. Add a regression test set
that asserts the cache property directly:

- render(items[:N]) must be a strict byte-prefix of render(items[:N+1])
- to_string() is deterministic for identical inputs
- the prefix property holds across mixed entry shapes (normal steps,
  errors, system messages, follow-up tasks)
- conditional field inclusion doesn't collapse to ambiguous output

Known limitation, not addressed here: max_history_items compaction in
MessageManager.agent_history_description rewrites earlier bytes once
the cap is exceeded (the omitted-count message changes). That's a
larger redesign and deserves its own PR.
2026-05-22 17:55:43 -07:00
Saurav Panda df063f9068 feat(llm/google): forward cached_content into generate_content
Adds a cached_content field on ChatGoogle (and a per-call kwarg on
ainvoke) that gets threaded into the GenerateContentConfigDict before
each generate_content call. This lets callers point Gemini at an
explicit CachedContent resource (e.g. "cachedContents/abc123") instead
of relying on implicit caching, which is constrained by the ~5-minute
TTL window.

Token accounting already pulls cached_content_token_count from
response.usage_metadata into ChatInvokeUsage.prompt_cached_tokens, so
the savings show up in usage stats without further work.

Cache creation itself (client.caches.create) is left to the caller —
this PR only adds the forwarding hook so explicit caching becomes
opt-in usable. A follow-up can wire agent-side cache lifecycle if
useful.
2026-05-22 17:54:04 -07:00
Saurav Panda db39960431 fix(tokens): add OpenRouter pricing fallback 2026-05-22 13:53:29 -07:00
Saurav Panda 5c54ef4197 chore(llm): rename flash-lite recommendation to gemini-3.1-flash-lite 2026-05-22 10:49:12 -07:00
Saurav Panda b00c41b66b chore(llm): recommend gemini-3-flash-preview in examples and tests
- add gemini-3-flash-preview-lite to VerifiedGeminiModels and token mappings
- list both gemini-3-flash-preview[-lite] alongside existing -latest aliases in CLOUD.md and api-v2.md SupportedLLMs
- swap example code and recommendations (examples/, AGENTS.md, quickstart.md, bug report placeholder) to gemini-3-flash-preview / -lite
- update Google CI test + evaluate_tasks judge LLM to gemini-3-flash-preview / -lite
2026-05-22 10:47:58 -07:00
Mark McDonald 115199d2ba fix: handle types.HttpOptionsDict better, add more tests 2026-05-22 14:30:40 +08:00
Mark McDonald 2eb3e3cded feat: add client header to GoogleChat
Added header per integration
[guidelines](https://ai.google.dev/gemini-api/docs/partner-integration).
2026-05-22 14:20:24 +08:00
Saurav Panda 0fa8768d56 Merge branch 'main' into fix/sec-evaluate-restricted 2026-05-19 11:40:00 -07:00
Saurav Panda 8f2967a5c0 fix(tools): include prohibited_domains in evaluate guard
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.
2026-05-18 18:37:12 -07:00
Saurav Panda a7ce680948 fix(tools): refuse evaluate() on restricted browser profiles
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.
2026-05-18 18:30:04 -07:00
Saurav Panda 8c41cf795a fix(daemon): restrict unix socket file to owner-only access
The per-session HMAC auth token (de14b9aa) gates command dispatch, but
asyncio.start_unix_server creates the socket file with the process
umask, leaving it 0o755 by default. On multi-user hosts a co-tenant
can connect() and probe behavior even though the handshake will
ultimately fail.

Set umask to 0o077 around start_unix_server and chmod 0o600 after,
matching the auth-token file's posture.
2026-05-18 18:28:10 -07:00
Saurav Panda 29b03e43b4 Merge branch 'main' into fix/sec-download-path-traversal 2026-05-18 17:28:52 -07:00
Saurav Panda 7d061a0327 fix(security): fold IDNA label separators before IPv4 classification
Addresses third P1 codex review on #4866.

Per RFC 3490 / UTS46, four code points act as label separators in IDNA
processing — `.` (U+002E), `。` (U+3002 IDEOGRAPHIC FULL STOP),
`.` (U+FF0E FULLWIDTH FULL STOP), and `。` (U+FF61 HALFWIDTH IDEOGRAPHIC
FULL STOP). WHATWG URL parsing folds all four to `.` before resolution,
so `http://127。0。0。1/` and `http://127。0。0。1/` reach 127.0.0.1.

NFKC handles only U+FF0E and partially maps U+FF61 → U+3002, leaving the
IPv4 parser unable to match the dominant ideographic-dot form. Explicitly
replace U+3002 and U+FF61 with `.` after NFKC.

Test `test_idna_dot_separators_blocked` covers all four dot variants
both at `_is_url_allowed` and `_is_ip_address` levels, plus a combined
case with circled digits (`①②⑦。⓪。⓪。①`).
2026-05-18 16:40:24 -07:00
Saurav Panda e34d2cc6c2 fix(security): NFKC-normalize host before IPv4 classification
Addresses second P1 codex review on #4866.

WHATWG URL canonicalization maps fullwidth digits (`127.0.0.1`),
circled digits (`①②⑦.⓪.⓪.①`), and Unicode-prefixed hex forms
(`0x7f000001`) to ASCII IPv4 literals (`127.0.0.1` and `0x7f000001`)
before resolution. The classifier saw only the original string and
returned False, so `block_ip_addresses=True` was still bypassable by
re-encoding the IP with any equivalent Unicode digit variant.

Add a `unicodedata.normalize('NFKC', ...)` step after percent-decoding
and before parsing. NFKC handles all the bot's example forms and is
stdlib-only (no IDNA dependency). Wrapped in try/except to preserve the
never-throw invariant.

Tests:
- test_unicode_normalized_ipv4_blocked covers fullwidth, fullwidth+ASCII
  hex, and circled-digit forms at both `_is_url_allowed` and
  `_is_ip_address` boundaries.
- test_idn_domains_not_misclassified_as_ip is a false-positive guard
  ensuring legitimate IDN domains (`café.example`, `日本.example`, their
  punycode equivalents) remain classified as domains.
2026-05-18 16:26:51 -07:00
Saurav Panda b6ef0e2888 fix(security): percent-decode host before IPv4 classification
Addresses second codex review on #4866 (P1).

Chromium percent-decodes the host component before applying its IPv4
parser, so a URL like `http://%30x7f000001/` (decodes to `0x7f000001`,
i.e. 127.0.0.1) or `http://%31%32%37.0.0.1/` (decodes to `127.0.0.1`)
still reaches the IP address despite `block_ip_addresses=True`. Without
decoding, `_is_ip_address` sees the literal `%`-encoded string and
returns False — bypassing the block.

Call `urllib.parse.unquote` on the host before passing it to both
`ipaddress.ip_address` and `socket.inet_aton`. Wrap in try/except to
preserve the never-throw invariant for the classifier.

Adds two regression tests:
- test_percent_encoded_ipv4_blocked covers mixed (`%30x7f000001`), fully
  encoded canonical (`%31%32%37.0.0.1`), and fully encoded decimal
  (`%32%31%33%30%37%30%36%34%33%33`) bypass forms.
- test_malformed_percent_encoding_does_not_crash covers lone `%`, `%zz`,
  `%2` — `unquote` leaves these as-is and the classifier must not throw.
2026-05-18 16:17:26 -07:00
Saurav Panda b0d543fff4 fix(security): catch non-OSError failures from inet_aton
Addresses codex review on #4866.

`socket.inet_aton` raises `UnicodeEncodeError` (not `OSError`) for
hostnames containing lone surrogates — common in URLs produced by
URL-decoding malformed UTF-8. The earlier patch only caught `OSError`,
so `_is_ip_address('\udcff')` would propagate the exception through
`_is_url_allowed` and crash the navigation security check whenever
`block_ip_addresses=True`.

Restore the original code's defensive `except Exception` posture for
both `ipaddress.ip_address` and `socket.inet_aton`. The classifier
should never throw — it returns True (recognized IP) or False (not a
recognizable IP); downstream domain-allowlist handling then applies.

Regression test covers lone-surrogate hostnames in
`test_malformed_unicode_hostnames_do_not_crash_classifier`.
2026-05-18 15:52:46 -07:00
Saurav Panda e6acd12d1e Merge branch 'main' into fix/sec-ip-canonicalization 2026-05-18 15:50:55 -07:00
Saurav Panda 65a377c20f fix(tools): only rewrite upload path to FileSystem on local sessions
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.
2026-05-18 15:05:08 -07:00
Saurav Panda 1032b2eef3 Merge branch 'main' into fix/sec-upload-file-containment 2026-05-18 15:02:16 -07:00
Saurav Panda c2ac67ab90 fix(downloads): sanitize attacker-controlled filenames and verify containment
GHSA-rv9j-wqjp-2fv4 (critical), GHSA-66xh-g88g-2h8j, GHSA-hpr4-fqgr-xhj9.

`DownloadsWatchdog` joined attacker-controlled filenames from CDP
(`Page.downloadWillBegin.suggestedFilename`) and `Content-Disposition`
headers directly into the configured `downloads_path`. Strings like
`../../escape.bin` or `/etc/shadow.bak` would `os.path.join` outside the
downloads directory, writing the fetched bytes (also attacker-controlled
— the response body is the exploit content) to an arbitrary location
with the agent's process privileges.

`download_file_from_url` triggers passively for any
`Content-Disposition: attachment` response, so this is reachable from any
visited site — `allowed_domains` does not mitigate it.

Add two private helpers on DownloadsWatchdog:

- `_sanitize_download_filename(name)`: keep only the basename, normalize
  Windows separators, strip null bytes, fall back to `'download'` for
  empty / pure-traversal inputs.
- `_is_path_contained(path, dir)`: realpath containment check for the
  on-disk sinks.

Wire the sanitizer at every attacker-controlled filename ingress:

- `download_will_begin_handler` (CDP suggestedFilename → cache + events)
- `_handle_cdp_download` (same field, separate path)
- Network-monitor Content-Disposition parser
- `download_file_from_url` (suggested_filename argument)
- `_handle_download` (Playwright `download.suggested_filename`)

Wire the containment check at every on-disk write site:

- `download_file_from_url` write
- `_handle_download` (Playwright save_as path)
- `trigger_pdf_download` write (defense in depth — already basename'd)
2026-05-18 13:52:24 -07:00
Saurav Panda 626bda9072 fix(security): canonicalize non-standard IPv4 forms in block_ip_addresses
GHSA-xrfv-gg9f-wwjp, GHSA-g27c-8gp4-28cv.

`SecurityWatchdog._is_ip_address` only recognized IP strings that
`ipaddress.ip_address()` accepts — i.e. the canonical dotted-quad form
(`127.0.0.1`) and full IPv6. Chromium and the kernel resolver, however,
also accept several non-standard IPv4 representations:

  http://2130706433/     → 127.0.0.1   (decimal int)
  http://0x7f000001/     → 127.0.0.1   (hex)
  http://0177.0.0.1/     → 127.0.0.1   (octal)
  http://127.1/          → 127.0.0.1   (short-form)
  http://127.0.1/        → 127.0.0.1   (short-form)

`block_ip_addresses=True` was therefore trivially bypassed by re-encoding
the IP in any of these forms.

Fall back to `socket.inet_aton` after `ipaddress.ip_address()` fails — it
accepts the same liberal IPv4 forms the kernel resolver does, so the
classifier matches the browser's behavior.

The existing `test_ipv4_lookalike_domains_allowed` test was codifying the
buggy behavior for `1.2.3` (which IS a short-form IPv4 == 1.2.0.3).
Removed that assertion and added a dedicated `TestNonStandardIPv4Representations`
class covering decimal/hex/octal/short-form blocking, lookalike-domain
non-interference, and the interaction with `allowed_domains`.
2026-05-18 13:47:17 -07:00
Saurav Panda a209c5ed38 fix(tools): contain upload_file path inside FileSystem dir
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.
2026-05-18 13:43:34 -07:00
Saurav Panda 92defb6eae fix(mcp): default retry_with_browser_use_agent allowed_domains to None
GHSA-vfcm-843v-w6v3.

`retry_with_browser_use_agent` defaulted `allowed_domains` to `[]` when the
client omitted the argument, and then forwarded that value to
`BrowserProfile(allowed_domains=[])`. `SecurityWatchdog` interprets the empty
list as "no allowlist configured — allow every URL", silently disabling any
admin-configured allowlist on the underlying profile.

Default to `None` so admin profile defaults are preserved when the client
omits the argument, and treat an explicit empty list the same as omitting
(falsy override is not applied). Schema default removed and description
updated so MCP clients see the new contract.
2026-05-18 13:38:13 -07:00
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 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 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 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
Alezander9 f8f7be2e90 Fix test assertions to match updated error messages 2026-04-08 22:12:16 -07:00
ShawnPana 454dbfdaba style: flatten assert to pass pre-commit formatter 2026-04-02 12:34:27 -07:00
ShawnPana 7db93b7ac7 fix: accept shutting-down race in test_close_orphaned_daemon
SIGTERM may not kill daemon before the CLI re-probes, producing a
"daemon may still be shutting down" warning instead of "Browser closed".
2026-04-02 12:28:15 -07:00
ShawnPana f8fdddc66d fix: update cloud tests to use config.json for API key instead of env var 2026-04-02 11:56:54 -07:00
ShawnPana 1aa6f6dd3b fix: remove tests for deleted TunnelManager.is_available() 2026-04-01 22:06:02 -07:00
ShawnPana addefa0352 fix: add ActionHandler to test SessionInfo constructions
test_cli_upload and test_cli_coordinate_click create SessionInfo
without actions, hitting the type-narrowing guard. Now provide
ActionHandler in all test cases.
2026-04-01 21:58:58 -07:00
ShawnPana c312191e8e fix: update test_cli_connect for --connect deprecation 2026-04-01 20:06:42 -07:00
ShawnPana 0bf1f02d97 fix: CI failures — ruff formatting, type errors, test_setup_command
- Ruff format all skill_cli and test files
- Fix type: get_config_value returns str|int|None, callers cast properly
- Fix type: BrowserWrapper.actions is non-optional (always provided)
- Fix type: config comparison uses 'is' not '=='
- Rewrite test_setup_command for new setup.handle(yes=True) API
- Add None guard in test_cli_lifecycle for state file
2026-04-01 19:58:33 -07:00
ShawnPana 254f117cf2 chore: remove mock-based robustness tests (violates no-mock policy)
test_cli_lifecycle.py already covers the important paths with real
daemon subprocesses. The mock-heavy tests duplicated coverage with
less confidence.
2026-04-01 19:36:16 -07:00
ShawnPana 8577c789eb fix: close log file handle on successful daemon start in test 2026-04-01 19:29:27 -07:00
ShawnPana ca05f46352 refactor: remove --agent/register/tab-ownership, sessions-as-agents model
Multi-agent isolation is now achieved through separate sessions
(--session NAME), each with its own browser. Removed:
- register command and agents.json
- --agent flag and agent_id plumbing
- TabOwnershipManager and all tab locking logic
- dispatch lock and focus swapping between agents
- tab_ownership.py (deleted)
- test_tab_ownership.py (deleted)

Simplified tab commands: no lock checks, no _tab_list injection,
no _resolved_target_id params. agent_focus_target_id stays for
single-agent tab tracking.

Tested: 3 concurrent subagents on separate cloud sessions,
3 concurrent subagents on separate headless Chromium sessions.
2026-04-01 17:34:46 -07:00