| Files | Last commit | Last update |
|---|---|---|
fix(a11y): return clean error for invalid CSS selectors (#1604) An invalid selector passed to a11y --selector made document.querySelector throw, and the exception bubbled up as a raw evaluation error with isolated-world line numbers (and, with --json, an embedded stack trace). A valid-but-missing selector already returned a clean message; align the invalid case by catching the throw and returning 'Invalid selector: <sel>'. | 3 months ago | |
fix(chrome): discover macOS user applications (#2388) * fix(chrome): discover macOS user applications Search ~/Applications after /Applications so user-installed browsers launch automatically. Co-Authored-By: ch3rry <1595486059h@gmail.com> * test(chrome): cover user application discovery Check filesystem selection and preserve system-browser priority. Document the macOS search locations in CLI, MCP, and browser references. * fix(mcp): sync browser discovery description --------- Co-authored-by: ch3rry <1595486059h@gmail.com> | 3 days ago | |
fix(flow): send run requests only to public addresses (#2432) * fix(flow): send run requests only to public addresses * fix(flow): do not filter proxy hosts from the environment * fix(flow): refuse proxy hosts as targets and close classifier gaps * fix(flow): build proxies explicitly, honor NO_PROXY, drop name-based exemption * fix(flow): read proxy env like hyper-util and resolve proxy names asynchronously * fix(flow): an unreachable proxy fails only the requests that would use it A proxy name that did not resolve aborted every run, even for targets NO_PROXY sends direct. The proxy now keeps its name and the resolver refuses it with the lookup error, so only requests routed through it fail. * fix(flow): a flow recorded against a name that resolves locally keeps running The resolver refused every name that resolved to a non-public address, so flows recorded against lvh.me, .test or intranet hosts stopped working. flow save now records reach local when the browser reached the start host directly at non-public addresses, on the URL's port, that the name still resolves to; a run of that flow admits only local addresses. A recording through any proxy reports the proxy's port, so it never counts as local. * fix(flow): look up an http proxy only when a request uses it, and name reach in local refusals Every http:// proxy name was resolved before the first request, so a stale or off-VPN proxy stalled each run up to the lookup timeout even when NO_PROXY excluded every target. The proxy URL now carries a stand-in name, unique to the run, that the resolver maps back to the real name when a connection to the proxy is made. A flow file without a recorded reach that hits a local address now says to record it again so the file gets "reach": "local". * fix(flow): say why a flow recorded through a proxy is not marked local A recording through a proxy stores the proxy's address, so flow save records no reach, and the run's advice to save again produced the same file. The save report now carries reachNote naming the addresses seen and asking for a direct recording, and the run's refusal says the same. * fix(flow): show reachNote in flow save text output The default CLI output and MCP text dropped reachNote, hiding why a flow recorded through a proxy cannot run directly. * fix(flow): a local flow never reaches link-local or metadata addresses, and a loopback recording stays on loopback Reach::Local admitted every non-public address, so a flow saved as local for a name that once resolved to loopback followed that name to 169.254.169.254. A recording whose addresses are all loopback now saves reach loopback, localhost flows are loopback-only, and a local reach refuses link-local and cloud metadata addresses. The save note was produced only when both the recorded and the resolved addresses were non-public, so a recording with no start-host address or a proxy at a public address got no reach and no note. Any start host that resolves only to local addresses without a matching direct connection now gets the note. * fix(flow): refuse a link-local or metadata IP literal before sending IP-literal hosts skip the resolver, so a flow whose start URL was a link-local literal still sent to 169.254.x. The host check now refuses link-local and cloud metadata literals for every request and redirect. | 2 days ago | |
fix(react): pick the react-dom renderer instead of hardcoding id 1 (#1491) `react tree`, `react inspect`, and `react suspense` read `hook.rendererInterfaces.get(1)`, assuming the first renderer to call `hook.inject()` is react-dom. In Turbopack RSC apps (e.g. Next.js 16.3+) the `react-server-dom-turbopack` Flight client registers first as id 1 with zero fiber roots, so `get(1)` walked an empty interface and every command silently reported nothing (`react suspense` -> "0 boundaries", `react tree` -> empty). Introduce a shared `__abPickReactRendererId` helper, injected via a `{{PICK_RI}}` placeholder, that selects the first non-Flight renderer with mounted fiber roots (falling back to any renderer with roots, then any renderer). Use the chosen id for `inspectElement` as well. Add unit tests asserting the scripts inject the picker and no longer hardcode renderer id 1. | 4 months ago | |
Prefer snapshots over screenshots (#2453) * Prefer snapshots over screenshots in the core skill and chat prompt * Never screenshot to check for an element; wait and re-snapshot * Screenshot only when asked or for content the DOM cannot represent | 1 day ago | |
fix(cli): scroll at the current pointer (#2275) * fix(cli): dispatch wheel input at the tracked pointer Route wheel events through the shared mouse-event builder so a preceding mouse move controls the dispatch position. Keep explicit protocol coordinates and scroll deltas intact, and add a real Chrome regression fixture. Co-Authored-By: David Gaynor <david@aheadhealth.com> * docs(mouse): describe wheel pointer behavior Document that wheel input starts at the tracked pointer and add the move-then-wheel recipe across CLI help, README, docs, MCP description, and the core command reference. * chore(mcp): sync the mouse wheel description Regenerate the shipped MCP catalog from the updated wheel tool description. * fix(cli): keep raw mouse position in sync Track successful raw mouse input so later wheel events use the current position. Cover nested scrolling and explicit coordinates. * fix(mouse): preserve pointer after rejected wheel events --------- Co-authored-by: David Gaynor <david@aheadhealth.com> | 9 days ago | |
fix: preserve Safari and iOS WebDriver sessions (#2385) * fix: preserve Safari and iOS WebDriver sessions Recognize WebDriver sessions in command startup and launch reuse, and release their resources before replacing them. Co-Authored-By: Lucas Guedes <13969616+lukasedw@users.noreply.github.com> Co-Authored-By: Jony <13896935+zyz619963502zyz@users.noreply.github.com> * test: cover WebDriver session reuse and replacement * fix: replace terminated WebDriver sessions Check the existing session before reusing a matching launch and bound cleanup of unresponsive drivers. * test(webdriver): mark the reuse test ignored on macOS (#2401) The test returned early on macOS and reported a pass without running. Use cfg_attr ignore so the skip shows in test output. Co-authored-by: roboeve[bot] <311457182+roboeve[bot]@users.noreply.github.com> --------- Co-authored-by: Lucas Guedes <13969616+lukasedw@users.noreply.github.com> Co-authored-by: Jony <13896935+zyz619963502zyz@users.noreply.github.com> Co-authored-by: roboeve[bot] <311457182+roboeve[bot]@users.noreply.github.com> | 5 days ago | |
fix(flow): send run requests only to public addresses (#2432) * fix(flow): send run requests only to public addresses * fix(flow): do not filter proxy hosts from the environment * fix(flow): refuse proxy hosts as targets and close classifier gaps * fix(flow): build proxies explicitly, honor NO_PROXY, drop name-based exemption * fix(flow): read proxy env like hyper-util and resolve proxy names asynchronously * fix(flow): an unreachable proxy fails only the requests that would use it A proxy name that did not resolve aborted every run, even for targets NO_PROXY sends direct. The proxy now keeps its name and the resolver refuses it with the lookup error, so only requests routed through it fail. * fix(flow): a flow recorded against a name that resolves locally keeps running The resolver refused every name that resolved to a non-public address, so flows recorded against lvh.me, .test or intranet hosts stopped working. flow save now records reach local when the browser reached the start host directly at non-public addresses, on the URL's port, that the name still resolves to; a run of that flow admits only local addresses. A recording through any proxy reports the proxy's port, so it never counts as local. * fix(flow): look up an http proxy only when a request uses it, and name reach in local refusals Every http:// proxy name was resolved before the first request, so a stale or off-VPN proxy stalled each run up to the lookup timeout even when NO_PROXY excluded every target. The proxy URL now carries a stand-in name, unique to the run, that the resolver maps back to the real name when a connection to the proxy is made. A flow file without a recorded reach that hits a local address now says to record it again so the file gets "reach": "local". * fix(flow): say why a flow recorded through a proxy is not marked local A recording through a proxy stores the proxy's address, so flow save records no reach, and the run's advice to save again produced the same file. The save report now carries reachNote naming the addresses seen and asking for a direct recording, and the run's refusal says the same. * fix(flow): show reachNote in flow save text output The default CLI output and MCP text dropped reachNote, hiding why a flow recorded through a proxy cannot run directly. * fix(flow): a local flow never reaches link-local or metadata addresses, and a loopback recording stays on loopback Reach::Local admitted every non-public address, so a flow saved as local for a name that once resolved to loopback followed that name to 169.254.169.254. A recording whose addresses are all loopback now saves reach loopback, localhost flows are loopback-only, and a local reach refuses link-local and cloud metadata addresses. The save note was produced only when both the recorded and the resolved addresses were non-public, so a recording with no start-host address or a proxy at a public address got no reach and no note. Any start host that resolves only to local addresses without a matching direct connection now gets the note. * fix(flow): refuse a link-local or metadata IP literal before sending IP-literal hosts skip the resolver, so a flow whose start URL was a link-local literal still sent to 169.254.x. The host check now refuses link-local and cloud metadata literals for every request and redirect. | 2 days ago | |
Fix Windows CI - Keep mcp-tools.json line endings as LF so the snapshot test passes on Windows - Use std::env::home_dir so Windows respects USERPROFILE when finding the home directory | 6 days ago | |
fix(flow): wait for in-flight requests on save and finalize redirect hops (#2433) * fix(flow): wait for in-flight requests before saving flow save cloned the recorded entries right after one drain, so a request the last step had started but not finished had no response body and was dropped. The save now drains until requests tagged to the saved steps finish, bounded by the command timeout. * fix(flow): do not wait on third-party or streaming requests when saving The save waits only for unfinished requests that could become flow requests: first-party, not telemetry, XHR/Fetch with a JSON (or not yet known) type, or a document. Event streams and other non-JSON responses never delay it. * fix(flow): bound the save wait by redirects, the client read timeout, and report what is left out A redirect hop never finishes, so it no longer counts as an unfinished request. flow_save carries the default timeout like the wait commands, so the client read timeout follows it. Requests still loading at the deadline are reported as gaps. * fix(har): finalize a redirect hop when the redirect is followed Network.requestWillBeSent with redirectResponse now fills the previous entry for that request id (status, headers, mime type) and stamps its finish time, so the hop no longer stays incomplete in har stop and network requests. The flow save wait no longer needs a special case for it. * fix(test): box the command future at execute_command The debug-build command future is large enough to overflow a test thread's stack when a test awaits it directly; it now lives on the heap for every caller. * fix(flow): redact and scope still-loading gaps, finalize redirect hops in network requests * fix(flow): keep secret and login gaps, detect undecodable secrets, return the final redirect hop A step with a loading request only loses its generic no-request reason, not the secret-drop or login-wall gaps. Secret detection decodes percent escapes that are not valid UTF-8 instead of skipping the comparison. network request returns the last hop that shares a request id. The har_compute_timings doc comment is back on its function. * fix(flow): decode every escape leniently when looking for a secret The form-encoded comparison and the dropped-request label use the lossless decode too, and an undecodable escape is kept exactly as written, so a lowercase escape in a secret still matches. * fix(flow): flow save does not wait on a long poll, and names requests still loading when it fails A first-party JSON request whose headers came and whose body never ended kept every save waiting for the whole command timeout. A response with headers that gets no bytes for the network idle window is no longer waited on and is reported as still loading. A save that failed for want of requests returned before the still-loading gaps were added, so the error hid the request that explained it. The error now names them. * fix(flow): flow save waits on a request with no answer only if the last step started it A request a long-poll server holds without writing headers kept every save waiting for the whole command timeout. A request with no response that an earlier step started has outlived the steps after it, so it is reported as still loading instead of waited on. One the last step started is still awaited up to the command timeout. * fix(flow): flow save reads --timeout The save path never read the flag, so a request the last step left hanging still blocked for the default timeout. The CLI and the MCP tool now pass it. | 2 days ago | |
fix: save_state captures cross-domain cookies and localStorage (#1064) The Rust rewrite of save_state only captured cookies and localStorage for the current page's origin, silently dropping cross-domain data (e.g. SSO/CAS auth cookies). This was a regression from the JS version. Cookies: replace Network.getCookies with Network.getAllCookies to return cookies from all domains the browser has visited. localStorage: track visited origins in BrowserManager during navigation, then collect their localStorage via a temporary CDP target with Fetch interception (serves blank HTML to avoid real network requests). Co-authored-by: hyunjinee <leehj0110@kakao.com> | 7 months ago | |
fix(daemon): keep security settings across daemon restarts (#2429) * fix(daemon): keep security settings across daemon restarts Allowed domains, action policy and confirm actions were lost with the daemon, so a restart silently dropped containment (#1894). They are now stored in an owner-only <session>.settings file before they apply, read back at daemon start, and removed by close. A file that cannot be used stops the daemon from starting. * docs: document persisted security settings * fix(daemon): harden stored security settings A CLI-driven daemon restart no longer clears the file, and close clears it only after the daemon confirmed the close, so a denied or parked close leaves the session contained. A relative action policy path is made absolute before it is stored. close --all also clears files of sessions whose daemon is gone. The boot refusal names the namespace and fits the stderr cap. The file is written with O_EXCL, read without following symlinks, and must belong to the daemon's user. * docs: describe when stored security settings are cleared * fix(daemon): close --all keeps settings of sessions that did not close close --all cleared the stored settings and reported success for a close that policy denied or that waited for confirmation. It now treats those as not closed. The legacy AGENT_BROWSER_POLICY path is stored absolute like the current variable, a ready daemon stores its security settings again so a racing sweep cannot leave it uncontained, and the boot hint quotes the session and namespace for the shell. * fix(cli): close keeps stored settings and policy paths stay valid UTF-8 close no longer deletes the stored settings before the daemon starts, so a policy that denies close still applies when the daemon was gone; the file is cleared only when the daemon refuses to start because of it. A policy path that is not valid UTF-8 after resolving is rejected instead of being stored lossy, and the path test no longer assumes forward slashes. * fix(cli): close --all closes sessions without a daemon under their stored policy close --all deleted the settings file of every session with no running daemon, so a policy that denies close was bypassed. Those sessions are now closed through the same path as close: a daemon starts with the stored settings, a denied or parked close keeps the file and is reported as failed, and a file the daemon refuses is cleared. * fix(cli): name the socket dir in the refusal hint and stop orphan daemons a failed close --all started * fix(cli): close clears an empty directory in place of the settings file and names an entry it cannot remove * test(cli): close --all leaves no daemon for a dead session it could not close * fix(cli): sync the settings directory after the rename and give the two-step way to confirm a parked close * fix(daemon): restore a stored empty allowed-domains list as no filter * test(daemon): a separator-only allowed-domains list leaves no filter | 2 days ago | |
feat: preserve refs across snapshots (#1812) * Preserve refs across snapshots * test: reproduce iframe ref reuse across document boundaries * fix: invalidate refs across frame document and session changes --------- Co-authored-by: Pete Hunt <239742+petehunt@users.noreply.github.com> | 1 month ago | |
fix(download): make wait --download find finished downloads and place the file (#2027) * fix(download): make wait --download find finished downloads and place the file wait --download subscribed to events only when it ran, so a download that finished after the triggering command was missed. It also returned the requested path without moving the file, which stayed wherever the browser saved it. Enable Browser download events at launch and connect while keeping the configured location, record downloads from the event drain, and have wait --download claim the oldest unclaimed finished download, move it to the requested path and fail when no file is there. Without a path it reports where the browser saved the file. Downloads handled by the download command are never claimed. Fixes #561 Fixes #1300 Co-Authored-By: Wian Broes <wian.broes@gmail.com> Co-Authored-By: justjavac <justjavac@gmail.com> * fix(download): wait for the file Chrome is still flushing before moving it * fix(download): claim completions missed before the wait subscribes and keep failed moves claimable Events that arrived after the per-command drain were not delivered to the new receiver, so a finished download timed out. A failed move to the destination also consumed the download, so a retry with a valid path timed out. * fix(download): only move files reported by a browser agent-browser launched The saved path comes from a CDP event. Over --cdp, --auto-connect, or a provider, the remote endpoint controls it and could point wait --download <path> at any local file. Reject the move unless the browser was launched locally, and require the file to be inside the configured download directory when one is set. * fix(download): leave CDP browsers' download settings alone and restore the configured dir after download CDP connects no longer reset Browser.setDownloadBehavior, and the wait skips the local file check for them. download restores the configured directory when it finishes. A refused move releases the download. --------- Co-authored-by: Wian Broes <wian.broes@gmail.com> Co-authored-by: justjavac <justjavac@gmail.com> | 14 days ago | |
fix(flow): wait for in-flight requests on save and finalize redirect hops (#2433) * fix(flow): wait for in-flight requests before saving flow save cloned the recorded entries right after one drain, so a request the last step had started but not finished had no response body and was dropped. The save now drains until requests tagged to the saved steps finish, bounded by the command timeout. * fix(flow): do not wait on third-party or streaming requests when saving The save waits only for unfinished requests that could become flow requests: first-party, not telemetry, XHR/Fetch with a JSON (or not yet known) type, or a document. Event streams and other non-JSON responses never delay it. * fix(flow): bound the save wait by redirects, the client read timeout, and report what is left out A redirect hop never finishes, so it no longer counts as an unfinished request. flow_save carries the default timeout like the wait commands, so the client read timeout follows it. Requests still loading at the deadline are reported as gaps. * fix(har): finalize a redirect hop when the redirect is followed Network.requestWillBeSent with redirectResponse now fills the previous entry for that request id (status, headers, mime type) and stamps its finish time, so the hop no longer stays incomplete in har stop and network requests. The flow save wait no longer needs a special case for it. * fix(test): box the command future at execute_command The debug-build command future is large enough to overflow a test thread's stack when a test awaits it directly; it now lives on the heap for every caller. * fix(flow): redact and scope still-loading gaps, finalize redirect hops in network requests * fix(flow): keep secret and login gaps, detect undecodable secrets, return the final redirect hop A step with a loading request only loses its generic no-request reason, not the secret-drop or login-wall gaps. Secret detection decodes percent escapes that are not valid UTF-8 instead of skipping the comparison. network request returns the last hop that shares a request id. The har_compute_timings doc comment is back on its function. * fix(flow): decode every escape leniently when looking for a secret The form-encoded comparison and the dropped-request label use the lossless decode too, and an undecodable escape is kept exactly as written, so a lowercase escape in a secret still matches. * fix(flow): flow save does not wait on a long poll, and names requests still loading when it fails A first-party JSON request whose headers came and whose body never ended kept every save waiting for the whole command timeout. A response with headers that gets no bytes for the network idle window is no longer waited on and is reported as still loading. A save that failed for want of requests returned before the still-loading gaps were added, so the error hid the request that explained it. The error now names them. * fix(flow): flow save waits on a request with no answer only if the last step started it A request a long-poll server holds without writing headers kept every save waiting for the whole command timeout. A request with no response that an earlier step started has outlived the steps after it, so it is reported as still loading instead of waited on. One the last step started is still awaited up to the command timeout. * fix(flow): flow save reads --timeout The save path never read the flag, so a request the last step left hanging still blocked for the default timeout. The CLI and the MCP tool now pass it. | 2 days ago | |
feat(flow): record a browser task once and replay its requests (#2236) * feat(flow): add saved flow recording, storage and HTTP replay Records the commands of a task with the first-party JSON requests each one fired, stores them without credentials, and replays the output request directly with a response shape check. * feat(flow): add flow start, save, run and list commands The daemon tags HAR entries with the running command and announces saved flows for a site after navigation. A failed run marks the flow unverified only when the site changed, not when params were rejected. * feat(flow): print flow results and the saved flows hint The hint shows recorded param values, the recorded page and item field names so an agent can call a flow without guessing its inputs. * feat(mcp): expose saved flows as core tools Adds flow list, run, start and save to the core profile, carries the saved flows hint in navigation results, and formats flow results as text. * docs(skills): document saved flows in the core skill Covers when to prefer flow run over driving the page, how to record a flow, and points derive-client at it for tasks the agent repeats. * fix(network): let mocked cross-origin responses pass CORS A route with --body answered cross-origin requests without CORS headers and with no content type, so the page saw a network error and HAR capture dropped the body. Mocked responses now echo the request origin, answer preflights, and label JSON bodies as application/json. * feat(flow): fall back to the page and to a command replay A run now tries direct HTTP, then the same request from the page, then a replay of the recorded commands, and starts the next run at the tier that worked. Every tier checks the recorded response shape; a change marks the flow unverified and is reported as drift. Session routes answer the HTTP tier too, so mocks reach every tier. A write the site accepted is never sent again by a lower tier, and a replay refuses params it cannot apply. * feat(flow): find params and references when saving A value a request sent that came from an item of an earlier list becomes a param with its source; a value copied from a single field is filled from that response on each run, and the requests it depends on run first. A login form on the page at save time names the last gap. The saved flows hint says where each detected param comes from. * feat(webmcp): list saved flows next to declared tools webmcp list now shows the saved flows for the page's site under an observed section, so declared and observed tools can be compared. * fix(flow): keep each GraphQL request and treat queries as reads Requests were deduplicated by method and URL, so a GraphQL app kept only its first POST. The body is now part of the key, and a POST that only carries GraphQL queries no longer needs --allow-write; mutations still do. * feat(flow): record a flow on an already open page flow start keeps the tab's URL, so a task that begins on a logged-in page can be saved without an open command, and a replay starts from that page. * fix(flow): replay recorded commands on live apps Snapshot refs are gone by the next run, so commands that used one are saved as the role and name it pointed at. Replayed commands wait for an element the page is still rendering, and a captured request matches a recorded one by its body or GraphQL operation, since one endpoint serves every query. * fix(flow): send writes the way a browser does The HTTP tier now sends Origin on writes, taken from the recorded referer, since sites check it before accepting one. A write the site did not accept may carry stale page tokens, so it moves on to the page and the replay tiers instead of stopping. * fix(flow): chain only generated values from read responses Short words and locale codes repeat across a site's responses, so they turned into long chains of references, some through POST requests. A reference now needs a value that looks generated, comes only from a read, and replaces whole tokens, never part of a longer word. * feat(flow): take params from the start URL and find nested lists A query value of the recorded page URL that the API requests also sent, such as ?keyword=, becomes a param without --param. --where, --fields and the hint's item fields now use the heaviest list anywhere in the response, since GraphQL nests it under data. * feat(flow): read JSON embedded in server-rendered pages Sites that render on the server send no API request for their main data, but often embed it in the HTML as a JSON script, a Next.js __NEXT_DATA__ block or a window.x = {...} assignment. flow save now keeps those blocks from first-party documents as requests that fetch the page and read the same block back, so they get params, shape checks and --fields like any API response. The page's own path segments and blocks of the same document are no longer taken as params or references. * fix(flow): treat a response without its lists as no results Many APIs drop an empty list and its counts instead of sending []. That used to read as a changed shape and mark the flow unverified. When only lists and counts are missing and no new list appeared, the run now returns no items and leaves the verified state as it was. * docs: document saved flows Adds a saved flows section to the network page and two lines to the README command list: recording, params, the run tiers, discovery and storage. * feat(flow): describe saved flows in the shape of WebMCP tools Saved flows now carry a description and an inputSchema built from their params, returned by flow list and shown by webmcp list after the tools the page declares. When a page declares WebMCP tools, they come before the saved flows hint, and the hint suggests them first. flow run still runs flows; webmcp invoke only calls the page's own tools. * fix(flow): close the gaps found in security review - Every resolved request URL, and every redirect the HTTP tier follows, must pass the session's current domain allowlist. - A command replay sends every write the recording caused, so it needs --allow-write whenever the flow recorded one, not only for writes the output depends on. - Param values are substituted into parsed JSON strings, so quotes and backslashes keep request bodies and replayed commands valid. - Suggested commands shell-quote recorded param values, and inferred param names keep only letters, digits and underscores. - Values typed into password-like fields are not stored, auth and credential commands are not recorded, and flow files and directories are created owner-only. - webmcp list prints each saved flow's inputs as name, type and recorded value. * fix(flow): keep typed secrets out of saved commands and requests type stores its input in text, which the redaction skipped. Both fields are now redacted, and the typed value is kept in memory during the recording so that flow save drops any captured request whose URL, body or headers carry it, and reports why. * feat(flow): capture flows with --capture flow and save several per session Recording moves from `flow start` to a launch flag. Every recorded command returns recorded.seq, failed commands stay in the recording as ok:false, and `flow steps` lists them. `flow save <name>` takes --from/--to/--description, no longer resets the recorder, and close reports unsaved steps. The hint hides only flows saved in the same session, and har stop is refused while capture owns the recorder. Flow files are version 2; older files must be saved again. * fix(flow): keep runs on the flow's own site A flow records the base domain of its start URL as allowedHosts. A run rejects any request, redirect or recorded navigation outside it, whatever --allowed-domains says, and the in-page tier only runs on a tab that is already on that site. Flow origins are validated as hostnames before they become directory names, and IP hosts no longer collapse to a shared base. * feat(flow): list writes at save and confirm each one before a run flow save records writes[{id, method, url, kind}] with kind explicit or suspected. A GET is suspected when a click fired it and no output depends on it, or when its path has a whole logout, signout, unsubscribe, delete or remove segment. --allow-write takes all or write ids, in the CLI and in flow_run. A write it does not cover asks for confirmation as policy action flow_write:<id>, so --action-policy and --confirm-actions reach the http and page tiers; the command replay checks the writes no request covers. * feat(flow): redact secret fields by what the page says and keep typed values as params Recording reads type, autocomplete and the field's name, id, label and placeholder from the element behind a fill or type, so an unlabeled one-time-code or cc-* input is redacted. Any other typed value becomes an auto-named param with source typed, stored as a placeholder in the step, or the --param name when one has the same value. Params record their source (typed, query, response) and the file lists redactedSteps. * feat(flow): stop a run on auth, rate limit and challenge responses A 401 or 403 returns auth_expired, a 429 returns rate_limited with retryAfter when the site sent one, and a bot challenge page returns blocked. Each stops the chain instead of falling to a lower tier, carries a machine-readable code and leaves the flow's verified state alone. * fix(flow): ask only about the writes a run sends A run now gates the writes among the requests its output needs, not every write the recording caused; the command replay checks the rest. Adds a Chrome e2e against a local fixture: sign-in cookie, CSRF write through a response value, GraphQL query, a GET side effect, a cross-domain redirect, and two saves in one session. commands.mdx stays as it was because the docs route fixtures freeze it. * fix(flow): close review findings on credentials, redirects, replay and typed values - A 401 or 403 on a request recorded with stripped credential headers falls through to the page and replay tiers; auth_expired is reported when every way is refused. - A blocked redirect returns its 3xx, a write with a 2xx or 3xx answer is accepted, a write with no answer returns outcome_unknown and is never resent, and the in-page write does not follow redirects. - The replay runs only plain interactions, checks every URL in a recorded command, and stops when the tab leaves the flow's site. - keyboard type and inserttext read the focused element; typed params keep no example. - base_domain uses the public suffix list; a flow whose origin directory does not match its start URL must be saved again. - A save of an earlier range no longer moves the save marker back, and challenge pages are recognized only in HTML. * fix(flow): keep vault and key-by-key passwords out of flows and close URL and retry gaps - Replay URL checks read every string the way the URL parser does, so tabs, newlines and leading control characters cannot hide a host, and URL keys are checked whatever the value looks like. - The in-page fetch sends a write once; a thrown fetch is outcome_unknown. - A command that is not recorded tags no requests to the previous step, and the password auth login fills is registered as a secret. - Keys pressed in a secret field are gathered into one secret, redacted from the steps, and requests carrying it are dropped at save. * fix(flow): keep modifier chords and clipboard text out of secret handling gaps A key joins a secret only when it is bare or Shift plus a key, so Control+a no longer corrupts the buffer. A paste chord or clipboard paste into a secret field redacts the step and drops the requests it tagged, and clipboard write text is redacted and registered as a secret. * fix(flow): register a pasted password once the paste has finished After a paste chord or clipboard paste into a secret field, the focused field's value is read and registered as a secret in full, with any typed prefix, and later keys extend it. A request a later step sends with it is no longer saved. When the value cannot be read, the requests tagged to the paste step are still dropped. * fix(flow): replay once per run, recognize encoded secrets and link writes to earlier responses - The command replay runs at most once per run; every request that falls back to it reads its answer from that replay, and it is skipped when another tier already sent a write it would repeat. - Requests carrying a secret are recognized in form-encoded, percent-encoded and JSON-escaped forms. - A write's request takes a value from an earlier response and depends on it, so a rotating token is read fresh at each run. * fix(flow): never resend a replayed write, keep rejected writes eligible, match replays by query - Before any tier sends a write, the run checks whether it was already sent. A write the replay sent is answered from the replay's capture, and stops with outcome_unknown when the capture has no answer. - Only a 2xx or 3xx answer, or an unknown outcome, counts as a sent write; a 4xx rejection leaves the replay available. - Replayed requests match by method, host, path, recorded query and body, ignoring query parameters the recording never had; a missing or ambiguous match fails for that request. JSON bodies compare by value. * fix(flow): read the flow once per run and carry it through confirmation A flow_run loads its file once. Policy, the write gate and the run use that copy, and a confirmation keeps it so the confirmed run executes the file that was checked, not one read again from disk. The write gate is one function used before the run and inside it. * refactor(flow): one secret-field predicate and one header classifier FieldTraits::looks_secret replaces four copies of the type, autocomplete and name check. HeaderClass names the four ways a recorded header is treated, in place of three separate lists. * refactor(flow): type the saved flow and move to a module directory Flow, Step, Request, Write, Param and Tier are serde types with unknown fields rejected, so the file schema is checked on load and a run no longer walks serde_json::Value. Request.write replaces the writes lookups in the run path. The file format is version 1; nothing older has shipped, so the old-version path is gone. Fixtures build flows through serde_json. * refactor(flow): one write ledger and next_tier decide when a request is sent again WriteLedger records which writes a run sent, and next_tier picks the next tier from the attempts so far. They replace the accepted, retry, completed and already-sent checks that were spread over the run loop, the replay and its guard. A write that was sent is only read back from the replay's capture. * refactor(flow): a Sender per tier behind ports, and the run loop in flow HttpSender, PageSender and ReplaySender each live in their own file and implement Sender. The browser session is reached through the Host and CommandRunner ports, which actions.rs implements in one wiring struct, so the replay no longer lives in actions.rs. run_requests is the run loop; an in-memory Senders drives it in tests without a browser or a network. The navigation hint moves into flow. * refactor(flow): the recorder keeps its fields and offers begin, tag, end, steps and save actions.rs no longer reads or writes recorder internals. It enters and exits a command, begins and ends it with what the page said, and asks for the steps or a save. Range resolution, marking a save and the session's saved list live behind save. Step stabilizing moves into the recorder. * refactor(flow): one host policy, one failure constructor, no pass-throughs HostPolicy is the only place a URL is checked against a flow's hosts; the recording's first-party test, the run, the redirect policy, the replay and the page tier all use it. TierResult::fail with Failure::drift, unavailable and unknown_outcome replaces five result builders. replay_match, validate_name's wrapper, write_policy_action and flow_for_command are gone; a Write names its own policy action. * refactor(flow): drop the click-fired write guess and the --from keywords A GET is a suspected write only when its path has a whole logout, signout, unsubscribe, delete or remove segment; guessing from the command that fired it needed the output's dependencies and the request's resource type. --from takes a step number, and without it a save starts at the last navigation after the previous save. Help, MCP, the tool catalog, docs and the skill reference follow. * test(flow): table-driven cases, behavior names, and key tests through the save Hosts, check_response, GraphQL and secret-field cases are tables. The tests named after review comments carry the behavior they check. Key-buffer, paste and clipboard tests ask whether a request carrying the secret survives a save instead of reading the recorder's secrets. * fix(flow): a request is a write only when the writes list says so Request.write was a second copy of what Flow.writes records, and it defaulted to false, so a file could disagree with itself and a write could skip the ledger. The run reads the writes list alone. * refactor(flow): build the flow from its types and put write queries on Flow build_flow and the reference linker create Step, Request and Param directly instead of json! values that were parsed back into the same types. run_writes, replay_only_writes and AllowWrite::check replace four free functions that walked the writes list. * refactor(flow): three tier functions over one Browser port http::send, page::send and replay::send replace the Sender trait and its three structs. Host and CommandRunner become one Browser trait. The Senders seam and its in-memory adapter stay. * refactor(flow): Stop and RunOutcome name why a run ended, and a flow is read once Stop replaces the four string codes that were matched in send, memory and actions. RunOutcome builds the stopped, drift and failed messages with unit tests, so handle_flow_run no longer formats them. The run's host check is the per-request one after references are filled. A flow_run that cannot read its flow fails at the first read, before policy, so policy and the run never see two different files. * fix(flow): tag a request by when it started and wait on conditions in the e2e A request belongs to the command that was running when it started, using the request's own start time against each command's begin time, so a late event read during the next command keeps its step. The replay's wait no longer stalls for its full timeout: a request body that still has references to fill is matched on the request alone. The e2e wait for the server to see a click's request and for the browser to finish reading it, instead of sleeping. * fix(flow): check every request's host before the first one is sent A flow with a foreign host in a later request sent the earlier requests before rejecting it. The run now checks every request URL against the flow's hosts up front, on the template with references unfilled, and again per request once they are filled. * fix(flow): a flow that sends a write it does not list fails to load A request that changes state and is missing from the writes list would run with no gate and no once-only guarantee, so an edited file could drop it. Loading now refuses the file and asks to save it again. * fix(flow): the replay lets each command's requests finish before the next A command that depends on what the previous one fetched, such as a click that sends a token the page stored from the last response, raced the response and sent the wrong body. * refactor(flow): ship flow run over http only Flow run sends the saved requests over direct HTTP. The in-page fetch and command replay tiers, the cross-tier write ledger, recorded commands in the file and the tier preference are removed; a 401 or 403 is auth_expired. Every HTTP guard stays: host policy, write gate, single load, shape verification, redaction and owner-only files. Docs, help, skill reference and the MCP catalog describe the HTTP path only. * fix(flow): report a stripped credential instead of an expired session A request recorded with an authorization, token or CSRF header is marked strippedAuth at save. A 401 or 403 for it stops the run with credential_not_sendable, since signing in again cannot supply what the file does not keep. Other 401 and 403 responses stay auth_expired. The file no longer lists redactedSteps: it holds no steps to point to. * fix(flow): keep params out of GraphQL operations and expand placeholders once A param fills the variables and other fields of a GraphQL body, never the operation text, and an operation with a placeholder in its text counts as a write. After values are filled, a request that classifies as a write but is not in writes is refused before anything is sent, for params and for references. Params and references are expanded in one pass over the template, so a response or param value that contains placeholder text is inserted as data instead of being expanded again, which looped without end. * fix(flow): refuse a read whose filled path names a state change After params and references are filled, a GET whose path has a whole logout, signout, unsubscribe, delete or remove segment and is not listed in writes is refused before it is sent, the same as a request that becomes a write by method or GraphQL operation. * fix(flow): encode values per slot, gate every write, never resend a write on redirect Placeholders are parsed once into parts and bound in a single pass, with each value encoded for where it lands: a URL query or path segment, a form body, or a JSON string. Text that was inserted is never read as a placeholder again. At save, a typed value is found in the form its slot writes it, so quotes, backslashes, newlines, ampersands and plus signs in a JSON or form body become params instead of staying literal. A flow with no output sends every request, and every write among them goes through the write gate. An empty dependency no longer replaces a nonempty output with an empty result. A write never follows a redirect, including a 307 or 308; the 3xx is its answer. * fix(flow): read a form by its shape and refuse values that break a raw body A body shaped k=v&k=v is a form whatever its content type, so a param value with & or = stays inside its field. For a header, a plain text body or a multipart body, a value with a line break, or one that contains the multipart boundary, is refused before anything is sent. * fix(flow): keep path segments shared across requests literal when linking response values A value found in an earlier response replaced a path segment such as /api when the response held the same word (gitRootDirectory = "api"). A segment present in the path of more than one recorded request is site structure, so it is no longer linked to a response. * fix(flow): accept capture request ids for --output and list valid ids on error Saved ids are renumbered within the saved range, while the ids a capture shows keep their step sequence. --output now accepts either form, and an unknown id lists the ids that are valid. * fix(flow): drop per-request tracing and client-identity headers when saving x-client-ip, x-forwarded-for, x-real-ip, forwarded, traceparent, tracestate, x-request-id, request-id, x-amzn-trace-id, sentry-trace, baggage, cf-connecting-ip and true-client-ip identify the machine or one request. Replay sends only the saved headers and reads none of these, so they are classified as dropped next to cookie and host. * fix(flow): do not promote a page-query value that only the Referer carries A start-URL query value counts as sent when any recorded entry, saved or not, contains it, so `?create=true` became a param although only the Referer of the saved requests held it. After the requests are templated, a page-query param used by no URL, body or other header is dropped and the Referer keeps its recorded value. * fix(flow): decide structural values by source instead of how many paths share them A response value is left literal when it is already a path segment of the request it came from or of an earlier one. Counting requests also stopped ids that two later paths use, such as a project id, from becoming params or references. * style(flow): remove the empty line after a doc comment * fix(flow): refuse an --output id that names two different requests When a range starts after step 1, a capture id and a saved id can be the same string for different requests. The error names both meanings. The accepted id forms are documented. * fix(flow): template the restored Referer against the params that remain Restoring the raw Referer when a page-query param is dropped also removed the placeholders of params that are kept. The recorded Referer is now templated against the remaining params. * fix(flow): report drift and signed requests, name dropped secret requests, link ids to their source - A recorded list missing from the response is drifted; a present empty list stays ok. The main list path is saved and checked, including lists deeper than the shape signature. - A 401 or 403 on a request carrying a per-request signature or timestamp reports request_signed; static or word-like values keep auth_expired with a hint naming the header. - A request dropped for carrying a secret-field value is named by method and path with the secret redacted, or by host when the value cannot be located. - Response values link to the latest supplying response that does not echo the value from its own path, preferring lists, so a param chosen from a list carries through detail and nested requests. * fix(flow): keep the recorded list's items when only counts are missing A response that omits a recorded count but still has its list is ok and returns the items; it is empty only when the list itself is empty. * fix(flow): reword capture guidance for the relaunch behavior and validate raw launch capture The launch body's capture value is checked like the per-command setting, the not-capturing message and --capture help describe the relaunch, and the capture e2e asserts it. | 4 days ago | |
fix(inspect): reject WebSocket clients from other origins (#2182) The inspect proxy accepted any WebSocket handshake, so a web page could connect to it and run CDP commands in the inspected page. The Origin header, when present, must now match the Chrome host that serves the DevTools frontend, the same rule Chrome applies to its own debug port. Co-authored-by: muhtasham <20128202+Muhtasham@users.noreply.github.com> | 16 days ago | |
fix: keydown flood on macOS after press (#2112) - Stop sending the Windows key code as nativeVirtualKeyCode. On macOS Chrome reads it as a native key code, so press hit the wrong key and flooded the page with keydowns - Add an e2e test that checks one press produces exactly one keydown and keyup with the right code Fixes #2053 Co-authored-by: apolenkov <23296915+apolenkov@users.noreply.github.com> Co-authored-by: Railly Hugo <railly.hugoquispe@vercel.com> | 9 days ago | |
feat(flow): record a browser task once and replay its requests (#2236) * feat(flow): add saved flow recording, storage and HTTP replay Records the commands of a task with the first-party JSON requests each one fired, stores them without credentials, and replays the output request directly with a response shape check. * feat(flow): add flow start, save, run and list commands The daemon tags HAR entries with the running command and announces saved flows for a site after navigation. A failed run marks the flow unverified only when the site changed, not when params were rejected. * feat(flow): print flow results and the saved flows hint The hint shows recorded param values, the recorded page and item field names so an agent can call a flow without guessing its inputs. * feat(mcp): expose saved flows as core tools Adds flow list, run, start and save to the core profile, carries the saved flows hint in navigation results, and formats flow results as text. * docs(skills): document saved flows in the core skill Covers when to prefer flow run over driving the page, how to record a flow, and points derive-client at it for tasks the agent repeats. * fix(network): let mocked cross-origin responses pass CORS A route with --body answered cross-origin requests without CORS headers and with no content type, so the page saw a network error and HAR capture dropped the body. Mocked responses now echo the request origin, answer preflights, and label JSON bodies as application/json. * feat(flow): fall back to the page and to a command replay A run now tries direct HTTP, then the same request from the page, then a replay of the recorded commands, and starts the next run at the tier that worked. Every tier checks the recorded response shape; a change marks the flow unverified and is reported as drift. Session routes answer the HTTP tier too, so mocks reach every tier. A write the site accepted is never sent again by a lower tier, and a replay refuses params it cannot apply. * feat(flow): find params and references when saving A value a request sent that came from an item of an earlier list becomes a param with its source; a value copied from a single field is filled from that response on each run, and the requests it depends on run first. A login form on the page at save time names the last gap. The saved flows hint says where each detected param comes from. * feat(webmcp): list saved flows next to declared tools webmcp list now shows the saved flows for the page's site under an observed section, so declared and observed tools can be compared. * fix(flow): keep each GraphQL request and treat queries as reads Requests were deduplicated by method and URL, so a GraphQL app kept only its first POST. The body is now part of the key, and a POST that only carries GraphQL queries no longer needs --allow-write; mutations still do. * feat(flow): record a flow on an already open page flow start keeps the tab's URL, so a task that begins on a logged-in page can be saved without an open command, and a replay starts from that page. * fix(flow): replay recorded commands on live apps Snapshot refs are gone by the next run, so commands that used one are saved as the role and name it pointed at. Replayed commands wait for an element the page is still rendering, and a captured request matches a recorded one by its body or GraphQL operation, since one endpoint serves every query. * fix(flow): send writes the way a browser does The HTTP tier now sends Origin on writes, taken from the recorded referer, since sites check it before accepting one. A write the site did not accept may carry stale page tokens, so it moves on to the page and the replay tiers instead of stopping. * fix(flow): chain only generated values from read responses Short words and locale codes repeat across a site's responses, so they turned into long chains of references, some through POST requests. A reference now needs a value that looks generated, comes only from a read, and replaces whole tokens, never part of a longer word. * feat(flow): take params from the start URL and find nested lists A query value of the recorded page URL that the API requests also sent, such as ?keyword=, becomes a param without --param. --where, --fields and the hint's item fields now use the heaviest list anywhere in the response, since GraphQL nests it under data. * feat(flow): read JSON embedded in server-rendered pages Sites that render on the server send no API request for their main data, but often embed it in the HTML as a JSON script, a Next.js __NEXT_DATA__ block or a window.x = {...} assignment. flow save now keeps those blocks from first-party documents as requests that fetch the page and read the same block back, so they get params, shape checks and --fields like any API response. The page's own path segments and blocks of the same document are no longer taken as params or references. * fix(flow): treat a response without its lists as no results Many APIs drop an empty list and its counts instead of sending []. That used to read as a changed shape and mark the flow unverified. When only lists and counts are missing and no new list appeared, the run now returns no items and leaves the verified state as it was. * docs: document saved flows Adds a saved flows section to the network page and two lines to the README command list: recording, params, the run tiers, discovery and storage. * feat(flow): describe saved flows in the shape of WebMCP tools Saved flows now carry a description and an inputSchema built from their params, returned by flow list and shown by webmcp list after the tools the page declares. When a page declares WebMCP tools, they come before the saved flows hint, and the hint suggests them first. flow run still runs flows; webmcp invoke only calls the page's own tools. * fix(flow): close the gaps found in security review - Every resolved request URL, and every redirect the HTTP tier follows, must pass the session's current domain allowlist. - A command replay sends every write the recording caused, so it needs --allow-write whenever the flow recorded one, not only for writes the output depends on. - Param values are substituted into parsed JSON strings, so quotes and backslashes keep request bodies and replayed commands valid. - Suggested commands shell-quote recorded param values, and inferred param names keep only letters, digits and underscores. - Values typed into password-like fields are not stored, auth and credential commands are not recorded, and flow files and directories are created owner-only. - webmcp list prints each saved flow's inputs as name, type and recorded value. * fix(flow): keep typed secrets out of saved commands and requests type stores its input in text, which the redaction skipped. Both fields are now redacted, and the typed value is kept in memory during the recording so that flow save drops any captured request whose URL, body or headers carry it, and reports why. * feat(flow): capture flows with --capture flow and save several per session Recording moves from `flow start` to a launch flag. Every recorded command returns recorded.seq, failed commands stay in the recording as ok:false, and `flow steps` lists them. `flow save <name>` takes --from/--to/--description, no longer resets the recorder, and close reports unsaved steps. The hint hides only flows saved in the same session, and har stop is refused while capture owns the recorder. Flow files are version 2; older files must be saved again. * fix(flow): keep runs on the flow's own site A flow records the base domain of its start URL as allowedHosts. A run rejects any request, redirect or recorded navigation outside it, whatever --allowed-domains says, and the in-page tier only runs on a tab that is already on that site. Flow origins are validated as hostnames before they become directory names, and IP hosts no longer collapse to a shared base. * feat(flow): list writes at save and confirm each one before a run flow save records writes[{id, method, url, kind}] with kind explicit or suspected. A GET is suspected when a click fired it and no output depends on it, or when its path has a whole logout, signout, unsubscribe, delete or remove segment. --allow-write takes all or write ids, in the CLI and in flow_run. A write it does not cover asks for confirmation as policy action flow_write:<id>, so --action-policy and --confirm-actions reach the http and page tiers; the command replay checks the writes no request covers. * feat(flow): redact secret fields by what the page says and keep typed values as params Recording reads type, autocomplete and the field's name, id, label and placeholder from the element behind a fill or type, so an unlabeled one-time-code or cc-* input is redacted. Any other typed value becomes an auto-named param with source typed, stored as a placeholder in the step, or the --param name when one has the same value. Params record their source (typed, query, response) and the file lists redactedSteps. * feat(flow): stop a run on auth, rate limit and challenge responses A 401 or 403 returns auth_expired, a 429 returns rate_limited with retryAfter when the site sent one, and a bot challenge page returns blocked. Each stops the chain instead of falling to a lower tier, carries a machine-readable code and leaves the flow's verified state alone. * fix(flow): ask only about the writes a run sends A run now gates the writes among the requests its output needs, not every write the recording caused; the command replay checks the rest. Adds a Chrome e2e against a local fixture: sign-in cookie, CSRF write through a response value, GraphQL query, a GET side effect, a cross-domain redirect, and two saves in one session. commands.mdx stays as it was because the docs route fixtures freeze it. * fix(flow): close review findings on credentials, redirects, replay and typed values - A 401 or 403 on a request recorded with stripped credential headers falls through to the page and replay tiers; auth_expired is reported when every way is refused. - A blocked redirect returns its 3xx, a write with a 2xx or 3xx answer is accepted, a write with no answer returns outcome_unknown and is never resent, and the in-page write does not follow redirects. - The replay runs only plain interactions, checks every URL in a recorded command, and stops when the tab leaves the flow's site. - keyboard type and inserttext read the focused element; typed params keep no example. - base_domain uses the public suffix list; a flow whose origin directory does not match its start URL must be saved again. - A save of an earlier range no longer moves the save marker back, and challenge pages are recognized only in HTML. * fix(flow): keep vault and key-by-key passwords out of flows and close URL and retry gaps - Replay URL checks read every string the way the URL parser does, so tabs, newlines and leading control characters cannot hide a host, and URL keys are checked whatever the value looks like. - The in-page fetch sends a write once; a thrown fetch is outcome_unknown. - A command that is not recorded tags no requests to the previous step, and the password auth login fills is registered as a secret. - Keys pressed in a secret field are gathered into one secret, redacted from the steps, and requests carrying it are dropped at save. * fix(flow): keep modifier chords and clipboard text out of secret handling gaps A key joins a secret only when it is bare or Shift plus a key, so Control+a no longer corrupts the buffer. A paste chord or clipboard paste into a secret field redacts the step and drops the requests it tagged, and clipboard write text is redacted and registered as a secret. * fix(flow): register a pasted password once the paste has finished After a paste chord or clipboard paste into a secret field, the focused field's value is read and registered as a secret in full, with any typed prefix, and later keys extend it. A request a later step sends with it is no longer saved. When the value cannot be read, the requests tagged to the paste step are still dropped. * fix(flow): replay once per run, recognize encoded secrets and link writes to earlier responses - The command replay runs at most once per run; every request that falls back to it reads its answer from that replay, and it is skipped when another tier already sent a write it would repeat. - Requests carrying a secret are recognized in form-encoded, percent-encoded and JSON-escaped forms. - A write's request takes a value from an earlier response and depends on it, so a rotating token is read fresh at each run. * fix(flow): never resend a replayed write, keep rejected writes eligible, match replays by query - Before any tier sends a write, the run checks whether it was already sent. A write the replay sent is answered from the replay's capture, and stops with outcome_unknown when the capture has no answer. - Only a 2xx or 3xx answer, or an unknown outcome, counts as a sent write; a 4xx rejection leaves the replay available. - Replayed requests match by method, host, path, recorded query and body, ignoring query parameters the recording never had; a missing or ambiguous match fails for that request. JSON bodies compare by value. * fix(flow): read the flow once per run and carry it through confirmation A flow_run loads its file once. Policy, the write gate and the run use that copy, and a confirmation keeps it so the confirmed run executes the file that was checked, not one read again from disk. The write gate is one function used before the run and inside it. * refactor(flow): one secret-field predicate and one header classifier FieldTraits::looks_secret replaces four copies of the type, autocomplete and name check. HeaderClass names the four ways a recorded header is treated, in place of three separate lists. * refactor(flow): type the saved flow and move to a module directory Flow, Step, Request, Write, Param and Tier are serde types with unknown fields rejected, so the file schema is checked on load and a run no longer walks serde_json::Value. Request.write replaces the writes lookups in the run path. The file format is version 1; nothing older has shipped, so the old-version path is gone. Fixtures build flows through serde_json. * refactor(flow): one write ledger and next_tier decide when a request is sent again WriteLedger records which writes a run sent, and next_tier picks the next tier from the attempts so far. They replace the accepted, retry, completed and already-sent checks that were spread over the run loop, the replay and its guard. A write that was sent is only read back from the replay's capture. * refactor(flow): a Sender per tier behind ports, and the run loop in flow HttpSender, PageSender and ReplaySender each live in their own file and implement Sender. The browser session is reached through the Host and CommandRunner ports, which actions.rs implements in one wiring struct, so the replay no longer lives in actions.rs. run_requests is the run loop; an in-memory Senders drives it in tests without a browser or a network. The navigation hint moves into flow. * refactor(flow): the recorder keeps its fields and offers begin, tag, end, steps and save actions.rs no longer reads or writes recorder internals. It enters and exits a command, begins and ends it with what the page said, and asks for the steps or a save. Range resolution, marking a save and the session's saved list live behind save. Step stabilizing moves into the recorder. * refactor(flow): one host policy, one failure constructor, no pass-throughs HostPolicy is the only place a URL is checked against a flow's hosts; the recording's first-party test, the run, the redirect policy, the replay and the page tier all use it. TierResult::fail with Failure::drift, unavailable and unknown_outcome replaces five result builders. replay_match, validate_name's wrapper, write_policy_action and flow_for_command are gone; a Write names its own policy action. * refactor(flow): drop the click-fired write guess and the --from keywords A GET is a suspected write only when its path has a whole logout, signout, unsubscribe, delete or remove segment; guessing from the command that fired it needed the output's dependencies and the request's resource type. --from takes a step number, and without it a save starts at the last navigation after the previous save. Help, MCP, the tool catalog, docs and the skill reference follow. * test(flow): table-driven cases, behavior names, and key tests through the save Hosts, check_response, GraphQL and secret-field cases are tables. The tests named after review comments carry the behavior they check. Key-buffer, paste and clipboard tests ask whether a request carrying the secret survives a save instead of reading the recorder's secrets. * fix(flow): a request is a write only when the writes list says so Request.write was a second copy of what Flow.writes records, and it defaulted to false, so a file could disagree with itself and a write could skip the ledger. The run reads the writes list alone. * refactor(flow): build the flow from its types and put write queries on Flow build_flow and the reference linker create Step, Request and Param directly instead of json! values that were parsed back into the same types. run_writes, replay_only_writes and AllowWrite::check replace four free functions that walked the writes list. * refactor(flow): three tier functions over one Browser port http::send, page::send and replay::send replace the Sender trait and its three structs. Host and CommandRunner become one Browser trait. The Senders seam and its in-memory adapter stay. * refactor(flow): Stop and RunOutcome name why a run ended, and a flow is read once Stop replaces the four string codes that were matched in send, memory and actions. RunOutcome builds the stopped, drift and failed messages with unit tests, so handle_flow_run no longer formats them. The run's host check is the per-request one after references are filled. A flow_run that cannot read its flow fails at the first read, before policy, so policy and the run never see two different files. * fix(flow): tag a request by when it started and wait on conditions in the e2e A request belongs to the command that was running when it started, using the request's own start time against each command's begin time, so a late event read during the next command keeps its step. The replay's wait no longer stalls for its full timeout: a request body that still has references to fill is matched on the request alone. The e2e wait for the server to see a click's request and for the browser to finish reading it, instead of sleeping. * fix(flow): check every request's host before the first one is sent A flow with a foreign host in a later request sent the earlier requests before rejecting it. The run now checks every request URL against the flow's hosts up front, on the template with references unfilled, and again per request once they are filled. * fix(flow): a flow that sends a write it does not list fails to load A request that changes state and is missing from the writes list would run with no gate and no once-only guarantee, so an edited file could drop it. Loading now refuses the file and asks to save it again. * fix(flow): the replay lets each command's requests finish before the next A command that depends on what the previous one fetched, such as a click that sends a token the page stored from the last response, raced the response and sent the wrong body. * refactor(flow): ship flow run over http only Flow run sends the saved requests over direct HTTP. The in-page fetch and command replay tiers, the cross-tier write ledger, recorded commands in the file and the tier preference are removed; a 401 or 403 is auth_expired. Every HTTP guard stays: host policy, write gate, single load, shape verification, redaction and owner-only files. Docs, help, skill reference and the MCP catalog describe the HTTP path only. * fix(flow): report a stripped credential instead of an expired session A request recorded with an authorization, token or CSRF header is marked strippedAuth at save. A 401 or 403 for it stops the run with credential_not_sendable, since signing in again cannot supply what the file does not keep. Other 401 and 403 responses stay auth_expired. The file no longer lists redactedSteps: it holds no steps to point to. * fix(flow): keep params out of GraphQL operations and expand placeholders once A param fills the variables and other fields of a GraphQL body, never the operation text, and an operation with a placeholder in its text counts as a write. After values are filled, a request that classifies as a write but is not in writes is refused before anything is sent, for params and for references. Params and references are expanded in one pass over the template, so a response or param value that contains placeholder text is inserted as data instead of being expanded again, which looped without end. * fix(flow): refuse a read whose filled path names a state change After params and references are filled, a GET whose path has a whole logout, signout, unsubscribe, delete or remove segment and is not listed in writes is refused before it is sent, the same as a request that becomes a write by method or GraphQL operation. * fix(flow): encode values per slot, gate every write, never resend a write on redirect Placeholders are parsed once into parts and bound in a single pass, with each value encoded for where it lands: a URL query or path segment, a form body, or a JSON string. Text that was inserted is never read as a placeholder again. At save, a typed value is found in the form its slot writes it, so quotes, backslashes, newlines, ampersands and plus signs in a JSON or form body become params instead of staying literal. A flow with no output sends every request, and every write among them goes through the write gate. An empty dependency no longer replaces a nonempty output with an empty result. A write never follows a redirect, including a 307 or 308; the 3xx is its answer. * fix(flow): read a form by its shape and refuse values that break a raw body A body shaped k=v&k=v is a form whatever its content type, so a param value with & or = stays inside its field. For a header, a plain text body or a multipart body, a value with a line break, or one that contains the multipart boundary, is refused before anything is sent. * fix(flow): keep path segments shared across requests literal when linking response values A value found in an earlier response replaced a path segment such as /api when the response held the same word (gitRootDirectory = "api"). A segment present in the path of more than one recorded request is site structure, so it is no longer linked to a response. * fix(flow): accept capture request ids for --output and list valid ids on error Saved ids are renumbered within the saved range, while the ids a capture shows keep their step sequence. --output now accepts either form, and an unknown id lists the ids that are valid. * fix(flow): drop per-request tracing and client-identity headers when saving x-client-ip, x-forwarded-for, x-real-ip, forwarded, traceparent, tracestate, x-request-id, request-id, x-amzn-trace-id, sentry-trace, baggage, cf-connecting-ip and true-client-ip identify the machine or one request. Replay sends only the saved headers and reads none of these, so they are classified as dropped next to cookie and host. * fix(flow): do not promote a page-query value that only the Referer carries A start-URL query value counts as sent when any recorded entry, saved or not, contains it, so `?create=true` became a param although only the Referer of the saved requests held it. After the requests are templated, a page-query param used by no URL, body or other header is dropped and the Referer keeps its recorded value. * fix(flow): decide structural values by source instead of how many paths share them A response value is left literal when it is already a path segment of the request it came from or of an earlier one. Counting requests also stopped ids that two later paths use, such as a project id, from becoming params or references. * style(flow): remove the empty line after a doc comment * fix(flow): refuse an --output id that names two different requests When a range starts after step 1, a capture id and a saved id can be the same string for different requests. The error names both meanings. The accepted id forms are documented. * fix(flow): template the restored Referer against the params that remain Restoring the raw Referer when a page-query param is dropped also removed the placeholders of params that are kept. The recorded Referer is now templated against the remaining params. * fix(flow): report drift and signed requests, name dropped secret requests, link ids to their source - A recorded list missing from the response is drifted; a present empty list stays ok. The main list path is saved and checked, including lists deeper than the shape signature. - A 401 or 403 on a request carrying a per-request signature or timestamp reports request_signed; static or word-like values keep auth_expired with a hint naming the header. - A request dropped for carrying a secret-field value is named by method and path with the secret redacted, or by host when the value cannot be located. - Response values link to the latest supplying response that does not echo the value from its own path, preferring lists, so a param chosen from a list carries through detail and nested requests. * fix(flow): keep the recorded list's items when only counts are missing A response that omits a recorded count but still has its list is ok and returns the items; it is empty only when the list itself is empty. * fix(flow): reword capture guidance for the relaunch behavior and validate raw launch capture The launch body's capture value is checked like the per-command setting, the not-capturing message and --capture help describe the relaunch, and the capture e2e asserts it. | 4 days ago | |
fix: clear captured page errors when requested (#2253) * fix: clear captured page errors when requested Handle errors --clear in the shared daemon and empty the error buffer. Co-Authored-By: YIMURENIJIANG MAIMAITIMING <183707051+narmi924@users.noreply.github.com> * test: verify page error clearing and document MCP behavior * fix: label cleared page errors correctly * test(errors): clear captured errors without launching Chrome --------- Co-authored-by: YIMURENIJIANG MAIMAITIMING <183707051+narmi924@users.noreply.github.com> | 10 days ago | |
feat(session): persistent session tab binding with --pin-tab (#1589) * fix(tabs): stop event-discovered tabs from stealing the active tab Event-drained Target.targetCreated pages no longer move the active tab pointer. New pure helper active_page_index_after_add only activates when the page list was empty or activation is explicit, mirroring the existing active_page_index_after_removal pattern. Explicit paths (open, tab new) keep activating as before. Closes #1426 Co-authored-by: soichisumi <soichi.sumi@gmail.com> * feat(session): persistent session tab binding with --pin-tab strict mode Sessions sharing one Chrome over --cdp no longer hijack each other's tabs. The daemon persists a session-to-target binding ({session}.target, 0600, atomic write) and re-attach selects the bound target instead of index 0. With --pin-tab (sticky, AGENT_BROWSER_PIN_TAB), a destroyed bound tab fails loudly with a stable tab_gone error code instead of silently adopting a neighbor. CDP target ids are accepted as tab refs (tab <targetId>) and re-attached bound tabs get their CDP domains enabled regardless of attach order. Binding persistence is crash-safe and never stores query strings, fragments, or credentials. Builds on the event-drain no-steal fix from the previous commit: event-discovered targets register without activating, so they can never overwrite a session's binding. Closes #1530 Closes #1531 Closes #883 Co-authored-by: Vlad Ra <vlad19@gmail.com> * docs(skill): require a named session before any command The default session is one shared, conversation-persistent browser. The skill examples never showed --session, so parallel agents (and agents sharing a machine with a human) collided on it by default. The skill now opens with a mandatory named-session setup step. Closes #214 * fix(session): complete no-steal on attachedToTarget + batch pin/code propagation Address review findings on #1589: - #3: event-discovered tabs stole the active/pinned tab via the Target.attachedToTarget auto-attach path, which the original fix (Target.targetCreated drain only) never covered. Both handlers now share BrowserManager::register_discovered_page, so neither can activate a discovered tab. Explicit tab new / window new / click --new-tab register their page before the attach event and keep activating. - #4: batch mode dropped the tab_gone error code and never applied --pin-tab per command. run_batch now injects pinTab and preserves every response code, matching the single-command path. - Regression coverage now binds to the production decision function and goes red when the fix is reverted; the prior test exercised a duplicated helper and passed regardless. * fix(session): bind active tab on runtime pin-enable + keep tab_gone sticky Address review findings #1 and #2 on #1589: - #1: enabling --pin-tab at runtime only flipped the flag; the active tab was never bound, so the session was pinned-but-unbound until a restart re-derived the binding from disk. set_pin_tab now binds the active tab on enable, so enforcement is immediate. - #2: a pinned session whose page list drained to empty (bound tab closed externally -> tab_gone) would silently recover: add_page_with_activation's empty-pages branch force-activated the next discovered tab and bind_active_target cleared bound_target_gone, hijacking the session onto a tab it never opened. Discovered (non-activated) tabs are now refused the activation while a pin is in the tab_gone state; recovery stays explicit. - Regression coverage binds to the production functions and goes red when either fix is reverted. * docs(session): make pin-tab test comments agnostic (drop finding numbers) * fix(session): surface pin-persist errors, keep batch warnings, scope no-steal to pinned Address maintainer review on #1589: - Toggling --pin-tab/--no-pin-tab swallowed tab-binding load/save errors (`if let Ok(Some)` + `let _ = save`), reporting success while the change never reached disk and reverted after a daemon restart. Both now fail loudly, matching the attach-time persist path. - JSON batch output copied `code` but not `warning`, hiding a failed binding persistence in batch mode. run_batch now copies `warning` too, matching the single-command serialization. - The no-steal rule for event-discovered tabs was applied to every session, which regressed the legacy behavior a non-pin session relies on (an external popup becoming active) and broke a11y frame-session scoping (e2e_a11y_preserves_nested_frame_sessions_across_tab_switches). No-steal is now scoped to pinned sessions; legacy sessions follow the newest tab. Both the pin e2e suite and the a11y e2e now pass. * docs(session): tighten pin-tab comments * docs(session): correct drain-handler comments for pin-scoped activation * fix(session): persist pin toggle before committing live state A failed --pin-tab/--no-pin-tab toggle mutated state.pin_tab and the BrowserManager before persisting, so a load/save error returned an error while the daemon's live isolation mode had already flipped (torn state: command reports failure, session silently pinned/unpinned in memory). Persist first; commit the live state only after persistence succeeds. * style(session): use em dash in pin-toggle error strings to match sibling messages * fix(session): atomic attach rollback, gone-safe ensure_page, revive on restore Address maintainer round-3 findings on #1589: - Binding recovery failure left state.browser alive, so the next command skipped the attach path and acted on the wrong tab. All attach/connect sites now go through apply_tab_binding_on_attach_or_rollback, which tears the connection down on failure so recovery reruns. - ensure_page created a blank tab (binding it and clearing tab_gone) when a pinned session's sole tab closed. It now skips creation while the pin is in the tab_gone state; recovery stays explicit. - The pin restore path enabled CDP domains directly, hanging on Page.enable for a Memory-Saver-discarded tab. It now revives the tab first (revive_and_enable_active reuses ensure_renderer_alive), matching tab switch/close. Regression test covers the gone-safe ensure_page path. * fix(session): atomic attach rollback, gone-safe ensure_page, revive on restore Address maintainer round-3 findings on #1589, swept by class not instance: - Binding recovery failure left state.browser alive, so the next command skipped the attach path and acted on the wrong tab. All attach/connect sites go through apply_tab_binding_on_attach_or_rollback, which tears the connection down on failure. Class sweep of every fallible step after state.browser is set also found open_fresh_tab_for_auto_connect leaving a torn browser on failure; it now rolls back too. - ensure_page created a blank tab (binding it, clearing tab_gone) when a pinned session's sole tab closed. It now skips creation while the pin is in the tab_gone state; recovery stays explicit. - The pin restore path enabled CDP domains directly, hanging on Page.enable for a Memory-Saver-discarded tab. revive_and_enable_active reuses ensure_renderer_alive first, matching tab switch/close. Regression test covers the gone-safe ensure_page path. * test(session): cover the rollback and restore-revive seams, drop dead primitive Final pre-merge gate on #1589 found the two round-3 fixes that shipped without a regression test, and the primitive the third one routed around instead of removing. - apply_tab_binding_on_attach_or_rollback had no test. Added one driving a corrupt binding file, asserting the failed attach is rolled back rather than left committed. Force-red: drop rollback_failed_launch from the wrapper's error arm and it fails. - revive_and_enable_active on the pin restore path had no test either, and it is the seam that already regressed once. Added one over the mock CDP endpoint with a discarded bound tab; it times out if the revive is replaced by a bare enable_domains. - enable_domains_pub had no callers left once the restore path moved to revive_and_enable_active. It is the unguarded primitive whose misuse caused that regression, so leaving it exported just hands it to the next call site. Removed. * docs(session): tighten the new binding test comments Match the house length (three to five lines) and drop the review narrative: what the test pins and how to force it red, nothing about which round found the gap. * fix(session): harden pinned tab attachment * docs(session): clarify pin-tab re-testing * fix(session): structure tab-gone recovery data --------- Co-authored-by: soichisumi <soichi.sumi@gmail.com> Co-authored-by: Vlad Ra <vlad19@gmail.com> | 2 months ago | |
refactor: resolve session settings through one module (#2387) | 5 days ago | |
refactor: resolve session settings through one module (#2387) | 5 days ago | |
fix: synchronize recording cursor and mouse movement timing (#1869) * fix: correct cursor release and mouse movement timing * fix: render recording cursor with dragged page content | 1 month ago | |
fix(recording): suppress Windows ffmpeg console windows (#2382) * fix(recording): hide the Windows ffmpeg console Start the recording encoder with CREATE_NO_WINDOW so the detached daemon does not allocate a visible console. Co-Authored-By: Jeremy Mack <305901+mutewinter@users.noreply.github.com> * fix(recording): hide the ffmpeg preflight console Use the same Windows process flags for the availability probe before recording starts or restarts. --------- Co-authored-by: Jeremy Mack <305901+mutewinter@users.noreply.github.com> | 5 days ago | |
Fix Windows CI - Keep mcp-tools.json line endings as LF so the snapshot test passes on Windows - Use std::env::home_dir so Windows respects USERPROFILE when finding the home directory | 6 days ago | |
fix(snapshot): give empty table cells reusable refs (#2267) * fix(snapshot): assign refs to empty table cells Give table cells and headers refs regardless of their accessible name. Co-Authored-By: Hyunjin Lee <63354527+hyunjinee@users.noreply.github.com> * test(snapshot): resolve empty cell and header refs in Chrome --------- Co-authored-by: Hyunjin Lee <63354527+hyunjinee@users.noreply.github.com> | 9 days ago | |
Fix Windows CI - Keep mcp-tools.json line endings as LF so the snapshot test passes on Windows - Use std::env::home_dir so Windows respects USERPROFILE when finding the home directory | 6 days ago | |
native (#594) * Native Rust rewrite of agent-browser daemon Single-binary Rust implementation replacing the Node.js/Playwright daemon with direct CDP (Chrome DevTools Protocol) communication. Includes full command parity, WebDriver/Safari/iOS backend routing, request tracking, frame context management, CDP protocol codegen, and comprehensive tests. * improvements * fix ci * fixes * faster builds | 8 months ago | |
fix(daemon): delete `<session>.config` on daemon exit (#2128) * fix(daemon): delete <session>.config on daemon exit The CLI writes <session>.config once a daemon is ready, but shutdown never removed it, so one file per session name accumulated in the socket directory. Remove it during run_daemon shutdown, before the .pid file, and only while .pid still holds this daemon's pid so a replacement daemon for the same session keeps its config. Fixes #2098 Co-Authored-By: jomatsu <50577611+jomatsu@users.noreply.github.com> * refactor(cli): declare session sidecar files in one place Daemon shutdown and the CLI's stale cleanup each kept their own list of files to remove, and the lists had drifted (.config was missed on shutdown, .engine/.provider/.extensions on stale cleanup). - Add session_files.rs with a SessionFile enum. Each file declares its extension and whether it only describes a running daemon (.target and .log persist). - Both cleanup paths now call remove_daemon_files, which removes every daemon file. - Shutdown removes them only while .pid still holds our pid, so a replacement daemon keeps all of its files, not just .config. - Build every sidecar path through SessionFile instead of format!. Tested with cargo fmt, clippy -D warnings and cargo test on macOS. Not compiled for Windows locally. * fix(cli): route every sidecar deletion through the owned cleanup Review follow-ups on the shared sidecar list. - Drop the early .port (Windows) and .stream deletions in the socket server. They ran before the ownership check, and the final cleanup already removes both. - Dashboard kill used its own list and left .version, .config, .provider and .port behind. It now uses remove_daemon_files, guarded by the killed pid. - Add integration tests that run a real daemon and shut it down: one checks daemon files go and .target/.log stay, one checks a replacement daemon's files are left alone. Both fail if the cleanup block is removed or made unconditional. - Assert literal survivors in the session_files unit test instead of deriving them from is_daemon_file. Tested with cargo fmt, clippy -D warnings and cargo test on macOS. * fix(daemon): stop accepting connections before replying to close (#2138) The daemon replied to close and kept its listener open for another ~100ms before exiting. A command sent right after close returned could connect to the exiting daemon and fail with "Invalid response: EOF", so `close` followed by `open` lost the page. Close now goes through a small handshake: the connection that handled close asks the accept loop to stop, the loop drops the listener, and only then is the reply written. The next command gets a refused connection, cleans up the old session files, and starts a fresh daemon. The old daemon's exit cleanup leaves those new files alone because it only removes files while .pid still holds its own pid. Fixes #2134 Tested with cargo fmt, clippy -D warnings and cargo test on macOS, plus an integration test that fails without the fix. With real Chrome, `open A; close; open B` failed 20/20 before and 0/20 after. --------- Co-authored-by: jomatsu <50577611+jomatsu@users.noreply.github.com> | 13 days ago | |
Fix Windows CI - Keep mcp-tools.json line endings as LF so the snapshot test passes on Windows - Use std::env::home_dir so Windows respects USERPROFILE when finding the home directory | 6 days ago | |
Discover WebMCP tools with compact summaries on change (#1842) * Include current WebMCP tools in browser action responses * Keep WebMCP discovery compact, passive, and untrusted * fix: preserve WebMCP context across navigation and batches Keep registered tools and pending invocations across same-document navigation, and let document events invalidate page state. Render every MCP batch WebMCP summary through the shared content-boundary formatter. Add regression coverage and allow asynchronous tool events in the existing context test. | 1 month ago |
| Files | Last commit | Last update |
|---|---|---|
| 3 months ago | ||
| 3 days ago | ||
| 2 days ago | ||
| 4 months ago | ||
| 1 day ago | ||
| 9 days ago | ||
| 5 days ago | ||
| 2 days ago | ||
| 6 days ago | ||
| 2 days ago | ||
| 7 months ago | ||
| 2 days ago | ||
| 1 month ago | ||
| 14 days ago | ||
| 2 days ago | ||
| 4 days ago | ||
| 16 days ago | ||
| 9 days ago | ||
| 4 days ago | ||
| 10 days ago | ||
| 2 months ago | ||
| 5 days ago | ||
| 5 days ago | ||
| 1 month ago | ||
| 5 days ago | ||
| 6 days ago | ||
| 9 days ago | ||
| 6 days ago | ||
| 8 months ago | ||
| 13 days ago | ||
| 6 days ago | ||
| 1 month ago |