Commit Graph
93 Commits
Author SHA1 Message Date
leeguooooo ad4fb14ed9 feat: stealth status self-check command (issue #5)
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).
2026-06-11 23:33:24 +09:00
leeguooooo fc73ee6c90 feat(pick): atomic combobox select for react-select / ARIA / native (issue #2 P1)
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.
2026-06-11 22:55:32 +09:00
leeguooooo fa47a0b8e5 feat: eval prints its origin URL, type --focused, AGENT_BROWSER_HUMANIZE bogus warn
- 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).
2026-06-11 22:48:31 +09:00
leeguooooo 6ecda4d706 fix: per-invocation env (CLICK_MODE / HUMANIZE) reaches a running daemon
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.
2026-06-11 22:29:39 +09:00
leeguooooo 5a61a64559 feat(stealth): silent operation — never steal the user's foreground tab
Driving the user's real Chrome should not yank their view around. Now the agent
operates entirely in the background:
- New tabs are created with `background: true` (CreateTargetParams) so opening
  one never foregrounds it (the ab-connect extension already used active:false;
  this covers the raw-CDP path too).
- Dropped the two AUTO `Page.bringToFront` calls (auto-connect fresh tab, and the
  internal active-page switch). The explicit `bringToFront` command is untouched —
  surfacing a tab stays opt-in.
- enable_domains now sets `Emulation.setFocusEmulationEnabled(true)` so a
  backgrounded agent tab still renders (screenshots work), isn't render-throttled,
  and reports document.hasFocus()/visibilityState='visible' — which also removes
  the "tab is hidden the whole session" bot tell.

Verified headless: hasFocus=true/visible while backgrounded; click + screenshot
still work. Default behaviour, no flag.
2026-06-11 20:11:22 +09:00
leeguooooo 9bd6587278 feat(stealth): in-bbox landing jitter + eased wheel/drag (humanize v4)
Completes the humanize suite:
- Clicks land on a jittered point inside the element's box (Fast/Human) instead
  of its exact centre. `resolve_element_center` now also returns the element
  width/height (box_model_dims); the CSS-selector path reports zero size → land
  on centre (no jitter, no regression). Jitter is clamped to the inner box so the
  click never misses.
- Wheel scrolls split into eased, jittered segments (humanize::scroll_segments,
  unit-tested) instead of one instant jump.
- Drag follows the curved trajectory at Fast/Human (linear 10-step at Off).

Off is unchanged throughout. 9/9 unit tests; verified headless — jittered click
still lands (→ iana.org), segmented scroll moves the page.
2026-06-11 19:52:33 +09:00
leeguooooo bab58991fe feat(stealth): adaptive anti-bot detection drives humanize level (v2)
After each navigation, probe the loaded page for known behavioural anti-bot
vendor fingerprints — cookies (_abck/Akamai, _px/PerimeterX, datadome,
reese84/Imperva, …), script URLs, and window globals — and escalate this
session to HumanizeLevel::Human when one is present, else fall back to the Off
baseline. So ordinary sites run at full speed (instant clicks) and only pages
actually guarded by behavioural detection pay for human-like motion.

`AGENT_BROWSER_HUMANIZE` still forces a fixed level and short-circuits the probe.
Best-effort: a failed probe leaves the level unchanged. Verified end-to-end
(headless --launch): a HUMANIZE=human click on example.com traverses the curved
trajectory and lands correctly (→ iana.org), identical outcome to Off.
2026-06-11 19:29:49 +09:00
leeguooooo cf4c27d13d fix: resolve Hermes-found CLI bugs (wait --url, find role, invalid selector, polish)
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
- wait --url: the arg parser never read `--timeout`, so a non-matching pattern
  waited the large default and wedged the daemon. Parse it. Also: matching was a
  literal substring (`includes`) so globs never matched — convert `**`/`*`/`?`
  globs to an anchored regex. And `poll_until_true` now bounds each probe with a
  timeout and tolerates transient navigation errors, so a hung `Runtime.evaluate`
  can never block past the deadline (un-wedges the daemon).
- find role <role> [--name]: the query was `[role="X"], X`, which matches a
  literal <X> tag / explicit attribute but NOT implicit-role elements — so
  `find role link` (<a href>) and `find role heading` (<h1>) never matched. Add a
  proper ARIA-role → implicit-element map and broaden accessible-name matching
  (aria-label/title/alt/value/text).
- click on a syntactically-invalid selector returned `✓ Done`: querySelector
  throws, and Runtime.evaluate returned the thrown DOMException as an objectId
  that was clicked as if it were the element. Check exception_details → error.
- output: a title-less page now prints `✓ <url>` instead of an empty title line.
- docs(skill): tab refs are `t2`, not `2` (SKILL.md, electron).

Verified live (isolated launch): wait --url glob matches instantly; non-matching
honors --timeout (2s) and leaves the daemon responsive; find role link/heading
match; invalid selector errors. Unit tests added for the glob + role map + parse.
2026-06-10 14:48:49 +09:00
leeguooooo 1a4c440d9e ci: fix long-broken CI (version-sync, dead dashboard job, fmt, clippy, flaky test)
The fork's CI had never been green. Pre-existing failures:
- version-sync: check-version-sync.js read packages/dashboard/package.json,
  which doesn't exist in this fork (workspace is just "."). Drop the dashboard
  comparison; check package.json vs cli/Cargo.toml only.
- Dashboard job: `pnpm install --filter dashboard` for a non-existent package.
  Remove the job.
- Format check: repo was never `cargo fmt`-clean. Ran cargo fmt (mechanical).
- Clippy -D warnings (newly enforced on Rust 1.94 stable): manual_contains in
  commands.rs (.iter().any()->.contains()), question_mark in element.rs
  (if-let-Err -> ?), result_large_err on the tungstenite handshake callback in
  connect.rs (allow — the Result type is fixed by the accept_hdr_async contract).
- rust-cross: lightpanda::waits_for_ready_without_logs spawns a real process +
  binds a socket with timing assumptions; flaky in CI. Marked #[ignore].

Also: skill docs note fork.30's relay-preferred auto-connect (plain
`agent-browser open` is dialog-free once the ab-connect extension is loaded) and
the extension's new "agent-browser-stealth" display name.
2026-06-10 11:49:11 +09:00
leeguooooo 06c75af46a fix(connect): passively-discovered tabs no longer steal the active tab
After connect+grouping worked, follow-up eval/get-title/screenshot drifted to a
foreign tab: on a shared browser, Target.targetCreated events for tabs the user
or OTHER agent sessions open stream in and are drained on every command. The
drain path routed them through add_page(), which sets active_page_index to the
new page — so the session's active tab silently jumped to a foreign tab and its
commands landed there.

Add BrowserManager::add_background_page() (push without touching active, dedup by
target_id) and use it in the event-drain path. Explicit opens (tab new, the
add-and-switch paths) keep using add_page() and still focus the new tab.

Closes the last gap in concurrent multi-agent: each session now drives its OWN
tab regardless of other sessions'/the user's tab activity.
2026-06-10 00:42:16 +09:00
leeguooooo 6b99d304b1 feat(stealth): shrink detectable surface — lazy Runtime.enable, native timezone/WebRTC, opt-in canvas noise
Borrow anti-detection hardening from Scrapling/patchright, preferring native
CDP/Chrome overrides over JS lies:

- Runtime.enable is now opt-in via AGENT_BROWSER_CAPTURE_CONSOLE (default off).
  It was called on every session INCLUDING CdpAttach (the user's real Chrome),
  leaking the patchright/rebrowser "runtime" CDP signal and undermining the
  "real browser, no lies" guarantee. Runtime.evaluate/callFunctionOn and
  runIfWaitingForDebugger work without it; only console/error capture needs it.
  The console/errors commands now return a hint when capture is disabled.
- Timezone alignment via native Emulation.setTimezoneOverride, opt-in with
  AGENT_BROWSER_TIMEZONE=<IANA>|auto (FullLaunch only). Intl and Date both
  follow with no JS artifact.
- WebRTC IP-leak handling via the --force-webrtc-ip-handling-policy Chrome
  flag: auto disable_non_proxied_udp when a proxy is set (so the real IP can't
  leak past the proxy); AGENT_BROWSER_BLOCK_WEBRTC=1 hides the local IP when
  there is no proxy; =0 opts out.
- Opt-in canvas/audio fingerprint noise via AGENT_BROWSER_HIDE_CANVAS=1
  (FullLaunch only). Session-stable seed so reads stay consistent within a
  session while differing from the headless-stable hash.

Adds 5 unit tests; full suite 751 passed, 0 failed.
2026-06-04 13:28:34 +09:00
leeguooooo ed61be3359 feat(profile): --profile auto + stop steering users into temp-profile launches
Addresses the footgun raised in issue #1 follow-up: plain `--launch` silently
uses a temporary EMPTY profile (no cookies/login), and the connect-failure
error even recommended it — trapping agents into thinking they reused the
logged-in browser when they didn't.

- `--profile auto`: resolves to the Chrome profile last used (from Local State
  `profile.last_used`), falling back to "Default", then the first profile. So
  `--launch --profile auto open <url>` reuses real login state without naming
  the profile. (--profile <name>/Default already worked.)
- connect-failure error now recommends `--launch --profile auto` and states
  plainly that bare `--launch` is a temporary EMPTY profile — no cookies/login.
- bare `--launch` (no --profile, not CI) now prints a warning to that effect.
- README: fix Setup (relaunch with --remote-debugging-port, not chrome://inspect)
  and split Standalone mode into throwaway vs. keep-your-login (`--profile auto`).

Tests: resolve_chrome_profile("auto") prefers last_used, falls back to Default.
2026-06-01 18:30:11 +09:00
leeguooooo 21d591ee65 fix(stealth): apply stealth on the --launch path (FullLaunch JS patches + UA strip)
handle_launch's fresh-launch path (the path `--launch open <url>` takes) never
called apply_stealth_to_browser — only the launch FLAGS were applied (e.g.
--disable-blink-features=AutomationControlled, which is why navigator.webdriver
was already false). As a result the 32 JS fingerprint patches and the
Emulation.setUserAgentOverride HeadlessChrome→Chrome UA strip NEVER ran on a
launched browser: navigator.userAgent kept the HeadlessChrome marker (a
longstanding bug — identical on the prior prebuilt binary).

Add the apply_stealth_to_browser call after launch (the auto_launch path
already had it; only the explicit-launch path was missing it).

Verified, FullLaunch headless:
- navigator.webdriver === false, navigator.userAgent => Chrome/<v> (no Headless)
- new tabs and the initial page both clean
- bot.sannysoft.com: 0 failed / 31 passed
2026-06-01 14:39:03 +09:00
leeguooooo ad0fb424c3 fix(ux): silent-output, command aliases, and clearer connection errors
- output: a success response with no data payload now prints "Done" instead of
  nothing (a silent exit 0 looked like a no-op).
- commands: add top-level aliases for `get` status reads — `url`, `cdp-url`
  (and `cdp_url`), `title`, `html`, `text`, `value`, `count`, `box`, `styles`,
  `attr` — so `agent-browser url` no longer errors "Unknown command".
- connect errors now explain the Chrome 136+ realities:
  - connect-failure mentions the "Allow remote debugging?" consent dialog and
    that remote debugging is a startup flag, not a setting.
  - no-Chrome error tells the user to relaunch Chrome with
    --remote-debugging-port (auto-connect then works).
  - --cdp discovery failure explains Chrome 136+ dropped the HTTP discovery
    endpoints and to use the default auto-connect instead.
2026-06-01 12:49:35 +09:00
leeguooooo f62e204038 fix(stealth,hygiene): per-session stealth coverage + orphaned temp-profile sweep
Stealth coverage (the fork's core value was leaking on secondary surfaces):
- stealth scripts are registered per CDP session, so new tabs (`tab new`) and
  cross-origin iframe sessions created after the initial page had NO patches.
  Extract apply_stealth_via_mgr/apply_stealth_to_session and re-apply on
  tab_new and on iframe attach. Fixes automation markers (and FullLaunch UA)
  leaking in new tabs / cross-origin frames.

Resource hygiene (temp profiles filled the disk):
- ChromeProcess::drop already cleans the temp user-data-dir on normal exit, but
  a hard kill (kill -9 / version-mismatch restart / crash) skips Drop and leaks
  ~50MB per session. Add cleanup_orphaned_chrome_profiles() on daemon startup
  that sweeps agent-browser-chrome-* temp dirs NOT referenced by any live
  process (so an in-use profile is never deleted).
2026-06-01 12:38:57 +09:00
leeguooooo d027659571 feat(screenshot): hide scrollbars in headless screenshots (cherry-pick b4f2f37)
Cherry-picks upstream agent-browser #1396. Adds a configurable
--hide-scrollbars flag (AGENT_BROWSER_HIDE_SCROLLBARS env, hideScrollbars
config key, default true) that appends Chrome's --hide-scrollbars launch arg
for headless (non-extension) launches so native scrollbars aren't painted into
screenshots. Plumbed through flags.rs, connection.rs, main.rs, native/actions.rs
and native/cdp/chrome.rs; help text in output.rs + skill-data.

Fork adaptation:
- the arg lands in the headless && !has_extensions block, separate from the
  stealth base args — no interaction with anti-detection.
- dropped upstream docs/, agent-browser.schema.json and README hunks (removed
  or rewritten in this fork).

Verified: cargo check --tests passes.
2026-06-01 10:35:20 +09:00
leeguooooo 1b3d41e579 fix(timeout): cap defensive CDP guards so click can't hang multi-minute
Reported: a single `click @ref` could hang 5+ minutes, with multiple
queued click invocations adding up to 7+ minutes — worst case 30s
timeout × 3 CDP calls × N parallel processes:

  - verify_ref_identity (Accessibility.getPartialAXTree)  →  default 30s
  - resolveNode / getBoxModel                              →  default 30s
  - wait_for_paint_settled (Runtime.evaluate awaitPromise) →  default 30s

The latter two are best-effort defenses added in fork.3-5 to fix SPA
race / DOM-reuse bugs. They should never block a real click for
30s — the unguarded code path was always faster than the guarded
path-that-hangs.

  - verify_ref_identity   capped at 1s   (skips check on timeout)
  - wait_for_paint_settled capped at 500ms (skips wait on timeout)

Both skip-on-timeout intentionally: the worst case is the click
behaves like fork.2 (race-prone but fast), which is strictly better
than the user pkilling stuck processes.

Also rewrites the misleading "Chrome 144+ chrome://inspect tip" in
the auto-connect failure message — the toggle exposes target
discovery only, not the /json/version HTTP API the auto-connect
flow expects (verified by user: lsof shows :9222 listening but
curl /json/version returns 404).
2026-05-09 10:04:14 +09:00
leeguooooo d3bfd76c96 fix(connect): liveness probe + wait @ref support
Two changes that pair with each other:

1. connect_auto_with_fresh_tab now does a Runtime.evaluate "1"
   round-trip after creating the fresh tab. This catches the zombie
   CDP socket case (process alive, websocket dead) where every step
   up to that point reports success but the next user command would
   silently no-op against a dead session. Failing here lets the
   caller surface a proper "CDP session unresponsive" error instead
   of returning Ok and letting `agent-browser open URL` exit 0 with
   a still-blank tab.

2. handle_wait now recognizes @ref selectors (e.g. `wait @e8 --gone`).
   It polls resolve_element_object_id, which already runs the
   verify_ref_identity check from 007fd1b — so:
     - `wait @e8`             succeeds while the original element is
                              still mounted with its snapshot role+name
     - `wait @e8 --gone`      succeeds when the ref's identity changes
                              (modal closed, button re-textified, etc.)
   This gives users the "assert modal still open" primitive that
   prior versions could only approximate with screenshots.
2026-05-09 04:10:48 +09:00
leeguooooo 2b1a3c308a feat(daemon): preserve URL across version-mismatch restart
Before: after `npm i -g` upgrade, the next agent-browser command would
detect daemon version mismatch, kill the old daemon, spawn a fresh one,
and connect to a brand-new about:blank tab. The user's previous
navigation state was silently lost — `get url` returned about:blank
even though the user's Chrome was still on the same page.

Now: before killing the old daemon, the CLI synchronously asks it for
its current URL via the existing socket. If non-empty and not
about:blank, it's persisted to a `.restore-url` sidecar in the socket
dir. After the new daemon spawns and auto-connects, it reads the
sidecar (read-and-delete), navigates the fresh tab to the saved URL,
and prints `⚠ Restored previous URL: <url>`.

Manual `agent-browser close` does NOT write the sidecar, so a clean
shutdown won't trigger surprise navigation. The sidecar is consumed on
read regardless of whether navigation succeeded, so a stale entry
can't haunt later auto-launches.
2026-05-09 01:41:07 +09:00
leeguoooooandClaude Opus 4.6 81cdd3b216 fix(stealth): split minimal/full mode to eliminate detection lies on real Chrome
- CdpAttach mode: only removes navigator.webdriver (user's real Chrome
  already has genuine fingerprint, heavy patches create detectable lies)
- FullLaunch mode: applies all 32 patches (new Chrome needs full coverage)
- Improved webdriver removal: uses Object.defineProperty to override CDP
  getter on Navigator.prototype, not just delete
- CreepJS results: 0% stealth (was 20%), hasIframeProxy: gone

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 23:48:50 +09:00
leeguoooooandClaude Opus 4.6 7ee3d5fb94 feat(connect): make auto-connect to user's Chrome the default behavior
- Auto-connect is now ON by default (was opt-in via --auto-connect)
- Added --launch/--new flags to explicitly start a fresh browser
- CI environments (CI env var) automatically use --launch mode
- Friendly error message with platform-specific Chrome relaunch guide
- Mentions Chrome 144+ runtime CDP toggle (chrome://inspect)
- --cdp and --provider flags implicitly disable auto-connect
- AGENT_BROWSER_NO_AUTO_CONNECT=1 to disable, AGENT_BROWSER_FORCE_LAUNCH=1 to force

Track 3 of native-stealth migration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 23:48:50 +09:00
leeguoooooandClaude Opus 4.6 77616a209c feat(stealth): inject anti-detection patches in native Rust architecture
- Created cli/src/native/stealth.rs with stealth JS injection via CDP
- Extracted 32 patch IIFEs from TS stealth.ts into stealth_scripts.js
- Injected via Page.addScriptToEvaluateOnNewDocument on every launch/connect
- Added stealth Chrome args (disable AutomationControlled, use ANGLE GL)
- Auto-detects and cleans HeadlessChrome from User-Agent string
- Overrides navigator.userAgentData high-entropy hints
- Stealth enabled by default, disable with AGENT_BROWSER_STEALTH=0

Track 2 of native-stealth migration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 23:47:50 +09:00
Chris Tate 57405f9361 feat(react): React introspection, Web Vitals, and SPA primitives (#1257)
* feat(react): first-class React introspection, Web Vitals, and nextjs skill

Add React-general and web-universal features as first-class agent-browser verbs
(react tree/inspect/renders/suspense, vitals, pushstate). Genuinely Next.js-specific
workflows (PPR cookie protocol, /_next/mcp bridge, dev-server endpoints) ship as
a new `nextjs` skill that composes the primitives. No new runtime dependencies -
the React DevTools installHook.js is vendored (MIT) and include_str!'d into the
binary.

New commands:
  react tree                  Full React component tree (depth id parent name)
  react inspect <fiberId>     Props, hooks, state, source for one fiber
  react renders start|stop    Fiber profiler with Insts/Mounts/Re-renders/Self/DOM
                              + prev->next change details
  react suspense              Suspense boundaries + classifier (client-hook,
                              request-api, server-fetch, cache, stream, framework)
                              + root-cause grouping + recommendations
  vitals [url]                LCP/CLS/TTFB/FCP/INP + React hydration phases
  pushstate <url>             Generic SPA client-side navigation
  removeinitscript <id>       Remove a script registered via addinitscript

New launch flags:
  --init-script <path>        Register init scripts before first navigation
                              (repeatable; env AGENT_BROWSER_INIT_SCRIPTS)
  --enable <feature>          Built-in init scripts; currently react-devtools
                              (repeatable; env AGENT_BROWSER_ENABLE)

Other primitives:
  network route ... --resource-type <csv>  Filter by CDP resource type
  cookies set --curl <file>                Auto-detects JSON/cURL/Cookie-header

* fixes

* fixes

* fixes
2026-04-20 16:12:47 -05:00
Chris Tate 585d93a02b feat(tabs): t<N> prefix for tab ids; --label for named tabs; drop --tab peek flag (#1250)
* fix(tabs): preserve refs across --tab peek and cover outer-tab-closed path

Follow-up to #1249 so `--tab <id>` is actually useful for agents:

- Save and restore the outer tab's `ref_map`, `iframe_sessions`, and
  `active_frame_id` across a scoped command instead of clearing them.
  `snapshot` → `--tab N <cmd>` → `click @e1` now keeps the outer tab's
  refs intact. Scoped commands still see a clean slate so outer refs
  can't resolve against the scoped tab's DOM.
- Close the coverage gap the Vercel review bot flagged on #1249: the
  previous `e2e_tab_scoped_command_handles_outer_tab_closed` test used
  `tab_close`, which is in the scoped-dispatch exclusion list, so it
  never exercised the restore-skip branch it claimed to test. Renamed
  to `e2e_tab_close_with_tab_id_closes_active_tab` with an honest
  docstring, and added `e2e_tab_scoped_command_outer_tab_closed_mid_dispatch`
  that actually hits the branch via `window.opener.close()` on a
  script-opened intermediate tab.
- Add `e2e_tab_scoped_command_isolates_refs_from_outer_tab` pinning
  that outer refs don't bleed into the scoped tab's DOM resolution.
- Rewrite `e2e_tab_scoped_command_clears_state_on_switch` as
  `e2e_tab_scoped_command_preserves_outer_tab_state`, verifying the
  restored @e1 still clicks end-to-end.
- Update the 52 `--help` entries for `--tab <id>` to describe peek /
  restore semantics instead of a vague "Target specific tab ID".
- Update README, docs site, config schema, and the agent-facing
  skills reference with working examples (refs survive the peek) and
  a "when to use \`--tab <id>\` vs \`tab <id>\`" guide so agents pick
  the right flag for their workflow.

* fix(tabs): use t<N> prefix for tab ids, add --label for named tabs

Follow-on to the tab work in #1249 and the prior commit, redesigning the
tab handle surface before release since nothing ships these features yet.

## Why

Incrementing integer tab ids (`1`, `2`, `3`) look indistinguishable from
positional indices in command output, LLM-generated scripts, and docs. In
the common single-agent case where position and id coincide, readers have
no visual cue for which mental model they're using. Positional indices
silently shift when unrelated tabs open/close, so misreading a handle as
an index is a correctness hazard.

## Changes

**Tab ids are now `t1`, `t2`, `t3` (strings).** Bare integer `tabId`
values are rejected with a teaching message rather than silently accepted.
The `t` prefix matches the `@e1` element-ref convention and makes ids
unmistakably non-positional at a glance.

**Labels.** Tabs can be created with a user-assigned label (e.g. `docs`,
`app`) via `tab new --label <name> [url]`. Labels are interchangeable
with `t<N>` ids everywhere a tab ref is accepted. They're never
auto-generated, never rewritten on navigation, and must be unique within
a session.

**Dashboard fix.** `packages/dashboard/src/types.ts` declared
`TabInfo.index: number` but the daemon has been sending `tabId` (not
`index`) since #892, making `tab.index` `undefined` and breaking the
dashboard's close/switch buttons silently. Updated the TS types and
usages to consume `tabId` (string) and optional `label`, restoring the
dashboard's tab interactions.

## Surface

- `cli/src/native/browser.rs`: `TabRef::parse` / `format_tab_id` /
  `is_valid_label` / `PageInfo.label` / `BrowserManager::resolve_tab_ref`
  / `BrowserManager::has_label`. `tab_new` gains an optional label
  argument with duplicate rejection. All JSON responses use the string
  form and include the label.
- `cli/src/native/actions.rs`: scoped-command pre-dispatch and
  `handle_tab_{switch,close,new}` parse string refs and resolve to
  stable ids.
- `cli/src/{flags,commands,main,output}.rs`: `--tab` / config `tab`
  are `String`; `tab` subcommand accepts `t<N>` or a label and supports
  `tab new --label <name> [url]`. All 52 `--help` entries updated.
- `agent-browser.schema.json`: `tab` property type is now `string` with
  a pattern matching `t<N>` or label form.
- `packages/dashboard`: `TabInfo.tabId: string` / `label?: string | null`;
  `closeTabAtom`/`switchTabAtom` take `tabRef: string`; component props
  updated.
- Docs: README, docs site (`commands/` and `configuration/`), and the
  agent-facing skills reference rewritten with the new examples.

## Tests

- Added `TabRef::parse` / `format_tab_id` / `is_valid_label` unit tests
  pinning the bare-integer rejection, the teaching error, label rules,
  and round-tripping.
- Added `test_tab_switch_by_id` / `_by_label` / `test_tab_new_with_label`
  / `_with_label_and_url` / `_with_url_then_label` in `commands.rs`;
  rewrote `test_tab_unknown_subcommand_errors` since labels make
  `tab select` a legitimate ref.
- Added `e2e_tab_new_with_label_can_be_switched_and_peeked`,
  `e2e_tab_new_with_duplicate_label_errors`,
  `e2e_tab_scoped_command_rejects_bare_integer`.
- Migrated every existing tab e2e test (and one unit test) from
  integer `tabId` to the string form.

`cargo fmt`, `cargo clippy -- -D warnings`, all 30 non-ignored tab unit
tests, all 13 tab e2e tests, and `tsc --noEmit` on the dashboard all
pass.

* refactor(tabs): drop --tab scoped peek flag; keep t<N> ids and labels

After fleshing out `--tab <id|label>` in the previous commits (scoped
pre/post-dispatch save/restore, ref preservation, outer-tab-closed edge
case, full e2e coverage), the machinery-to-value ratio makes the feature
hard to justify. Nixing it now while nothing has shipped.

## Why

- Every new daemon feature touching per-tab state has to reason about
  scoped-dispatch interleaving. `ScopedRestore`, pre/post-dispatch hooks,
  and the exclusion list add ongoing maintenance tax.
- Three separate PRs (#892, #1249, and this one pre-nix) were needed to
  reach "works correctly." That's a smell.
- `tab <id|label>` switch + labels already cover the legible multi-tab
  workflow case.
- `--tab` vs `tab <id>` have opposite lifecycle semantics but look
  identical, teaching every agent two things where one would do.
- "Non-disruptive peek" isn't actually race-free: the daemon does swap
  active tab during execution, so a concurrent client between pre- and
  post-dispatch sees the scoped tab as active.
- Ref-based interaction with scoped tabs never worked ergonomically —
  refs are per-tab, so `--tab N click @e1` requires `@e1` to already be
  on tab N, which means a prior switch, which negates the peek.
- Adding a feature back is easy; removing shipped API is hard.

If per-tab caching (`HashMap<tab_id, RefMap>`) lands later, `--tab` can
be reintroduced essentially for free. That's the right time.

## Removed

- `--tab <id|label>` global flag (`cli/src/flags.rs`, `cli/src/main.rs`,
  all 52 `--help` entries in `cli/src/output.rs`).
- `tab` property in `agent-browser.schema.json` and the config-options
  row in `docs/src/app/configuration/page.mdx`.
- `ScopedRestore` struct, pre/post-dispatch save/restore in
  `execute_command` (`cli/src/native/actions.rs`).
- `impl Default for RefMap` in `cli/src/native/element.rs` (only added
  for `mem::take` in the scoped machinery).
- `e2e_tab_global_targeting`, `_snapshot`, `_snapshot_non_contiguous`,
  `e2e_tab_scoped_command_preserves_outer_tab_state`,
  `_isolates_refs_from_outer_tab`, `_restores_active_tab`,
  `_outer_tab_closed_mid_dispatch`. 590 lines.
- The "When to use `--tab` vs `tab <id|label>`" sections in README,
  docs site, and skills reference.

## Kept

- Stable tab ids (`t1`, `t2`, `t3`) with bare-integer rejection.
- User-assigned labels (`tab new --label docs [url]`), with duplicate
  rejection and interchangeable use everywhere a tab ref is accepted.
- `BrowserManager::{active_tab_id, has_tab_id, resolve_tab_ref, has_label}`
  accessors (still used by the remaining tab handlers).
- `TabRef::parse`, `format_tab_id`, `is_valid_label` and their unit
  tests.
- Dashboard TS fix (`TabInfo.tabId` + `label`).
- `e2e_tab_close_with_tab_id_closes_active_tab` (renamed docstring to
  drop the gone exclusion-list reference).
- `e2e_tab_new_with_label_can_be_switched_and_closed` (rewrite of the
  previous `_and_peeked` test — now exercises only switch and close).
- `e2e_tab_switch_rejects_bare_integer` (rewrite targeting the
  `tab_switch` daemon handler rather than the removed scoped path).

net: -900 lines across 12 files. `cargo fmt`, `cargo clippy -D warnings`,
all 25 non-ignored tab unit tests, all 6 tab e2e tests, and
`tsc --noEmit` on the dashboard all pass.
2026-04-16 14:33:43 -05:00
Chris Tate c201623710 fix(tabs): correct --tab scoped commands and un-break provider direct-page path (#1249)
* fix(tabs): initialize tab_id on missing PageInfo sites

PR #892 added a required `tab_id: u32` field to `PageInfo` but missed two
initializer sites, which broke the build on the PR branch. CI never caught
this because the external-contributor workflow status was `action_required`
and never ran.

- `cli/src/native/browser.rs:395` — the `direct_page` branch of
  `connect_cdp_inner` used by the cloud providers (Browserbase, Browserless,
  Browser Use, Kernel, AgentCore). Use `assign_tab_id()` to get a fresh id.
- `cli/src/native/browser.rs:1580` — a unit test initializer. Use `tab_id: 1`
  since the test doesn't exercise id assignment.

* feat(tabs): restore active tab and clear per-tab state for scoped --tab

Follow-up on PR #892's `--tab <id>` flag.

The original implementation called `tab_switch_by_id` directly from the
pre-dispatch block in `execute_command` but didn't touch the daemon's
per-tab state, and never restored the previously-active tab. Two concrete
issues this fixes:

1. `state.ref_map`, `state.iframe_sessions`, and `state.active_frame_id`
   were left intact across the pre-dispatch switch, so `--tab N click @e1`
   would try to resolve `@e1` against the scoped tab's DOM using a
   backend-node id from the outer tab. In practice the click handler's
   role+name fallback hid this as "element not found" errors, but on pages
   where both tabs have similarly-labelled elements it could click the
   wrong one.

2. The PR description promised scoped routing would "restore the previous
   active tab", but the implementation permanently switched. `--tab 3
   snapshot` would leave tab 3 as the active tab even after the command
   returned, surprising subsequent non-scoped commands.

This change:

- Saves the current tab's stable `tab_id` (not its array index, which
  would shift if the scoped command closed other tabs) before switching.
- Clears per-tab daemon state before the switch so refs/iframes/frame
  context can't leak between tabs.
- After the action runs, restores the original active tab (also via
  stable id) unless that tab was closed during the scoped command, in
  which case we leave the scoped tab active.
- Adds `BrowserManager::active_tab_id()` and `has_tab_id()` accessors
  to support the above without exposing the internal `pages` vector.

* test(tabs): regression tests for scoped --tab state clearing and restoration

Three new `#[ignore]` e2e tests pinning the fixed behavior:

- `e2e_tab_scoped_command_clears_state_on_switch` — populates `ref_map` on
  tab 1, runs a `tabId: 2`-scoped command, asserts `ref_map`,
  `iframe_sessions`, and `active_frame_id` are all cleared.
- `e2e_tab_scoped_command_restores_active_tab` — sets up two tabs, runs
  a scoped command against the non-active one, asserts a subsequent
  unscoped command reflects the originally-active tab.
- `e2e_tab_scoped_command_handles_outer_tab_closed` — runs a scoped
  `tab_close` that kills the outer tab itself, asserts no error and the
  scoped tab becomes active.

Also updates two misleading comments in the PR's existing
`e2e_tab_global_targeting*` tests to reflect restoration semantics; the
assertions themselves were already consistent with restoration.

* docs(tabs): document stable tab IDs and --tab scoped-command flag

Per AGENTS.md, changes that users or agents would need to know about must
land in every doc surface. Fills the gaps PR #892 left:

- `README.md` — new `--tab <id>` row in the Options table, rewrite the
  tab command examples to use `<id>` instead of `<n>`, add a paragraph
  explaining stable tab IDs and `--tab` peek semantics.
- `docs/src/app/commands/page.mdx` — same command-example rewrite plus a
  new "Stable tab IDs and `--tab`" subsection.
- `docs/src/app/configuration/page.mdx` — add `tab` row to the config
  options table so JSON config users can discover it.
- `agent-browser.schema.json` — add `tab` property with description,
  matching the config schema.
- `skills/agent-browser/references/commands.md` — same command-example
  rewrite plus a short paragraph for agents on when to use `--tab`.
2026-04-16 12:34:14 -05:00
Daniel Hails 67dc631977 Consistent Tab IDs & Global Tag Targeting (#892)
Introduces stable per-tab IDs and a global `--tab <id>` flag for scoping individual commands to a specific tab.

Breaking change: response payloads for `tab_list`, `tab_new`, `tab_switch`, `tab_close`, and `window_new` now use `tabId` instead of `index`. `tab_close` returns `{tabId, closed: true}` instead of `{closed, activeIndex}`. `agent-browser tab <unknown>` now errors instead of silently listing tabs.

Follow-up PR to land immediately after this fixes a compile error on the provider direct-page path, clears per-tab daemon state around scoped switches, and implements active-tab restoration so `--tab N` is non-intrusive as intended.
2026-04-16 12:02:55 -05:00
Chris Tate c691b269cb fix: improve config schema and serve from docs site (#1248)
Fix idleTimeout description to document human-friendly formats (30s,
5m, 1h) alongside raw milliseconds. Add trailing newline. Serve the
schema from the docs app at agent-browser.dev/schema.json via a
prebuild copy step, and update all $schema URLs to use the stable
docs-hosted URL instead of raw GitHub.
2026-04-16 10:42:44 -05:00
Tom Dale 19808d08f8 fix: load storage state at launch when --state / AGENT_BROWSER_STATE is set (#1241)
* fix: load storage state at launch when --state / AGENT_BROWSER_STATE is set

The `--state` flag and `AGENT_BROWSER_STATE` env var were documented as
restoring saved browser state (cookies + localStorage) at launch, but
`load_state()` was never called after the browser started. The feature
has been broken since it was introduced.

Adds `try_load_storage_state()` and calls it from every early-return
path in `auto_launch()` (lazy launch triggered by commands like
`navigate`) and from `handle_launch()` (explicit `launch` command).

Also adds 4 e2e tests covering all state-persistence paths:
- Explicit launch with `storageState` field
- Auto-launch via `AGENT_BROWSER_STATE` env var
- Session-name auto-restore via `try_auto_restore_state`
- Explicit `state_load` command (baseline sanity check)

Fixes #1164.

* style: apply cargo fmt to e2e_tests.rs

Reformats a single long format\! call to satisfy CI's rustfmt check.
No behavior change.

* fix: call try_load_storage_state in all handle_launch branches

The CDP URL, CDP port, auto-connect, and provider early-return branches
were skipping storage state loading because try_load_storage_state was
only called in the normal BrowserManager::launch() path at the bottom
of handle_launch().

Also compute storage_state_owned once and reuse it across all branches
rather than borrowing storage_state (a &str tied to cmd) in a helper
that needs an owned Option<String>.

* Fix storage state reload on reused launches

* Fix storage-state launch errors

* Fix storage state replay ordering

* Align storage-state errors across launch paths

* Fix storageState launch cleanup
2026-04-16 08:38:54 -05:00
jin.2andhyunjinee db29d5fead fix: inherit viewport dimensions in recording context (#1208)
* fix: inherit viewport dimensions in recording context

When `record start` creates a new browser context, it now re-applies the
current viewport settings (from `set viewport` or `set device`) so the
recording resolution matches what the user configured instead of falling
back to the default 1280×720.

Closes #1207

* style: apply cargo fmt to e2e test

* chore: remove obvious comments

* chore: remove obvious comments from e2e test

---------

Co-authored-by: hyunjinee <leehj0110@kakao.com>
2026-04-13 23:40:26 -05:00
2164e71c30 fix: use custom viewport dimensions in streaming frame metadata and image resolution (#1033)
* fix: use custom viewport dimensions in streaming frame metadata

  CDP's Page.screencastFrame metadata returns physical device dimensions
  instead of the emulated viewport, causing frame messages to report
  incorrect deviceWidth/deviceHeight when a custom viewport is set.

  Use the viewport dimensions captured at screencast start instead of
  the CDP metadata values, since the screencast image is already captured
  at the configured viewport size.

  Closes #1031

* fix: resize browser content area on viewport change for correct
  screencast dimensions

  Emulation.setDeviceMetricsOverride only changes the CSS viewport, but
  screencast captures the actual browser content area. This caused frame
  images to have incorrect dimensions (e.g., 1000x451 instead of
  1000x1000)
  when a custom viewport was set.

  - Call Browser.setContentsSize after setDeviceMetricsOverride so the
    content area matches the emulated viewport
  - Restart active screencast when viewport dimensions change so
    maxWidth/maxHeight parameters are updated
  - Skip redundant screencast restarts when dimensions are unchanged
  - Extend E2E test to verify actual JPEG image dimensions, not just
    metadata

* fix: pass viewport dimensions to --window-size at launch and log setContentsSize failures

- Add viewport_size to LaunchOptions so --window-size matches the
  configured viewport from the start, reducing reliance on the
  experimental Browser.setContentsSize CDP call at runtime
- Log Browser.setContentsSize failures instead of silently ignoring
  them with let _ =

* fix: remove duplicate viewport change detection block (dead code from merge)

* fix: use log::debug! instead of eprintln! for setContentsSize failure

* revert: use eprintln! instead of log crate for setContentsSize failure

The daemon's stderr pipe is closed after startup, so log crate
subscribers cannot output during normal operation. eprintln! is
visible during startup and in tests, matching the existing convention.

---------

Co-authored-by: hyunjinee <leehj0110@kakao.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 01:28:36 -05:00
juniper929andwangjingjing 6520e4123c fix: re-apply ignore_https_errors to recording context (#1178)
Security.setIgnoreCertificateErrors is session-scoped, so creating a new
BrowserContext for recording (Target.createBrowserContext) starts with the
default certificate validation enabled, ignoring the launch-time flag.

Store ignore_https_errors in BrowserManager alongside download_path, and
re-apply Security.setIgnoreCertificateErrors to the new session after
recording context creation — matching the existing pattern for download
behavior re-application.

Fixes #1172

Co-authored-by: wangjingjing <wangjingjing.99@bytedance.com>
2026-04-07 01:26:07 -05:00
Chris Tate 317e6869b6 Add AI chat to dashboard, refactor stream module, snapshot --urls, batch argument mode (#1160)
* chat

* refactor

* fixes

* fixes

* fixes

* fixes

* improvements

* download chat

* batch

* fixes

* fixes

* fixes

* fmt

* fixes

* fixes

* fixes

* fmt
2026-04-06 08:10:43 -05:00
jin.2andhyunjinee fcb6615f5a fix: support accessibility tree refs in upload command (#1156)
* fix: support accessibility tree refs in upload command (#1107)

The upload command only accepted CSS selectors while click/fill supported
accessibility tree refs (e.g. e1, @e1, ref=e1). This resolves the API
inconsistency by reusing resolve_element_object_id for all selector types.

* style: apply cargo fmt

---------

Co-authored-by: hyunjinee <leehj0110@kakao.com>
2026-04-05 15:38:49 -05:00
Chris Tateandctate c47756be9b fix(cli): honor AGENT_BROWSER_DEFAULT_TIMEOUT env var for wait commands (#1153)
* fix(cli): honor AGENT_BROWSER_DEFAULT_TIMEOUT env var for wait commands

The `AGENT_BROWSER_DEFAULT_TIMEOUT` environment variable was being ignored by CLI wait commands, causing them to use hardcoded 30-second timeouts instead of the configured default.

## Changes Made

- **Centralized timeout injection**: Modified `parse_command()` to automatically inject `flags.default_timeout` into any wait-family command that doesn't already have an explicit `--timeout` flag
- **Environment variable parsing**: Added `default_timeout` field to `Flags` struct that reads from `AGENT_BROWSER_DEFAULT_TIMEOUT` env var
- **Daemon propagation**: Updated daemon spawning to pass through the default timeout via environment variables
- **Unified timeout handling**: Added `timeout_ms()` helper method in `DaemonState` that all wait handlers now use instead of scattered `unwrap_or()` calls
- **Comprehensive test coverage**: Added 10 regression tests covering all wait command variants and edge cases

## Implementation Details

The fix uses a two-stage approach:
1. CLI parses the env var and injects timeout values into command JSON for any `wait*` action
2. Daemon reads the env var and provides a centralized fallback via `timeout_ms()` helper

This ensures new wait variants automatically inherit the default timeout without requiring per-variant wiring.

Fixes #1147

* fix: preserve 30s default timeout for backward compatibility

The default_timeout_ms fallback was set to 25_000ms, which silently
changes the existing 30_000ms behavior for users who haven't set
AGENT_BROWSER_DEFAULT_TIMEOUT. Restore the original 30s default.

---------

Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
2026-04-05 14:15:00 -05:00
jin.2andhyunjinee 9f51879012 fix: rewrite getByRole to use CDP accessibility tree with ref-based element resolution (#1145)
* fix: rewrite getByRole to use CDP accessibility tree instead of CSS selectors

The old `handle_getbyrole` generated `querySelectorAll('[role="link"], link')`
which matched `<link>` stylesheet elements instead of `<a>` anchor tags.
This happened because ARIA role names were used directly as CSS tag selectors,
and several roles differ from their HTML element names (e.g. link → a,
heading → h1-h6, textbox → input/textarea).

The fix replaces the JS-based DOM query with the CDP `Accessibility.getFullAXTree`
API, where the browser engine correctly computes implicit ARIA roles per the
WAI-ARIA / HTML-AAM spec. This is the same approach already used by `snapshot.rs`
and `element.rs` in this codebase.

Changes:
- Rewrite `handle_getbyrole` to query the browser's accessibility tree via CDP
- Add `find_ax_node_by_role` helper for AX tree traversal with role/name/exact matching
- Use `DOM.resolveNode` + `Runtime.callFunctionOn` to bridge AX node → DOM marker
- Add iframe support via `resolve_ax_session` (missing in old implementation)
- Fix cleanup to use correct CDP session (old code used default session, breaking iframe cleanup)
- Export `extract_ax_string` as `pub(super)` for reuse
- Add 4 regression tests for `find_ax_node_by_role`

Fixes #1123

* style: apply cargo fmt

* chore: remove redundant comments

* refactor: replace marker attribute with temporary ref for element resolution

Eliminates 3 CDP round-trips (DOM.resolveNode, Runtime.callFunctionOn,
Runtime.evaluate cleanup) by registering a temporary ref in the ref_map.
execute_subaction resolves the element via backendNodeId directly.
No more DOM pollution with marker attributes.

* fix: ref counter collision, ref_map leak, and stale fallback name

- Increment next_ref_num after inserting temp ref to prevent id collision
- Remove temp ref after execute_subaction to prevent unbounded ref_map growth
- Return actual AX name from find_ax_node_by_role for accurate fallback resolution
- Add RefMap::remove method

---------

Co-authored-by: hyunjinee <leehj0110@kakao.com>
2026-04-05 09:10:24 -05:00
9f8e518a46 feat: reuse Chrome profile login state via --profile <name> (#1131)
* feat(chrome): add Chrome profile name resolution and copy for --profile flag

When --profile receives a name without path separators (e.g., "Default"),
it now resolves the name against installed Chrome profiles, copies the
profile to a temp directory (excluding large cache dirs), and launches
Chrome with the copied profile to reuse login state.

Key changes:
- Add profile resolution: is_chrome_profile_name, find_chrome_user_data_dir,
  list_chrome_profiles, resolve_chrome_profile (3-tier matching)
- Add copy_chrome_profile with best-effort copy and exclusion list
- Wire preprocessing into launch_chrome before retry loop
- Add use_real_keychain field to LaunchOptions for conditional keychain flags
- Make --password-store=basic and --use-mock-keychain conditional

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(cli): add `profiles` command to list available Chrome profiles

Adds `agent-browser profiles` command that reads Chrome's Local State
file to list available profiles with directory names and display names.
Supports --json output. Added help text in print_command_help and
print_help.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add Chrome profile reuse documentation across all locations

Update all 5 documentation locations per AGENTS.md:
- output.rs: updated --profile help text and examples
- README.md: added Chrome Profile Reuse section, updated options table
- SKILL.md: added profile reuse as Option 2
- docs/src/app/sessions/page.mdx: added Chrome profile reuse section
- chrome.rs: added doc comments to get_chrome_user_data_dirs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: fix formatting and clippy warning in chrome.rs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: simplify profile resolution and launch integration

- Only clone LaunchOptions when profile name requires resolution
  (avoids unnecessary allocation on every Chrome launch)
- Remove redundant is_file() check before copy of Local State
  (copy() handles missing files naturally)
- Extract format_profile_list() to deduplicate error formatting
- Remove unnecessary section comments in tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(tests): use RAII TempDir guard for test cleanup

Replace manual remove_dir_all calls with a TempDir struct that
auto-cleans on drop, preventing temp dir leaks on test panics.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
2026-04-04 11:21:11 -05:00
Chris Tateandctate c69f611d78 Fix CDP attach hang on real browser sessions (Chrome 144+) (#1133)
When connecting to a real, already-running browser (Chrome 144+) via CDP,
targets may be paused waiting for the debugger after attach. Without an
explicit Runtime.runIfWaitingForDebugger call, page-level commands hang
indefinitely even though the WebSocket connection is live.

Add Runtime.runIfWaitingForDebugger after Runtime.enable in all target
attachment paths: enable_domains (covers initial attach, tab_new,
tab_switch), enable_domains_direct (provider proxies), and the iframe
auto-attach handler. The call is placed before Network.enable to avoid
the documented deadlock when Network.enable precedes the resume. It is
a no-op for targets that are not paused.

Fixes #1130

Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
2026-04-04 11:12:35 -05:00
05d86fadf5 fix: relaunch browser when launch options change (#996)
* fix: relaunch browser when launch options change (#993)

  When the daemon already held a running browser, handle_launch only
  checked connection type and liveness to decide reuse. Config changes
  like adding extensions to config.json were silently ignored.

  Store a hash of the relaunch-relevant LaunchOptions fields and compare
  on each launch command. If the hash differs the browser is closed and
  relaunched with the new options.

* fmt

* fix

* fix

* fmt

---------

Co-authored-by: hyunjinee <leehj0110@kakao.com>
Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
2026-04-04 10:41:02 -05:00
Hung-Che Lo 4b5ba9f245 fix(native): auto_launch() honours AGENT_BROWSER_PROVIDER for cloud providers (#1126)
When a non-launch command (e.g. open, snapshot) triggers auto_launch()
before the explicit launch command is processed, auto_launch() now checks
AGENT_BROWSER_PROVIDER and connects via the provider API instead of
always falling back to a local Chrome instance.

Also redirects daemon stderr to /dev/null when not in debug mode to
prevent crashes from broken pipe after the CLI drops the piped stderr
handle. Cloud providers may write to stderr during connection setup.

Fixes #1125
Related: #979
2026-04-04 10:29:04 -05:00
Chris Tateandctate c52d25d576 Fix HAR capture missing API requests under heavy traffic (#1135)
The CDP event broadcast buffer (256 events) was too small for pages with
many concurrent API requests, causing silent event drops. Modern SPAs
routinely fire 100+ API calls during page load, generating 300+ CDP
network events that would overflow the buffer between drain cycles.

Changes:
- Increase CDP broadcast buffer from 256 to 4096 (event channel) and
  512 to 4096 (raw channel)
- Reduce background drain interval from 500ms to 100ms
- Handle Network.loadingFailed events in HAR recording
- Enable Network.enable on cross-origin iframe sessions during HAR
  recording and request tracking
- Allow Network events from iframe sessions through the session filter
- Log a warning when buffer overflow occurs instead of silently dropping

Fixes #1128

Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
2026-04-04 10:25:12 -05:00
Chris Tate 13ed01b3bd agentcore docs (#1120)
* agentcore docs

* fixes

* fixes
2026-04-02 20:31:14 -05:00
Pahud HsiehandChris Tate 8561a755ef feat: add AWS Bedrock AgentCore browser provider (native Rust) (#397)
* feat: add AWS Bedrock AgentCore browser provider (native Rust)

- Add agentcore provider with SigV4 authentication
- AWS SDK deps are optional behind 'agentcore' feature flag
- Build with: cargo build --features agentcore
- Supports AGENTCORE_REGION, AGENTCORE_PROFILE_ID, AGENTCORE_BROWSER_ID env vars
- Returns session ID and Live View URL in launch response
- Add connect_cdp_with_headers for signed WebSocket connections

* test: add unit tests for AgentCore provider

* refactor: use lightweight manual SigV4 signing instead of AWS SDK

- Replace aws-sigv4/aws-config with manual HMAC-SHA256 signing
- Removes ~60s compile time and significant binary size
- Credentials read from AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY env vars
- Supports AWS_SESSION_TOKEN for temporary credentials

* fix: correct AgentCore API endpoints

- Host: bedrock-agentcore.{region}.amazonaws.com
- Start session: PUT /browsers/{id}/sessions/start
- Stop session: PUT /browsers/{id}/sessions/stop
- Add urlencoding for browser ID in path
- Add AWS_DEFAULT_REGION fallback

* fix: use profileConfiguration.profileIdentifier for AgentCore profile

The AWS Bedrock AgentCore API expects profile configuration in the format:
{
  "profileConfiguration": {
    "profileIdentifier": "<profile-id>"
  }
}

Not the flat "profileId" field that was previously used.

* feat: support AWS credential provider chain via AWS CLI

- Try env vars first (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
- Fall back to 'aws configure export-credentials --format env'
- Honor AWS_PROFILE environment variable
- Works with SSO, IAM roles, credential files, etc.

---------

Co-authored-by: Chris Tate <chris@ctate.dev>
2026-04-02 18:33:37 -05:00
1f4b6b9d7a fix: include buttons bitmask in drag mouseMoved events (#1087)
* fix: include buttons bitmask in drag mouseMoved events

The drag handler was omitting the `buttons` field from every
`mouseMoved` event dispatched during the move phase.  Without it the
browser sees `event.buttons === 0`, meaning no button is held, so
`dragstart`/`dragover`/`drop` never fire and the drop target never
receives the element.

Fix:
- Add `"buttons": 1` (left-button mask) to each `mouseMoved` sent
  while the button is held.
- Add `"buttons": 1` to `mousePressed` and `"buttons": 0` to
  `mouseReleased`, consistent with how `dispatch_click` handles the
  same fields in interaction.rs.
- Correct the parity-test fixture for `drag`, which was supplying a
  `selector` key instead of the `source` key that `handle_drag` reads.
- Add an e2e test (`e2e_drag_action_sends_buttons_during_move`) that
  drives the high-level `drag` action against the existing
  `html5_drag_probe` fixture and asserts that `mousemove` events carry
  `buttons == 1` and that `dragstart` fires on the source element.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style: fix rustfmt formatting in e2e drag test

---------

Co-authored-by: wangjingjing <wangjingjing.99@bytedance.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
2026-03-30 20:21:04 -05:00
Chris Tate 40fdb4284d 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
2026-03-30 18:35:12 -05:00
jin.2andhyunjinee 369f48752a fix: expose raw CDP args in console output and use preview for formatting (#1040)
Closes #1039

- Add `preview` field to `RemoteObject` to capture CDP object previews
- Implement `format_console_arg` using preview data (value → preview → description)
- Store raw CDP args in `ConsoleEntry` and include in JSON output
- Skip typed `ConsoleApiCalledEvent` deserialization in favor of direct param extraction
- Unify console arg formatting between daemon (actions.rs) and stream (stream.rs)

Before: `console.log({userId: "abc", count: 42})` → `"Object"`
After:  `console.log({userId: "abc", count: 42})` → `{userId: "abc", count: 42}`

JSON output now includes raw `args` array for programmatic access by AI agents.

Co-authored-by: hyunjinee <leehj0110@kakao.com>
2026-03-29 12:24:27 -06:00
Chris Tateandctate 6dd53449e8 Add auto-dismissal for alert and beforeunload dialogs (#1075)
* Add auto-dismissal for alert and beforeunload dialogs

This PR adds automatic handling of JavaScript dialogs to prevent the agent from blocking indefinitely when `alert()` or `beforeunload` dialogs appear on web pages.

## Summary

Previously, when a website displayed native browser confirmation dialogs (like alerts or "Are you sure you want to leave?" prompts), agent-browser would hang waiting for manual intervention. This is a common issue since many websites use these dialogs for notifications or navigation warnings.

## Changes Made

- **Auto-dismiss functionality**: Added a background task that automatically accepts `alert` and `beforeunload` dialogs while leaving `confirm` and `prompt` dialogs for explicit handling
- **New flag**: Added `--no-auto-dialog` flag to disable automatic handling when needed
- **Environment variable**: Added `AGENT_BROWSER_NO_AUTO_DIALOG` for configuration
- **Documentation**: Updated README and docs with usage examples and configuration details
- **Tests**: Added comprehensive test coverage for flag parsing and dialog handling logic

## Implementation Details

- Only `alert` (notification-only) and `beforeunload` (navigation warning) dialogs are auto-handled for safety
- `confirm` and `prompt` dialogs still require explicit `dialog accept/dismiss` commands to ensure agents make deliberate choices for destructive actions
- The feature is enabled by default since these dialog types rarely require user decision-making
- Uses Chrome DevTools Protocol's `Page.handleJavaScriptDialog` for reliable dialog dismissal

Fixes #1070

* Log dialog type and message before auto-dismissal

Without this, auto-dismissed alert/beforeunload dialogs are silently
swallowed and the agent has no way to see what the dialog said. Adding
an eprintln before the CDP call makes the dismissal visible in stderr
for debugging.

* Log dialog dismissal errors instead of silently discarding them

- Remove premature "accepted" from log message since it fires before
  the CDP command executes
- Replace `let _ =` with `if let Err(e)` to log failures when
  Page.handleJavaScriptDialog fails
- Apply rustfmt to auto-dialog tests

---------

Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
2026-03-29 12:00:27 -06:00
jin.2andhyunjinee 43d9c40bc4 fix: detect externally opened tabs in --cdp mode (#1042)
* fix: detect externally opened tabs in --cdp mode (#1037)

Tabs opened outside of agent-browser (e.g. by the user or another CDP
client) were invisible to `tab list` because:

1. `Target.targetCreated` with chrome://newtab/ was filtered by
   `is_internal_chrome_target`, and the subsequent `targetInfoChanged`
   with the real URL could not update a target that was never tracked.

2. The background drain loop only ran when `request_tracking ||
   har_recording` was active, so target events between commands were
   silently dropped from the broadcast channel.

Fix: promote untracked targets in `targetInfoChanged` to new targets,
run the background drain unconditionally (guarded by browser presence),
and extract `apply_drained_events` to share target lifecycle processing
(attach, domain filter, iframe sessions) between execute_command and
the background drain.

* refactor: clean up HashSet import and remove call-site duplication

- Import HashSet alongside HashMap instead of using fully-qualified path
- Replace duplicated drain+apply sequence in execute_command with
  drain_cdp_events_background call

* style: apply cargo fmt

---------

Co-authored-by: hyunjinee <leehj0110@kakao.com>
2026-03-29 11:51:49 -06:00
jin.2andhyunjinee dc26ff7667 fix: save_state captures cross-domain cookies and localStorage (#1064)
The Rust rewrite of save_state only captured cookies and localStorage
for the current page's origin, silently dropping cross-domain data
(e.g. SSO/CAS auth cookies). This was a regression from the JS version.

Cookies: replace Network.getCookies with Network.getAllCookies to
return cookies from all domains the browser has visited.

localStorage: track visited origins in BrowserManager during navigation,
then collect their localStorage via a temporary CDP target with Fetch
interception (serves blank HTML to avoid real network requests).

Co-authored-by: hyunjinee <leehj0110@kakao.com>
2026-03-28 13:41:53 -07:00
Chris Tatectategithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Stefan SmiljkoviczhanbaxuyongliangxuyongliangThomas Kosiewski
f9174513c2 dashboard (#1034)
* dashboard

* fix: re-apply download behavior on recording context (#1019)

* fix: re-apply download behavior on recording context

record start creates a new browser context via Target.createBrowserContext.
Browser.setDownloadBehavior called at launch only applies to the default
context, so downloads in the recording context are silently dropped.

Fix:
1. Store download_path on BrowserManager (from LaunchOptions)
2. After creating the recording context, call Browser.setDownloadBehavior
   with the new browserContextId

This ensures downloads work during recording.

Fixes #1018

* fix: add download_path to third BrowserManager constructor (auto_connect_cdp)

* fix: reap zombie Chrome process and fast-detect crash for auto-restart (#1023)

When Chrome crashes (e.g. SIGTRAP from CHECK() assertion), the daemon
now:

1. Reaps the zombie immediately via a SIGCHLD handler in the event loop
   that calls waitpid(-1, WNOHANG)
2. Detects the crash instantly on the next command via a non-blocking
   try_wait() check (has_process_exited), avoiding the 3-second CDP
   timeout that is_connection_alive() would incur
3. Auto-relaunches Chrome transparently for the caller

Fixes #1017

Co-authored-by: ctate <366502+ctate@users.noreply.github.com>

* fix: route keyboard type through text input (#1014)

* fix: handle --clear flag in console command (#1015)

The console and errors commands parsed --clear from CLI args but the
action handlers silently ignored the flag. The handlers did not accept
the cmd parameter so they had no way to read the clear field.

Changes:
- Add clear_console() method to EventTracker in network.rs
- Update handle_console to accept cmd, read the clear field, and clear
  the buffer when --clear is passed (returns {cleared: true})
- Update call site in execute_command to pass cmd

Co-authored-by: xuyongliang <yongliang.xyl@alibaba-inc.com>

* chore: patch release - ### Bug Fixes

- **Re-apply download behavior on r... (#1025)

* Add runtime stream enable/disable/status commands (#951)

* Add runtime stream management commands

* Run rustfmt and satisfy clippy

* Fix stream disable cleanup semantics

* Format stream disable regression tests

* fix: retain radio/checkbox elements in compact snapshot tree (#1008)

compact_tree() checked for "[ref=" to identify lines worth keeping, but
radio and checkbox elements render as e.g. [checked=false, ref=e1] where
the "[" opens before "checked=", not "ref=". Dropping the leading bracket
so the check is just "ref=" fixes the match for all elements with refs.

Fixes #1006

Co-authored-by: ctate <366502+ctate@users.noreply.github.com>

* chore: version packages (#1027)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fixes

* dashboard

* fixes

* remove observe

* fmt

* fixes

* fixes

* jotai

* fmt

* upload dashboard

---------

Co-authored-by: Stefan Smiljkovic <stefan@vanila.io>
Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
Co-authored-by: zhanba <c5e1856@gmail.com>
Co-authored-by: xuyongliang <478439790@qq.com>
Co-authored-by: xuyongliang <yongliang.xyl@alibaba-inc.com>
Co-authored-by: Thomas Kosiewski <thoma471@googlemail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-26 08:43:35 -07:00
Thomas Kosiewski 67b5ee1600 Add runtime stream enable/disable/status commands (#951)
* Add runtime stream management commands

* Run rustfmt and satisfy clippy

* Fix stream disable cleanup semantics

* Format stream disable regression tests
2026-03-25 11:36:16 -07:00