Browser automation CLI for AI agents
| Files | Last commit | Last update |
|---|---|---|
chore(docs): add Technical Writer agent skill (#2239) Adds the technical-writer skill from vercel/chat to .agents/skills, with a .claude symlink to .agents so Claude Code finds it. AGENTS.md now asks agents to use the skill for prose in this repo. The dash rule in AGENTS.md now says to rephrase instead of using an em dash, which matches the skill. Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> | 10 days ago | |
feat: dashboard provider support and session creation improvements (#1092) Add provider icons and session creation from the dashboard UI. Sessions can now be created with cloud providers (Browserbase, Browserless, Browser Use, Kernel) in addition to local engines. CLI changes: - Track provider via .provider files alongside .engine files - Add WaitUntil::None variant to skip lifecycle event waits for providers - Auto-set waitUntil=none when --provider is used with navigate - Fix Browser Use: use direct WSS connection (wss://connect.browser-use.com) - Add connect_cdp_direct for providers with page-level CDP proxies - Fix resolve_cdp_url to convert https:// provider URLs to wss:// - Treat empty CDP session_id as None (omit from protocol messages) - Fix Browserbase: send explicit JSON body + Content-Type header - Increase CDP connect timeout to 25s for remote providers - Clean up .provider files on session close Dashboard changes: - Show provider or engine icon per session in sidebar - New session dialog with unified engine/provider selector grid - Async session creation with loading state and error display - Kill zombie daemons on provider connection failure - Parse CLI JSON error output for user-friendly messages - Default new session URL to https://agent-browser.dev | 7 months ago | |
Type-check the Eve package with tsc-ts - Run the Eve package's typecheck with tsc-ts, which reports the same diagnostics as tsc - Keep the official tsc as typecheck:tsc and run it in CI while tsc-ts is new | 3 days ago | |
full native (#754) * full native * fix: apply cargo fmt formatting * fix: prevent zip path traversal in Chromium installer Use enclosed_name() to sanitize zip entry paths, preventing malicious archives from writing outside the extraction directory. * improvements * fix: apply cargo fmt formatting * benchmarks * bench * updates * fixes | 7 months ago | |
docs: simplify the README and repo docs (#2302) * docs: move README-only content to the docs site and fix the source build steps The source build used a removed pnpm build script and a ./bin/agent-browser path that does not exist. Timeout and page_wedged behavior and the Lambda example now live on the debugging and installation pages. The proxy CA config test reads the configuration page instead of the README. * docs: simplify the README and repo docs The README repeated most of agent-browser.dev and had drifted from it. It now covers install, the quick start loop, how it works, agent setup, and a command groups table that links to the docs. The benchmarks README states that phase 1 now installs a Rust release. | 5 days ago | |
Fix Windows ARM64 launcher binary selection (#1725) * fix: use x64 launcher binary on Windows ARM64 Co-authored-by: nexxusbruno-ship-it <271842339+nexxusbruno-ship-it@users.noreply.github.com> * fix: prefer native Windows ARM64 launcher binary Co-authored-by: nexxusbruno-ship-it <271842339+nexxusbruno-ship-it@users.noreply.github.com> --------- Co-authored-by: nexxusbruno-ship-it <271842339+nexxusbruno-ship-it@users.noreply.github.com> | 2 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 | |
Pin Linux release glibc floor (#1417) * Pin Linux release glibc floor - Build GNU Linux release artifacts against glibc 2.28 with zigbuild. - Add a release guard for newer GLIBC symbols. - Align Docker release helpers with the pinned build target. Co-authored-by: Hesham Khaled <54694393+heshamkhaledd@users.noreply.github.com> * Fix Docker build failure handling * Fix Docker build target handling * Fix Docker build target compatibility * Fix Docker single-target build mapping --------- Co-authored-by: Hesham Khaled <54694393+heshamkhaledd@users.noreply.github.com> | 5 months 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. | 1 day ago | |
feat(skills): load one reference with skills get --ref (#2310) * feat(skills): load one reference with skills get --ref skills get returned SKILL.md alone or every reference with --full. --ref <ref> returns only the named reference or template, matched by path, file name, or stem, and the MCP skills_get tool takes the same values in refs. The core guide's routing table now loads references this way instead of reading the skill directory. * test(evals): check that agents load one reference with skills get --ref reference-routing gives the model the core guide as already loaded and asks for tasks that need one reference. It passes when the model loads that reference with --ref, and fails on --full or reading the skill directory. A control case checks that a task core covers loads no reference. | 5 days ago | |
docs: simplify the README and repo docs (#2302) * docs: move README-only content to the docs site and fix the source build steps The source build used a removed pnpm build script and a ./bin/agent-browser path that does not exist. Timeout and page_wedged behavior and the Lambda example now live on the debugging and installation pages. The proxy CA config test reads the configuration page instead of the README. * docs: simplify the README and repo docs The README repeated most of agent-browser.dev and had drifted from it. It now covers install, the quick start loop, how it works, agent setup, and a command groups table that links to the docs. The benchmarks README states that phase 1 now installs a Rust release. | 5 days ago | |
docs(flow): mark saved flows experimental in the skill and MCP tools (#2444) The CLI help and the network docs already call saved flows experimental, but the agent-facing surfaces did not. A run can fail on a page that changed or answers differently each time, so the core skill, the commands reference and the four flow MCP tools now say so and point back to the UI. | 1 day ago | |
docs: migrate to Geistdocs (#1887) * feat(docs): migrate agent-browser to Geistdocs * fix(docs): use workspace pnpm configuration on Vercel * fix(docs): run Vercel installs and builds through Corepack * docs: document Vercel workspace build requirements * fix(docs): scope Vercel dependency installation to docs * feat(docs): switch navbar to Vercel Labs brand Bump @vercel/geistdocs to 2.4.1 and set navbarBrand: "labs" so the docs navbar uses the Vercel Labs logo and links to vercel.com/labs instead of the default OSS branding. * chore: regenerate lockfile after merge * fix(docs): restore navigation sections * test(docs): re-pin baseline to d01253d9 after main advanced Main updated the contact sheet example image (#1871) and the browser-use provider page (#1879); the rename-aware merge carried both into the migrated content. Re-pin the original-state baseline to the new main commit so the contract checks pin the current originals. * docs: update Geistdocs to 2.7.5 and check the MCP route * test(docs): re-pin baseline to 526157cf after main advanced * docs: port Obscura engine page to Geistdocs and re-pin baseline to efee3893 | 1 month ago | |
Remove AWS Windows testing setup (#1864) - Remove the EC2 and SSM scripts for remote Windows testing. - Remove the associated agent instructions and instance ignore rule. | 1 month ago | |
Align the screenshot guidance in the core skill (#2461) | 17 hours ago | |
docs(skills): slim the core skill and route detail to references (#2303) * docs(skills): slim the core skill and route detail to references The core guide loads on every use. It now keeps the loop, waits, common tasks, troubleshooting, and safety, and ends with a routing table to each reference and specialized skill. Detail it carried moves to commands.md and trust-boundaries.md. A unit test fails when a reference or skill is missing from the table. * fix(cli): point agents at skills get core in top-level help The help told agents to start with skills get core --full, which loads every reference, while the discovery stub and core guide start with skills get core. * docs(skills): start from skills get core on the skills page The page recommended skills get core --full, which loads every reference, and did not list webmcp-gen. * docs(agents): update the README only when install, quick start, or command groups change | 5 days ago | |
Fix Windows ARM64 launcher binary selection (#1725) * fix: use x64 launcher binary on Windows ARM64 Co-authored-by: nexxusbruno-ship-it <271842339+nexxusbruno-ship-it@users.noreply.github.com> * fix: prefer native Windows ARM64 launcher binary Co-authored-by: nexxusbruno-ship-it <271842339+nexxusbruno-ship-it@users.noreply.github.com> --------- Co-authored-by: nexxusbruno-ship-it <271842339+nexxusbruno-ship-it@users.noreply.github.com> | 2 months ago | |
chore(docs): add Technical Writer agent skill (#2239) Adds the technical-writer skill from vercel/chat to .agents/skills, with a .claude symlink to .agents so Claude Code finds it. AGENTS.md now asks agents to use the skill for prose in this repo. The dash rule in AGENTS.md now says to rephrase instead of using an em dash, which matches the skill. Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com> | 10 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 | 5 days ago | |
fix(daemon): decide session reuse and relaunch in one place (#2396) * feat(cli): add session settings option table and patch One table lists every session setting with its flag, effect class (launch or live), persistence class, secrecy and the rule that reads it from the resolved flags. Patch::from_flags builds the set of values an invocation set explicitly, with a wire encoding. Characterization tests check the patch carries every value DaemonOptions and the launch envelope helpers send. Nothing calls the module yet; parse_proxy moves into it unchanged. * refactor(daemon): read session settings through typed views DaemonState holds a Running value seeded once from AGENT_BROWSER_* at startup. The state initializers, auto launch, launch fallbacks, init script and enable lists, CDP headers, the extensions file and the policy and confirm-actions loaders read it through views instead of the environment. Behavior is unchanged: the seed applies each variable's existing parsing rules. The idle timeout resolution moves into the module so one definition serves the daemon and the view. * refactor(daemon): read idle timeout, provider and plugin launch defaults from settings The idle timeout, the provider named in policy checks and the headed, engine, user agent and color scheme defaults handed to plugin providers come from Running instead of the environment. Plugin providers keep their looser headed rule, where any value except 0, false, no or empty counts, behind a dedicated view. * refactor(cli): limit dead code allowances to the patch and option table The running views and the env seed are used by the daemon, so the compiler checks them again. Only the patch, the option table and the settings error keep an allowance until the CLI sends the patch. * test(daemon): pin separator-only allowlist and policy seeding A separator-only allowed domains value seeds an empty list and the resulting filter allows every host. The daemon state test now loads a real action policy file and the plugin provider headed rule has its own tests. * fix(cli): redact secrets from session settings debug output Patch and Running print through a manual Debug that hides secret rows and strips credentials embedded in the proxy URL. * feat(cli): plan a settings patch against the running settings Running::plan compares a patch with the running values and returns the changes, whether the browser has to relaunch, and the settings that follow. A key the patch does not name is left alone, an equal value is free, and setting one connection target displaces the others. Add --relaunch to force a relaunch with no change. * feat(daemon): apply the settings patch carried by each command The daemon plans the patch on every command. Timeouts, policy, confirm actions and auto-dialog apply in place, and launch settings close the browser so the next launch uses them. handle_launch_inner becomes plan plus launch from the session settings, with no per-field env fallbacks. launch_hash is replaced by a launch identity reported in lifecycle only. Every change is reported in lifecycle.settings.changes with secrets redacted, and relaunchedBrowser is set whenever the browser was relaunched. The idle timeout is read on each pass of the shutdown loop so it can change while the daemon runs. * feat(cli): send the settings patch on every command The CLI attaches Patch::from_flags to each command it sends and prints one stderr line per change the daemon reports. The separate launch envelopes, should_send_local_launch_config and attach_obscura_proxy_bypass are gone, and bare open no longer sends a second launch. ensure_daemon restarts the daemon only for --debug. Timeouts, policy, confirm actions and auto-dialog apply in place, so the ignored-timeout warning is removed. * docs: describe settings changes on a running session Cover the per-command patch, the one-line notice for each change, and --relaunch. * fix(cli): keep CDP headers and URL secrets with their endpoint CDP headers are dropped when the CDP endpoint changes unless the same patch sets them. CDP URLs lose userinfo, query and fragment wherever a value is reported. Setting a storage state replaces an inherited profile or extensions, and the reverse, so an inherited setting cannot wedge the launch. A null on the wire no longer clears a security setting. * fix(daemon): commit launch settings only after the launch succeeds A failed launch puts the previous launch settings back, and the failed response still reports what changed. Commands that never launch a browser apply live settings only and leave launch settings pending, so they no longer close the browser or claim a relaunch. * fix(cli): send no launch settings with get cdp-url, print one relaunch line get cdp-url, alone or in a batch, carries live settings only. A settings notice replaces the generic relaunch note. Add relaunch to the MCP common arguments and refresh the tool catalog. * docs: say which settings changes are reported and when * fix(cli): ignore local-only settings for external sessions, send no-xvfb off A session attached over CDP, auto-connect or a provider ignores settings that only describe a browser agent-browser launches itself, so an ambient value cannot relaunch it or start a new cloud session. The ignored requests are reported with effect ignored. AGENT_BROWSER_NO_XVFB set to an off value is now sent. * fix(daemon): apply launch settings at the launch, after the policy gates Live settings apply before the gates. Launch settings are planned there and applied only where the launch happens, so a denied or unconfirmed command leaves the browser running and changes nothing. A failed launch keeps the allowed domains it installed, except when the launch was rejected because they cannot be enforced for it. Settings-driven relaunches invalidate the ref map, an iOS relaunch keeps its device, and the new daemon tests hold the env lock. * fix(cli): send --relaunch once for a batch * docs: describe ignored, pending and kept settings and the env seed * fix(daemon): gate a launch by the provider its settings name A launch that names no connection target connects as the session settings say, so its policy actions now follow them: a settings provider is gated as a browser provider, an external target no longer adds the launch-mutator actions, and the plugin does not run when policy denies or asks to confirm. * fix(daemon): requested CDP headers replace the cached ones Changing or clearing --cdp-headers on a session at the same endpoint updated the settings but reconnected with the headers cached for that endpoint. The cache now follows the plan, and a failed launch puts it back. * fix(daemon): fail the launch when a named --state file cannot be loaded A launch that only inherits the state file still continues without it, as before; one whose command named the file fails, closes the browser and puts the previous launch settings back. * fix(daemon): keep the stored default timeout in step with a live change A command that omits --default-timeout sizes its wait from the daemon's stored config, which kept the value the daemon started with. * docs: list --relaunch after the existing global flags * fix(daemon): a provider named by settings decides the launch policy Policy for a launch now prefers the target the envelope names, then the external target the settings name, then the browser the session is attached to, so switching an attached session to a provider is gated by that provider's plugin and not the old attachment. * fix(daemon): a bare open that carries --state loads it strictly handle_launch_inner looked only at the launch envelope for the state file, so a bare open, which carries it in its settings, launched without state when the file could not be loaded. * fix(daemon): an external session carries no local launch settings Switching to CDP, auto-connect or a provider drops the inherited state file, profile, extensions, proxy and its credentials, executable path and args, and the external launch paths read no state file and install no proxy credentials. A launch that changes where the session connects no longer takes its policy from the old attachment, and an empty provider names no browser, so resetting to a local launch gates the launch mutators. * refactor(daemon): use the launch's storage state binding only once * docs: say inherited local settings are dropped for external sessions * chore: drop a test-generated state file * chore: ignore the parity test state file * fix(daemon): one rule for what names a browser, and a failed switch keeps local settings A blank or zero connection target names no browser in the settings views, the attached target, the launch envelope and the launch policy, so a direct launch with an empty cdpUrl is gated as the local launch it runs. Values dropped because a switch to an external browser failed are put back with the rest of the launch settings. An inherited engine no longer labels a CDP or auto-connect browser. * fix(cli): keep --relaunch for the first batch command that can launch A browserless batch item such as `read` consumed `--relaunch`, so the next command reused the browser. The CLI now asks the daemon's own predicate, `command_launches_browser`, before marking the relaunch as sent. * test(webdriver): change a setting Safari sessions track in the replacement test Headed is local-only, so a Safari session ignores it and the second launch reused the session instead of replacing it. * fix(cli): carry --relaunch until the daemon reports it applied A batch item denied by policy or parked for confirmation never reaches the launch, yet it marked the relaunch as sent, so the next command reused the browser. The batch now reads `lifecycle.settings.forcedRelaunch` from each response instead of predicting it. * fix(cli): count a relaunch reported by an early error or a confirm replay The WebDriver-unsupported and dialog-blocked errors now carry the settings outcome of the launch that ran before them, and the batch reads the replayed command's report nested in a `confirm` response. Without both, the next batch item relaunched a second time. * fix(daemon): confirmation replay runs under the current security settings A command parked under one action policy was replayed with its saved settings, so `confirm` could put back the policy, confirm actions or allowed domains the operator had since tightened. The replay drops security settings, and any change to them drops the pending confirmation. * fix(daemon): re-arm the idle timer from the timeout in effect now The shutdown loop re-armed the timer with the timeout read at the top of the pass, so a live change waited out the previous value, and enabling a timeout that started disabled never armed it. Every re-arm now reads the current timeout and the last activity. * docs: list relaunch as a shared MCP argument and note it applies once per batch * test: cover the CA trust, attached target and CDP headers a failed launch puts back * fix(daemon): keep a parked command's allowed domains in its confirmation replay Allowed domains apply only at the launch, so stripping them from the replay launched a confirmed command without the containment it was parked with. The replay now drops only the action policy and confirm actions; a change to the session's allowed domains already drops the pending confirmation. * docs: every MCP tool accepts relaunch; open and tab_new list it * test(docs): record the relaunch edit to the MCP shared arguments | 3 days ago | |
chore: enforce pnpm minimum release age (#1377) * chore: enforce pnpm minimum release age * fix: use package manager pnpm version in actions * chore: require node 24 | 5 months ago | |
scripts | 9 months ago | |
docs(skills): slim the core skill and route detail to references (#2303) * docs(skills): slim the core skill and route detail to references The core guide loads on every use. It now keeps the loop, waits, common tasks, troubleshooting, and safety, and ends with a routing table to each reference and specialized skill. Detail it carried moves to commands.md and trust-boundaries.md. A unit test fails when a reference or skill is missing from the table. * fix(cli): point agents at skills get core in top-level help The help told agents to start with skills get core --full, which loads every reference, while the discovery stub and core guide start with skills get core. * docs(skills): start from skills get core on the skills page The page recommended skills get core --full, which loads every reference, and did not list webmcp-gen. * docs(agents): update the README only when install, quick start, or command groups change | 5 days ago | |
chore: prepare v0.41.2 release (#2395) | 4 days ago | |
fixes | 9 months 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. | 3 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. | 3 days ago | |
fix(package): publish a Node 22 floor and drop engines.pnpm (#2404) The published manifest carried the repository's development constraints (Node 24, pnpm 11), so installs on Node 22 LTS warned with EBADENGINE and failed under engine-strict. The wrapper and postinstall only use stable core modules, and pnpm is not needed to run the package. Repository development stays on Node 24 and pnpm 11 via .node-version and packageManager. Fixes #1548 Co-authored-by: narmi924 <183707051+narmi924@users.noreply.github.com> | 4 days ago | |
Upgrade to TypeScript 7 - Move docs, dashboard and sandbox to TypeScript 7 (dashboard on Next 16.3) - Replace ESLint with oxlint in docs - Use esbuild and @babel/parser instead of the TypeScript API in docs tests and scripts | 1 day ago | |
Type-check the Eve package with tsc-ts - Run the Eve package's typecheck with tsc-ts, which reports the same diagnostics as tsc - Keep the official tsc as typecheck:tsc and run it in CI while tsc-ts is new | 3 days ago |
agent-browser
Browser automation for AI agents: open pages, read them as an accessibility tree, and act on elements from the shell.
agent-browser is a native Rust CLI that gives an AI agent a browser it can drive one command at a time. snapshot returns the page as compact text with a ref such as @e1 for each element. The agent acts on a ref, reads the page again to check the result, and repeats. The browser stays open between commands, so each step is one short shell call.
Install
npm install -g agent-browser
agent-browser install # download Chrome for Testing (first time only)
- Homebrew:
brew install agent-browser, thenagent-browser install. - Linux:
agent-browser install --with-depsalso installs the browser's system libraries. - No install:
npx agent-browser open example.com.
Project installs, building from source, upgrade, and doctor are covered in Installation.
Quick start
agent-browser open example.com
agent-browser snapshot -i # interactive elements, each with a ref
agent-browser click @e1 # act on a ref from that snapshot
agent-browser get url # check the result
agent-browser close
The snapshot for example.com:
- link "Learn more" [ref=e1]
- Refs describe the page at the time of the snapshot. After navigation or a DOM change, take a new snapshot before using refs again.
- CSS selectors, text, XPath, and semantic locators also work, for example
agent-browser find role button click --name "Submit". See Selectors. - A click fails, instead of reporting success, when the target is hidden, disabled, outside the viewport, or covered by another element such as a consent banner. Dismiss the covering element, take a new snapshot, and retry.
The full walkthrough is in the quick start.
How it works
The CLI sends each command to a background daemon that starts on the first command and controls the browser over the Chrome DevTools Protocol. Each --session gets its own daemon and browser. By default, the daemon closes a headless browser and exits after one hour without commands, saving state first when session persistence is configured.
Chrome for Testing is the default browser. You can also attach to a running Chrome with --cdp or --auto-connect (CDP mode), select the Lightpanda or Obscura engine with --engine, drive Mobile Safari in the iOS Simulator, or use a cloud browser provider such as Browserbase, Browserless, Browser Use, Kernel, or AgentCore.
For AI agents
The CLI ships skills that match its version:
agent-browser skills get core # the core loop, refs, waiting, and common workflows
agent-browser skills get core --ref commands # one reference, such as the full command list
agent-browser skills get core --full # adds the full command reference and templates
agent-browser skills list # specialized skills: electron, slack, dogfood, and more
Install the discovery skill into an agent harness from skills.sh:
npx skills add vercel-labs/agent-browser
The installed skill is a thin stub that tells the agent to load agent-browser skills get core, so its instructions always match the installed CLI. Or add this to your AGENTS.md:
Use `agent-browser` for browser automation. Before the first use, run
`agent-browser skills get core` and follow it.
For MCP clients, agent-browser mcp starts an MCP server over stdio. See MCP server.
Commands
Run agent-browser --help for the full list, or agent-browser <command> --help for one command.
| Group | Commands | Docs |
|---|---|---|
| Navigate and read | open, back, forward, reload, read, snapshot, get, is |
Core, Snapshots |
| Interact | click, dblclick, fill, type, press, keyboard, hover, focus, select, check, uncheck, scroll, drag, upload, mouse, find |
Core, Find elements |
| Wait | wait |
Wait |
| Capture | screenshot, pdf, record, download, diff |
Recording, Diffing, Files |
| Page and network state | cookies, storage, network, set, clipboard, eval, pushstate |
Network, Settings |
| Tabs, frames, and dialogs | tab, frame, dialog |
Tabs and frames |
| Debug and performance | console, errors, highlight, inspect, trace, profiler, vitals, react, a11y |
Debugging, Profiler, React |
| Sessions and auth | session, state, auth, profiles, confirm, deny |
Sessions, Security |
| Agents and integrations | skills, mcp, webmcp, flow, chat, batch, plugin |
Skills, WebMCP, Plugins |
| Observe | stream, dashboard |
Streaming, Dashboard |
| Setup | install, upgrade, doctor, connect, close |
Installation |
Flags, environment variables, and agent-browser.json are documented in Configuration.
Platforms
Native binaries ship for macOS (ARM64, x64), Linux (ARM64, x64, glibc and musl), and Windows (x64). The npm package picks the right binary for the host.
Development
Building from source requires Node.js 24 or later, pnpm 11 or later, and Rust:
pnpm install
pnpm build:source # builds the dashboard and the native CLI
pnpm link --global
agent-browser install
Run the Rust checks from cli/:
cargo fmt -- --check
cargo clippy
cargo test
cargo test e2e -- --ignored --test-threads=1 # launches real headless Chrome
AGENTS.md lists the files to update with each change, the docs checks, and the release process.
Documentation
- agent-browser.dev: introduction, guides, and reference
- Commands and Configuration
- Security: the auth vault, domain allowlists, action policies, and content boundaries
- Next.js and Vercel and eve: running agent-browser in Vercel Sandbox and eve agents
- Changelog
License
Apache-2.0