When connected to the user's real Chrome via the extension relay, sequential
commands could land on the wrong tab: `get url` returned x.com/home, then with
no navigation in between `eval` executed against x.com/notifications — so it
read the wrong page and returned nothing.
Root cause: the session's anti-drift anchor is `active_target_id` (pinned by
stable target_id), documented to be set "on every explicit open". But `open`
runs through `navigate()`, which never pinned. On the relay path `open` reuses
an existing tab via `navigate` rather than `add_page` (the only "explicit" path
that pins), so `active_target_id` stayed `None` and the session rode the fragile
`active_page_index`. A later passive tab close/reorder (drained before every
command) then drifted `eval`/`get url`/`snapshot` onto a foreign tab.
Fix:
- `navigate()` now syncs the index to the resolved active page and pins it by
target_id after a successful navigation — restoring the "pin on explicit open"
invariant for the relay path.
- `ensure_page()` pins its freshly-created tab too (matches `add_page`).
- Extract the pin-vs-index resolution into a pure `resolve_active_index()` and
cover the invariant with unit tests (pin beats stale index; falls back when
the pin is gone; survives passive background-tab discovery).
cargo fmt + clippy -D warnings clean; full suite 816 passed.
When a tab's chrome.debugger session was torn down and re-established
(cross-process navigation, MV3 service-worker restart wiping the in-memory
maps, DevTools stealing the debugger), the extension minted a brand-new
monotonic `cb-tab-N` for the same tab. The daemon stays bound to the old id and
the relay consumes attach/detach events without telling it to rebind, so the
session was orphaned permanently → `stale sessionId / tab is gone`, and re-open
never recovered.
Derive the session id from the STABLE Chrome tabId (`cb-tab-<tabId>`) instead.
Any re-attach of the same tab now restores the SAME session the daemon already
holds, so eval/snapshot transparently follow the new page after a navigation.
Extension 0.4.3 → 0.4.4. Adds a relay unit test for the detach→reattach-same-
session recovery contract.
Turn repetitive browser checks into unit-test-style YAML suites. Steps reuse
chrome-use's own commands; assertions (url/visible/hidden/text/count/eval)
compile to a single truthy `eval`. The runner re-invokes the binary per step
(inherits all flag/launch/daemon/ref semantics; the daemon stays up so each
step is a fast socket call), launches an isolated browser by default, captures a
screenshot on failure, and exits non-zero for CI. `setup: account:` injects a
cookie-use login. Ships a `test` skill (skills get test). Unit-tested step/assert
compilation.
#16: handle_screenshot now returns a canonicalized ABSOLUTE path, so the
`✓ Screenshot saved to …` line is the same regardless of process cwd and the
agent can read the file without guessing the cwd.
#15: `session list` now reflects the extension-relay connection — when the relay
is up it shows the active session as `(relay/extension → live Chrome)` instead
of "No active sessions", and the --json output gains a `relay` bool. Stops agents
misjudging a live relay connection as down.
DOM.setFileInputFiles is forbidden by Chrome's chrome.debugger API, so upload
always fails over the extension relay with an opaque -32000 "Not allowed". Map
it to a clear message: file upload needs a --launch/direct-CDP session, and
point at the cookies export|set --curl workaround. Note the limit in the core
skill upload line too.
The CWS rename to chrome-use updates the EXISTING item (knfcmbam…) with a
key-stripped package, not a new key-locked item — existing users auto-update and
reviews are kept. Native host allow_origins already lists both ids so the relay
doesn't break. Also point the icon/screenshots section at the generated assets.
Stage 1 of the agent-browser → chrome-use extension migration: the CLI now
writes a host manifest under both com.agent_browser.connect (extension ≤0.4.2)
AND com.leeguoo.chrome_use (the rebrand 0.5.0+), both pointing at the same
launcher, and host_installed()/uninstall recognize both. So the relay works no
matter which extension version a user has, with no forced re-install — which
lets the store roll 0.4.3 (cosmetic name only, host unchanged) and later 0.5.0
(new host) without ever breaking the relay or re-popping the consent dialog.
N parallel `open --profile <same>` (e.g. chatgpt-imagegen's web backend firing
3 image gens at once) collided on the profile-copy disk I/O and Chrome's profile
lock: every candidate burned its full ~30s launch timeout and ALL failed (0
success), because the loser instances hung without writing DevToolsActivePort.
ProfileLaunchLock takes a cross-process flock on a per-resolved-profile lock
file, held across the copy + launch until Chrome is up, so concurrent
same-profile launches queue instead of colliding — the storm becomes
all-succeed-serially instead of all-fail. The kernel releases the lock when the
holder exits, so a crash can't wedge the queue; acquisition is best-effort
(launch proceeds unlocked if it can't be taken). Uncontended single launches
are unaffected.
After the agent-browser → chrome-use rename, the new binary used ~/.chrome-use
+ host com.leeguoo.chrome_use and couldn't find the relay that the still-old
native-messaging host wrote to ~/.agent-browser → it fell back to raw
--remote-debugging-port and re-popped 'Allow remote debugging?'.
- config_home()/config_dir_basename(): decide once per run — prefer the new
.chrome-use, but keep using an existing .agent-browser install if that's the
only one present; fresh installs get .chrome-use. get_socket_dir() routes
through it so sockets/state are consistent within a run.
- relay_url_path(): the relay-cdp-url is a cross-binary handoff (host writes,
CLI reads), so read from whichever brand dir actually has the file
(~/.chrome-use OR ~/.agent-browser).
Combined with keeping HOST_NAME=com.agent_browser.connect (b6febbe), the renamed
chrome-use binary now relays through the existing ab-connect 0.4.2 extension
with zero dialog. Verified live: chrome-use found ~/.agent-browser/relay-cdp-url
and listed the user's real tabs, no consent dialog.
Reverting the host-name rename from the chrome-use rebrand. The host name is
invisible internal plumbing (lives only in NativeMessagingHosts/*.json and the
extension), so renaming it to com.leeguoo.chrome_use bought nothing user-facing
but broke the relay for every existing user: the new chrome-use binary couldn't
find a matching host/extension, silently fell back to raw --remote-debugging-port,
and re-popped the 'Allow remote debugging?' consent dialog.
Keeping com.agent_browser.connect means the renamed chrome-use binary keeps
working with the already-installed host json and the live ab-connect 0.4.2
extension — zero relay break, no dialog, and the store republish becomes an
OPTIONAL cosmetic display-name update (manifest bumped 0.5.0 → 0.4.3, name stays
chrome-use). Only the binary/command name changed for users.
Transferring a logged-in session between Chrome profiles previously needed
an ad-hoc external script to decrypt the source profile's cookie store. Make
it first-class:
- `cookies export --from <profile> [--domain <d>[,<d>]]` decrypts another
profile's on-disk cookies and prints CDP-shaped JSON for `cookies set --curl`.
- `cookies transfer --from <profile> [--domain <d>]` exports + injects into
the connected browser in one shot (reuses the cookies_set path).
Source profile is resolved by directory name, display name, or "auto". The
store is copied to a temp file (immune to a running Chrome's lock/WAL), read
via sqlite3, and values are decrypted (macOS v10: AES-128-CBC, key from the
shared 'Chrome Safe Storage' Keychain entry). httpOnly/secure/per-domain
auth cookies round-trip intact; SameSite=None without Secure is downgraded
so CDP accepts it. macOS only for now (clear error elsewhere).
Release binaries / Build macOS ARM64 (push) Has been cancelled
Release binaries / Build macOS x64 (push) Has been cancelled
Release binaries / Build Linux ARM64 (push) Has been cancelled
Release binaries / Build Linux musl ARM64 (push) Has been cancelled
Release binaries / Build Linux musl x64 (push) Has been cancelled
Release binaries / Build Linux x64 (push) Has been cancelled
Release binaries / Build Windows x64 (push) Has been cancelled
Release binaries / Attach binaries to GitHub Release (push) Has been cancelled
Standalone product rename across the whole repo (issue: project identity):
- Binary/package/repo/skill/docs: agent-browser[-stealth] → chrome-use
(single binary name `chrome-use`; old aliases agent-browser/abs dropped).
- Version: 0.27.0-fork.51 → 1.0.0 (drop the upstream-fork counter).
- Native-messaging host: com.agent_browser.connect → com.leeguoo.chrome_use
(CLI + ab-connect extension in lockstep — this is a breaking handshake change,
extension bumped 0.4.2 → 0.5.0, needs a Web Store republish).
- Config dir: ~/.agent-browser → ~/.chrome-use.
- README/zh: reframed from "stealth fork of agent-browser" to a standalone
product with a small `originally based on vercel-labs/agent-browser` credit.
- Kept AGENT_BROWSER_* env vars working (63 vars across the codebase; renaming
them would break every existing script/skill for no user-facing gain).
Build green, 802 unit tests pass, fmt + clippy clean. Upstream attribution to
vercel-labs/agent-browser preserved.
`open` waits for the `load` event by default. SPAs whose `load` never fires
(a long-pending XHR or a stuck sub-resource holds it open) made `open`
hard-fail after the lifecycle timeout — even though the DOM was ready and
eval/screenshot worked immediately right after.
- Graceful degradation: if the lifecycle event times out but document.readyState
is interactive/complete, navigate returns success carrying a `warning` in the
response (the CLI prints it to stderr; --json keeps the field) instead of
erroring. Only a still-loading document is a real failure.
- `open/goto/navigate` now accept `--wait-until <load|domcontentloaded|
networkidle|none>` so SPAs can return as soon as the DOM is parsed. The URL
parser skips the --wait-until value so it isn't mistaken for the URL.
- WaitUntil::as_str() for the warning label; output.rs surfaces response warnings.
Verified live: --wait-until domcontentloaded returns immediately on a page whose
load never fires; default load on the same page now succeeds at the timeout with
a clear stderr warning instead of failing. Adds parse tests for both arg orders
+ bogus value.
A bare --launch opens an isolated empty profile (no cookies/login/
extensions). A human watching the desktop sees a mystery Chrome window
under an unfamiliar profile and reads it as broken/suspicious.
- Seed the temp profile's Local State (profile.info_cache.Default.name,
the field Chrome's profile chip reads) + Default/Preferences with
'agent-browser (<session>)', so the window self-identifies which agent
session owns it.
- Rewrite the --launch warning to explain it's an isolated test profile and
point at the escape hatches: --profile auto / AGENT_BROWSER_PROFILE=auto
to reuse real Chrome, and --args "--load-extension=<dir>" for extensions.
- SKILL.md documents the same.
Adds a unit test for the profile-label writer.
SKILL.md + click --help + README now cover what agents could otherwise
only discover by trial:
- coordinate click (click <x> <y> / <x>,<y> / --coords) as a first-class form
- tabs / get-text aliases
- the 'stale sessionId … re-open your target URL' relay error and how to recover
- eval/screenshot/network '@ <url>' stamps as a per-read wrong-tab sanity check
- network requests --clear as the 'start capturing fresh' step
handleForwardCdpCommand fell through to anyConnectedTab() when a
daemon-supplied sessionId didn't map to an attached tab, so eval/screenshot/
network silently ran on an arbitrary tab — the root of "eval ran on the
wrong page, no warning" and the blank-screenshot-after-restart symptom.
Now: a provided sessionId/targetId MUST resolve to a real tab or the command
throws an actionable error ("stale sessionId … re-open your target URL").
anyConnectedTab() is only used for genuinely browser-level commands that
specify neither. Manifest 0.4.1 → 0.4.2 (needs a Chrome Web Store republish
for installed users to pick this up).
Field report #8: in extension-relay sessions, reads (eval/screenshot/network)
could silently run against whatever tab drifted into focus, with no signal,
and network capture was intermittently empty.
- #8.1: screenshot and `network requests` now print `screenshot @ <url>` /
`network @ <url>` to stderr (mirrors the existing `eval @ <url>`), and the
responses carry `origin`. A read against the wrong/drifted tab — and the
"0 captured" vs "wrong page" ambiguity — is now obvious.
- #8.3: `network requests --clear` now enables Network capture immediately
instead of lazily on the next read, so requests fired between `--clear` and
the following read are tracked (fixes the "No requests captured" on first
try, works on retry" race). Extracted enable_request_tracking helper.
- #8.2: the daemon version-mismatch restart notice now spells out that
in-memory context (active tab, refs, captured requests) is reset and tells
the user to re-open the target URL if the next read looks blank/wrong.
Verified on a launched browser: coordinate clicks land, screenshot/network
stamps appear, and a fetch after --clear is captured on the first read.
Field-report ergonomics fixes so agents stop wasting a round on a wrong guess:
- Coordinate click is now first-class: `click <x> <y>`, `click <x>,<y>`,
and `click --coords <x>,<y>` dispatch a raw viewport-point click (no
element resolution), reusing the humanize trajectory + press dwell. Was
previously only reachable via eval(elementFromPoint(...).click()).
- Aliases: `tabs` (plural) → the `tab` subcommand tree; `get-text`/`get_text`
→ `get text <selector>`.
- `find <value> <action>` with a bare value (no locator keyword), e.g.
`find "I'm not a robot" click`, now errors with the corrected command
(`find text "I'm not a robot" click`) plus concrete examples, instead of
a bare "Valid options: role, text, ..." list.
Adds parse-layer regression tests for every form.
remove_page_by_target_id left active_target_id dangling when the pinned
page itself was removed, so resolved_active_index silently fell back to
active_page_index — which after a passive about:blank discovery can point
at a blank tab. That matches issue #7's intermittent symptom: `wait` then
eval/snapshot landing on about:blank in a --launch session.
Re-pin to the surviving active page after removing the pinned target so
the pin is never left pointing at a target that no longer exists. Adds
pure regression tests for the re-anchor invariant (BrowserManager needs a
live CDP client, so the method can't be unit-constructed directly).
The srcdoc-iframe contentWindow Proxy returned native window methods
unbound, so iframe.contentWindow.getComputedStyle()/addEventListener()/
setTimeout() ran with the Proxy as `this` and threw "Illegal invocation"
on any page that uses a srcdoc iframe under --launch (FullLaunch). The
sibling matchMedia proxy already bound its methods; this one did not.
Wrap each function in an apply/construct trap that swaps the Proxy
receiver for the real window while passing .prototype/.name/.toString/
identity straight through (a plain .bind() drops .prototype and breaks
instanceof/constructors). Cached in a WeakMap for stable identity.
Verified before/after on a launched stealth browser: getComputedStyle,
addEventListener, setTimeout all OK; .prototype preserved.
From Hermes's fork.49 re-dogfood (9/11 fixes confirmed PASS):
- tab list: a page can set its title to a multi-KB string (= a giant URL); cap
the title column like the URL so the row stays readable.
- skill: clarify that true multi-agent isolation needs the extension-connect path
(per-session tab groups) — raw `--cdp` shares the browser, so a sibling
session's `open` can navigate your tab. Use the extension for concurrent agents.
- skill: prefer `eval --json` for array/object results (plain render is
multi-line / pipe-hostile); note type/fill don't fire keydown (use `keyboard
type` when key events are required).
(Hermes's "find-text click bypasses humanize" was a false alarm — verified both
paths curve; the apparent 1-vs-12 was cursor continuity on the same target.)
Within a session, commands are pinned to the agent's opened tab (fork.47). But
two agents on the same (default) session share one daemon + active tab and
clobber each other. Document that each concurrent agent must use a unique
--session — which gives it its own isolated tab group on the shared real Chrome.
Local stealth verification with no external detector: reports mode (connect vs
launch), live fingerprint probes (navigator.webdriver / window.chrome / plugins /
UA-headless) as pass/fail, and an audit of the active overrides for the path
(incl. the iframe-proxy state from #4). `--json` for a stable shape agents can
gate a sensitive flow on. Distinct from `doctor` (install/env health).
--launch mode scored ~20% stealth on CreepJS because the srcdoc-iframe
contentWindow Proxy trips `hasIframeProxy` — the proxy that hides automation is
itself a fingerprintable tell (violates this fork's own "native > JS lies" rule).
Add a config-driven opt-out (no detectable global): AGENT_BROWSER_DISABLE_IFRAME_PROXY=1
drops the patch via __abStealth.disableIframeProxy → the iframe IIFE early-returns
→ clean 0% CreepJS, trading the niche srcdoc-iframe masking. Default keeps current
behavior. README now documents the --launch 20% honestly and scopes the headline
0% to the extension-connect path. Verified: launch + srcdoc page intact with the
toggle; stealth tests green (config strip-prefix kept in sync).
- snapshot -c (compact) now always keeps lines with an interactive ARIA role
(button/link/textbox/combobox/option/…), not only `ref=`/`": "` lines — so a
clickable control can't vanish from compact output and leave the agent clicking
an empty ref (issue #2 P1). Additive: only ever keeps more. compact tests green.
- stale-ref error now leads with "take a fresh snapshot" and points to the `eval`
fallback for ref-churning SPAs, and demotes AGENT_BROWSER_VERIFY_REF=0 to a
flagged last resort instead of presenting it as the fix (issue #3 P1).
The session's active tab was a bare index into `pages`, which drifts when a
foreign/user/other-session tab is passively discovered, a tab closes, or the list
reorders — so `eval`/`screenshot`/`snapshot`/`click` could land on the wrong page.
With login state that's a safety bug (a fetch firing on the wrong origin), and it
made screenshot disagree with snapshot/eval.
Pin the intended tab by stable target_id (`active_target_id`), set on every
explicit open / tab new / tab switch / connect. `active_session_id` and
`active_target_id` resolve through it (falling back to the index only if the
pinned tab is gone), so all commands stick to the agent's tab regardless of
passive churn — and they all agree.
Verified (--cdp, multi-tab): a window.open foreign tab no longer drifts eval;
tab new / switch re-pin correctly.
Dense pages produced hundreds of legend lines on stdout (Hermes: HN dumped 320).
Print the first 40 with a "… and N more" summary; every marker is still drawn in
the image, and --json still returns the full list.
The biggest manual-cost point in the dogfood reports: `select @ref` is a silent
no-op on non-native dropdowns, and click+wait+Enter on react-select/ARIA/portal
menus took ~20 turns of hand-written eval to get right.
New `pick <selector> --option "<text>"` does it atomically in one in-page async
routine: native <select> → set value + input/change; custom widget → focus +
open (pointer/mouse sequence), poll up to 2.5s for the option to render anywhere
(portals included), match by visible text, scroll it in, fire the full
pointer/mouse sequence. ERRORS loudly if the option never appears — no silent
success.
Verified headless: native <select> → "Gamma"; portal combobox → "欧洲"
(non-ASCII); missing option → explicit error. Documented in the skill.
- eval now prints `eval @ <url>` to stderr (stdout stays the raw value) so an
agent can catch tab drift — e.g. a logged-in fetch that hit the wrong origin —
before trusting the result. Mitigates the issue #2/#3 P0 safety concern. (eval
already returned the origin; the default output just never surfaced it.)
- `type --focused <text>`: type into the currently-focused element with no
selector, for custom widgets that move focus to a hidden input (issue #2 P3).
- AGENT_BROWSER_HUMANIZE set to an unrecognized value now warns once (like the
--humanize flag) instead of being silently ignored (Hermes #3).
A coordinate click resolved from a CSS selector (incl. the getByText/find path's
located node) skipped the occlusion check that @ref clicks already get, so an
overlay on top made the click land on the overlay while still reporting ✓ Done —
the worst failure mode for an agent (Hermes #1, issue #2/#3). Now: if the click
point doesn't hit the target (elementFromPoint isn't the element / a descendant /
an ancestor wrapper), dispatch through the DOM instead, which fires the real
handler. Best-effort probe (a flaky check never blocks the normal path); skipped
for strict CLICK_MODE=coord and non-left/multi-clicks.
Verified: occluded button click hits 0→1 (was silent ✓Done); normal click
unaffected.
Root cause behind Hermes #1 (CLICK_MODE=dom "does nothing") and #2 (--humanize
"does nothing"): both are env vars the daemon reads, but the daemon's env is
frozen at spawn — set them on a command to an already-running daemon and they
were silently ignored. (Confirmed: setting CLICK_MODE=dom at daemon spawn made
dom_click fire; setting it later did not.)
Fix: the client forwards AGENT_BROWSER_CLICK_MODE / AGENT_BROWSER_HUMANIZE in the
command envelope (_clickMode/_humanize); execute_command applies them per command
— mirrors CLICK_MODE into the process env (interaction::click reads it fresh) and
sets the humanize session level. Each command is authoritative.
Verified on an already-running daemon: CLICK_MODE=dom now fires dom_click
(hits 0→1); --humanize human typing applies.
- `eval --file <path>`: read JS from a file, sent verbatim — avoids shell-mangling
of non-ASCII identifiers/strings (Chinese), quotes, and large scripts (issue #3).
- `tab list`: truncate multi-KB URLs (JWT/OTP login links) middle-out with a char
count so the list stays readable (issue #3).
- skill: fix the snapshot example to match real output
(`- role "name" [ref=eN]`, not `@e1 [role]`); document that eval runs in the
page MAIN world with persistent state (top-level `const` collides — use IIFE /
window / unique names) and to prefer --file/--stdin/-b for non-ASCII or big JS.
When connected to the user's real Chrome, mgr.close() disconnected but never
closed the tabs the session opened — so every session (especially one that
failed before calling close, or a forgotten one) left its tabs piling up in the
user's browser. Idle-timeout and shutdown have the same exit path.
Track the target_ids this session creates via Target.createTarget in
`created_targets` (only ever our own tabs — never the user's existing tabs, which
the raw-CDP path attaches to, nor other sessions'). On close(), for the connected
path (not a launched browser, which Browser.close handles wholesale), close each
of those targets — the extension maps Target.closeTarget → chrome.tabs.remove.
Verified against a throwaway --cdp Chrome: open + 2 `tab new` → 3 pages; `close`
→ back to 1 (our 2 closed, the pre-existing tab untouched).