| Files | Last commit | Last update |
|---|---|---|
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 | |
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 | 4 days ago | |
fix(session): keep a connection target only after an attach succeeds (#2418) * fix(session): stop seeding connection keys from the daemon env, add --no-auto-connect A daemon spawned with AGENT_BROWSER_AUTO_CONNECT, CDP, CDP headers or provider counted that target as already committed, so a failed attach never rolled back and every later plain command retried it (#1621). The CLI resends these keys on every command, so the daemon no longer seeds them. --no-auto-connect (and --auto-connect false) resets the session to a local launch, overriding env and config, and is rejected with --auto-connect. * fix(session): report a displaced reset once and scope the auto-connect docs A reset of a key that a target set in the same patch already displaced was recorded twice (`--no-auto-connect --cdp`). The plan now reads the pending value. Docs say `--no-auto-connect` resets an `--auto-connect` session, and that the connection target is no longer seeded from the daemon env. * fix(session): keep env CDP headers sent with a provider or auto-connect target Headers sent with the same command as a provider or auto-connect target were displaced by it, so every later command saw them as new and relaunched, starting a new cloud session for providers. The plan now keeps headers the command sends. `AGENT_BROWSER_AUTO_CONNECT` is on only for a truthy value, as the CLI already read it; docs say so. | 4 days ago | |
fix: decode percent-encoded proxy credentials (#2389) * fix: decode proxy URL credentials before authentication Decode the username and password after splitting proxy URL userinfo so authentication receives the intended characters. Co-Authored-By: Alexandr Kazmin <316548136+alkaz-nodemaven@users.noreply.github.com> * test: cover encoded proxy credentials and document URL handling * docs: clarify proxy credential decoding fallback * fix: sync proxy credential guidance in MCP catalog --------- Co-authored-by: Alexandr Kazmin <316548136+alkaz-nodemaven@users.noreply.github.com> | 5 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(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 | |
fix(session): keep a connection target only after an attach succeeds (#2418) * fix(session): stop seeding connection keys from the daemon env, add --no-auto-connect A daemon spawned with AGENT_BROWSER_AUTO_CONNECT, CDP, CDP headers or provider counted that target as already committed, so a failed attach never rolled back and every later plain command retried it (#1621). The CLI resends these keys on every command, so the daemon no longer seeds them. --no-auto-connect (and --auto-connect false) resets the session to a local launch, overriding env and config, and is rejected with --auto-connect. * fix(session): report a displaced reset once and scope the auto-connect docs A reset of a key that a target set in the same patch already displaced was recorded twice (`--no-auto-connect --cdp`). The plan now reads the pending value. Docs say `--no-auto-connect` resets an `--auto-connect` session, and that the connection target is no longer seeded from the daemon env. * fix(session): keep env CDP headers sent with a provider or auto-connect target Headers sent with the same command as a provider or auto-connect target were displaced by it, so every later command saw them as new and relaunched, starting a new cloud session for providers. The plan now keeps headers the command sends. `AGENT_BROWSER_AUTO_CONNECT` is on only for a truthy value, as the CLI already read it; docs say so. | 4 days 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 | |
fix(session): keep a connection target only after an attach succeeds (#2418) * fix(session): stop seeding connection keys from the daemon env, add --no-auto-connect A daemon spawned with AGENT_BROWSER_AUTO_CONNECT, CDP, CDP headers or provider counted that target as already committed, so a failed attach never rolled back and every later plain command retried it (#1621). The CLI resends these keys on every command, so the daemon no longer seeds them. --no-auto-connect (and --auto-connect false) resets the session to a local launch, overriding env and config, and is rejected with --auto-connect. * fix(session): report a displaced reset once and scope the auto-connect docs A reset of a key that a target set in the same patch already displaced was recorded twice (`--no-auto-connect --cdp`). The plan now reads the pending value. Docs say `--no-auto-connect` resets an `--auto-connect` session, and that the connection target is no longer seeded from the daemon env. * fix(session): keep env CDP headers sent with a provider or auto-connect target Headers sent with the same command as a provider or auto-connect target were displaced by it, so every later command saw them as new and relaunched, starting a new cloud session for providers. The plan now keeps headers the command sends. `AGENT_BROWSER_AUTO_CONNECT` is on only for a truthy value, as the CLI already read it; docs say so. | 4 days ago |