Compare commits

...
8 Commits
Author SHA1 Message Date
leeguooooo fd2cdcde77 chore(release): 1.5.18 — issue-reporting guidance + clear data:-over-relay error
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
2026-06-17 14:10:07 +09:00
leeguooooo 8e001e3d88 feat(dx): guide users to file issues; clear error for data: URLs over the relay
#5: the CLI never pointed users at the issue tracker. `--help` and `--version`
now print the issues URL (https://github.com/leeguooooo/chrome-use/issues), so
agents hitting a rough edge know where to report it (the skill already nudges).

#1: navigating to a `data:` URL over the extension relay fails with a cryptic
`net::ERR_ABORTED` on an about:blank tab (chrome.debugger/chrome.tabs can't drive
a top-frame data: navigation; it works fine under --launch). Detect that case and
explain it — use a real http(s)://file:// URL or --launch — instead of leaking
the raw code. (Surfaced while stress-testing 5 concurrent agents.)
2026-06-17 14:10:04 +09:00
leeguooooo eb2bc343a0 chore(release): 1.5.17 — complete multi-agent isolation (no foreign tab/pop-up adoption on click)
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
2026-06-17 13:31:50 +09:00
leeguooooo d86c9c4be2 fix(relay): don't adopt foreign tabs/pop-ups on click — completes multi-agent isolation (#last hole)
After strict isolation (v1.5.16) a session's tracked set is only its OWN tabs, so
in `adopt_newly_opened` (run after every click to follow a pop-up) EVERY foreign
tab looks "new" relative to the session's `before` set and got adopted — a
click-heavy flow on a busy shared Chrome pulled other agents'/the user's tabs
(github, Lark, iphone-use) into the session mid-flow.

A pop-up the agent itself opened can't be told apart from a foreign tab over the
relay (the synthesized targetInfo carries no opener/window/group), so on the relay
`adopt_newly_opened` now adopts nothing: the agent drives only tabs it explicitly
created; pop-ups (OAuth/login windows) are the user's. Launched browsers (every
tab ours) still follow pop-ups. Verified live: a fresh click-heavy relay session
stays clean (only its own tabs), and 5 concurrent agents churning tabs show zero
cross-agent drift.
2026-06-17 13:31:49 +09:00
leeguooooo 32c25a6627 chore(release): 1.5.16 — strict multi-agent tab isolation on the relay
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
2026-06-17 11:50:10 +09:00
leeguooooo a8ce3dd3f8 fix(relay): strict multi-agent isolation — a session owns only its own tabs
Several agents (and other tools opening tabs) share one real Chrome via the
relay. Previously every session adopted ALL tabs from Target.getTargets, so
another agent's tab churn polluted the list, dropped the tab being driven, and
drifted commands onto the wrong page (the W-8BEN tax tab vanished mid-flow when a
concurrent iphone-use agent opened tabs).

A tab group belongs to exactly one agent. On the relay a session now tracks and
drives ONLY the tabs it created (its own colored group) plus pop-ups its own
clicks open — it never adopts the user's or other agents' tabs:

- discover_and_attach (relay): create the session's own tab and pin it; do not
  adopt any existing foreign tab.
- resync_targets (relay): never adopt unknown targets; never prune the session's
  tabs on a single getTargets snapshot (multi-agent churn / cross-process-nav
  gaps routinely omit live tabs) — prune only after RELAY_PRUNE_MISSES
  consecutive absent snapshots (debounced), pinned active always protected.
- adopt_newly_opened: a tab that appears right after this session's action is a
  pop-up we opened — record it as owned.

Launched browsers (every tab ours) keep adopting all tabs. Adds debounced_prune_ids
+ unit tests for the churn tolerance.
2026-06-17 11:50:09 +09:00
leeguooooo 997373fd57 chore(release): 1.5.15 — trusted activation for in-iframe buttons (#39)
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
2026-06-17 10:16:17 +09:00
leeguooooo 5a858af93f fix(iframe): trusted activation for in-iframe buttons — keyboard, not synthetic click (#39)
A DOM `.click()` is isTrusted:false, which security-sensitive embedded forms
reject — Google Payments' enabled `保存` button silently no-op'd, so a
cross-origin payment/checkout/KYC form could be read, scrolled, and typed into
but never submitted. A coordinate click can't help either: getBoxModel for a
sub-frame node returns frame-local coords that don't compose the iframe offset,
so it lands wrong (verified — the same-origin probe came back isTrusted:false
via the coordinate fallback).

Fix: click on an in-iframe ref now focuses the element in its own frame session
and dispatches a real Enter (Space for checkbox-like roles) on the page session.
Chrome routes the key to the focused element across frames (same mechanism as
`type --focused`), and Enter/Space on a focused button/link/checkbox fires a
trusted click. Non-activatable roles fall back to DOM .click().

Adds e2e_iframe_button_click_is_trusted (+ fixture): an in-iframe button records
event.isTrusted into its own text; the test asserts the ref-click delivers
isTrusted:true.
2026-06-17 10:16:15 +09:00
9 changed files with 364 additions and 40 deletions
+1 -1
View File
@@ -290,7 +290,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrome-use"
version = "1.5.14"
version = "1.5.18"
dependencies = [
"aes",
"aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "chrome-use"
version = "1.5.14"
version = "1.5.18"
edition = "2021"
description = "Fast browser automation CLI for AI agents"
license = "Apache-2.0"
+157 -26
View File
@@ -235,6 +235,43 @@ fn prunable_target_ids(
.collect()
}
/// Consecutive missing `getTargets` snapshots before an owned relay tab is
/// pruned. >1 so a single churning/partial snapshot (other agents opening/closing
/// tabs) or a brief cross-process-nav gap can't drop the tab the agent is driving.
const RELAY_PRUNE_MISSES: u32 = 3;
/// Debounced prune for the relay: target ids to drop, mutating per-target miss
/// counters. A tab in `live_ids` resets to 0; an absent (non-pinned) tab
/// increments and is pruned only at `RELAY_PRUNE_MISSES`. Counters for
/// no-longer-tracked targets are forgotten. Pure, so the multi-agent churn
/// tolerance is unit-testable without a live browser.
fn debounced_prune_ids(
pages: &[PageInfo],
live_ids: &HashSet<String>,
pinned: Option<&str>,
misses: &mut HashMap<String, u32>,
) -> Vec<String> {
let tracked: HashSet<&str> = pages.iter().map(|p| p.target_id.as_str()).collect();
misses.retain(|tid, _| tracked.contains(tid.as_str()));
let mut prune = Vec::new();
for p in pages {
let tid = p.target_id.as_str();
if live_ids.contains(tid) {
misses.remove(tid);
continue;
}
if pinned == Some(tid) {
continue;
}
let c = misses.entry(p.target_id.clone()).or_insert(0);
*c += 1;
if *c >= RELAY_PRUNE_MISSES {
prune.push(p.target_id.clone());
}
}
prune
}
/// Whether the resolved active page is a tab the session created (its target_id
/// is in `created_targets`). Pure core of [`BrowserManager::active_is_session_owned`]
/// so the relay no-hijack rule is unit-testable without a live browser.
@@ -464,6 +501,14 @@ pub struct BrowserManager {
/// the session's commands onto the wrong page — the wrong-origin-fetch hazard
/// in the dogfood reports. Falls back to the index if the pinned tab is gone.
active_target_id: Option<String>,
/// Per-target count of CONSECUTIVE `resync_targets` snapshots in which an
/// owned tab was missing from `Target.getTargets`. Over the relay a single
/// snapshot routinely omits live tabs (multi-agent churn, a cross-process nav
/// briefly dropping the target), so we must not prune on one miss — that lost
/// the tab the agent was driving. A tab is removed only after it's been absent
/// for `RELAY_PRUNE_MISSES` consecutive snapshots; any snapshot that includes
/// it resets the counter. Keyed by stable target_id.
relay_target_misses: HashMap<String, u32>,
next_tab_id: u32,
/// Whether to enable the CDP `Runtime` domain (console / error / exception capture).
/// OFF by default for stealth: a live `Runtime.enable` is a detectable CDP signal
@@ -600,6 +645,7 @@ impl BrowserManager {
visited_origins: HashSet::new(),
created_targets: HashSet::new(),
active_target_id: None,
relay_target_misses: HashMap::new(),
next_tab_id: 1,
capture_console: console_capture_enabled(),
};
@@ -702,6 +748,7 @@ impl BrowserManager {
visited_origins: HashSet::new(),
created_targets: HashSet::new(),
active_target_id: None,
relay_target_misses: HashMap::new(),
next_tab_id: 1,
capture_console: console_capture_enabled(),
};
@@ -823,7 +870,19 @@ impl BrowserManager {
self.active_page_index = 0;
self.pin_active_target();
self.enable_domains(&attach_result.session_id).await?;
} else if self.agent_group().is_some() {
// STRICT MULTI-AGENT ISOLATION (relay / the user's real Chrome).
// `page_targets` here are the USER's and OTHER agents' tabs. A tab
// group belongs to exactly ONE agent, so this session must NOT adopt
// any of them — it tracks ONLY tabs it creates (its own colored group)
// plus popups it opens. Adopting foreign tabs is precisely what let
// another concurrent agent's tab churn drop the tab we were driving and
// drift eval/click onto the wrong page (multi-agent failure). Open our
// own dedicated background tab in the session's group and pin it; the
// user's / other agents' tabs stay invisible to us.
self.tab_new(None, None).await?;
} else {
// A browser WE launched: every tab is ours, so adopt them all.
for target in &page_targets {
let attach_result: AttachToTargetResult = self
.client
@@ -849,24 +908,10 @@ impl BrowserManager {
target_type: target.target_type.clone(),
});
}
if self.agent_group().is_some() {
// Relay: the adopted tabs above are the USER's, in their real
// Chrome. NEVER make one of them the agent's working tab — that is
// how commands drifted onto whatever page the user was viewing
// between steps (eval/click/get landed on the user's foreground
// tab; #35). Open our own dedicated background tab in the session's
// group and pin THAT as active. The user's tabs stay adopted (so
// `tab list` / explicit `tab switch` can reach them) but are never
// auto-selected — the agent only ever drives a tab it owns.
self.tab_new(None, None).await?;
} else {
// A browser we launched: every tab is ours, so the first is fine.
self.active_page_index = 0;
self.pin_active_target();
let session_id = self.pages[0].session_id.clone();
self.enable_domains(&session_id).await?;
}
self.active_page_index = 0;
self.pin_active_target();
let session_id = self.pages[0].session_id.clone();
self.enable_domains(&session_id).await?;
}
Ok(())
@@ -1071,6 +1116,17 @@ impl BrowserManager {
};
if let Some(ref error_text) = nav_result.error_text {
// `data:` URLs abort over the extension relay: chrome.debugger /
// chrome.tabs can't drive a top-frame data: navigation, so it comes
// back net::ERR_ABORTED on an about:blank tab. Explain it instead of
// leaking the cryptic code (data: works fine under `--launch`).
if url.starts_with("data:") && error_text.contains("ERR_ABORTED") {
return Err(format!(
"Navigation failed: {error_text}. Chrome blocks top-frame `data:` URLs over \
the extension relay — use a real http(s):// or file:// URL, or run with \
`--launch` (where data: URLs work)."
));
}
return Err(format!("Navigation failed: {}", error_text));
}
@@ -1515,6 +1571,18 @@ impl BrowserManager {
/// the active tab, per #7/#8.1); the caller surfaces it so the agent knows a
/// tab opened instead of seeing the old page (issue #24-A).
pub async fn adopt_newly_opened(&mut self, before: &HashSet<String>) -> Option<PageInfo> {
// STRICT MULTI-AGENT ISOLATION: on the relay this session's `before` set is
// only its OWN tabs, so EVERY foreign tab (the user's, other agents') looks
// "new" relative to it and would be adopted here — exactly the leak where a
// concurrent agent's tabs (github/Lark/iphone-use) showed up in this
// session mid-flow. A tab the agent itself opened (a pop-up) can't be
// distinguished from a foreign tab over the relay (no opener/window/group
// in the synthesized targetInfo), so don't adopt anything: the agent drives
// only tabs it explicitly created, and pop-ups (e.g. an OAuth/login window)
// are the user's. A launched browser (every tab ours) still follows pop-ups.
if self.agent_group().is_some() {
return None;
}
let result: GetTargetsResult = self
.client
.send_command_typed("Target.getTargets", &json!({}), None)
@@ -1557,6 +1625,11 @@ impl BrowserManager {
title: sanitize_title(&target.title),
target_type: target.target_type.clone(),
};
// A tab that appeared right after THIS session's action (a click that
// opened a popup/new tab) is ours — record it as owned so it's tracked,
// protected from churn-pruning, and cleaned up on close, consistent with
// strict multi-agent isolation (we only ever own tabs we created/opened).
self.created_targets.insert(target.target_id.clone());
self.add_background_page(page.clone());
let _ = self.enable_domains(&attach.session_id).await;
if opened.is_none() {
@@ -1584,13 +1657,19 @@ impl BrowserManager {
.filter(should_track_target)
.collect();
let live_ids: HashSet<String> = live.iter().map(|t| t.target_id.clone()).collect();
let on_relay = self.agent_group().is_some();
for target in &live {
if self.update_page_target_info(target) {
continue;
}
// A target this session hasn't tracked yet — attach and add it in the
// background so it's listable/adoptable without stealing the active tab.
// STRICT MULTI-AGENT ISOLATION: on the relay (the user's real Chrome,
// shared with other agents), NEVER adopt a tab this session didn't
// create — it belongs to the user or another agent's group. Only a
// browser we launched (every tab ours) adopts unknown targets.
if on_relay {
continue;
}
let attach_result: AttachToTargetResult = match self
.client
.send_command_typed(
@@ -1621,12 +1700,26 @@ impl BrowserManager {
let _ = self.enable_domains(&attach_result.session_id).await;
}
// Drop tabs that no longer exist so `tab list` doesn't show phantom rows —
// but never prune the explicitly-pinned active target on a transient
// getTargets snapshot (issue #31; see `prunable_target_ids`).
let gone = prunable_target_ids(&self.pages, &live_ids, self.active_target_id.as_deref());
for tid in gone {
self.remove_page_by_target_id(&tid);
// Prune tabs that are gone. On a LAUNCHED browser a missing target really
// is closed, so prune immediately. On the RELAY a single `getTargets`
// snapshot routinely omits live tabs (multi-agent churn, a brief
// cross-process-nav gap) — dropping the tab we're driving on one bad
// snapshot is the failure we're fixing — so prune only after the tab has
// been absent for several CONSECUTIVE snapshots (debounced). The pinned
// active target is protected either way (issue #31).
let gone = if on_relay {
debounced_prune_ids(
&self.pages,
&live_ids,
self.active_target_id.as_deref(),
&mut self.relay_target_misses,
)
} else {
prunable_target_ids(&self.pages, &live_ids, self.active_target_id.as_deref())
};
for tid in &gone {
self.relay_target_misses.remove(tid);
self.remove_page_by_target_id(tid);
}
// Refresh url/title from each live tab. The relay only stamps target_info
@@ -2536,6 +2629,7 @@ async fn initialize_lightpanda_manager(
visited_origins: HashSet::new(),
created_targets: HashSet::new(),
active_target_id: None,
relay_target_misses: HashMap::new(),
next_tab_id: 1,
capture_console: console_capture_enabled(),
};
@@ -2992,6 +3086,43 @@ mod tests {
);
}
#[test]
fn debounced_prune_tolerates_transient_churn() {
// Multi-agent churn: a single getTargets snapshot omits our owned tab "B"
// (another agent opened/closed tabs). It must NOT be pruned on one miss.
let pages = vec![page("A"), page("B")];
let mut misses = HashMap::new();
let empty: HashSet<String> = HashSet::new();
// Misses 1 and 2: B absent but under threshold → not pruned.
assert!(debounced_prune_ids(&pages, &empty, Some("A"), &mut misses).is_empty());
assert!(debounced_prune_ids(&pages, &empty, Some("A"), &mut misses).is_empty());
// Miss 3 (== RELAY_PRUNE_MISSES): genuinely gone → pruned.
assert_eq!(
debounced_prune_ids(&pages, &empty, Some("A"), &mut misses),
vec!["B".to_string()]
);
}
#[test]
fn debounced_prune_resets_on_reappearance_and_protects_pin() {
let pages = vec![page("A"), page("B")];
let mut misses = HashMap::new();
let empty: HashSet<String> = HashSet::new();
let mut live_b: HashSet<String> = HashSet::new();
live_b.insert("B".to_string());
// Two misses for B, then it reappears → counter resets, so it survives
// indefinitely under intermittent churn.
debounced_prune_ids(&pages, &empty, Some("A"), &mut misses);
debounced_prune_ids(&pages, &empty, Some("A"), &mut misses);
assert!(debounced_prune_ids(&pages, &live_b, Some("A"), &mut misses).is_empty());
assert!(debounced_prune_ids(&pages, &empty, Some("A"), &mut misses).is_empty()); // back to miss 1
// The pinned active "A" is never pruned no matter how many misses.
for _ in 0..5 {
let gone = debounced_prune_ids(&pages, &empty, Some("A"), &mut misses);
assert!(!gone.contains(&"A".to_string()));
}
}
#[test]
fn resolve_active_index_pin_survives_passive_background_tab() {
// A foreign tab ("Z") gets appended by passive discovery after we pinned
+71
View File
@@ -35,6 +35,7 @@ fn native_test_fixture_html(name: &str) -> &'static str {
"html5_drag_probe" => include_str!("test_fixtures/html5_drag_probe.html"),
"pointer_capture_probe" => include_str!("test_fixtures/pointer_capture_probe.html"),
"upload_probe" => include_str!("test_fixtures/upload_probe.html"),
"iframe_button_probe" => include_str!("test_fixtures/iframe_button_probe.html"),
_ => panic!("Unknown native test fixture: {}", name),
}
}
@@ -573,6 +574,76 @@ async fn e2e_snapshot_and_click_ref() {
assert_success(&resp);
}
/// Clicking a button INSIDE an iframe by `@ref` must deliver a TRUSTED activation
/// (`event.isTrusted === true`), not a synthetic DOM `.click()`. Security-sensitive
/// embedded forms (Google Payments' `保存`) reject `isTrusted:false` clicks, so an
/// enabled submit button silently no-op'd (issue #39). The fix routes iframe-ref
/// clicks to a real `Input.dispatchMouseEvent` on the element's own frame session.
/// The fixture's iframe button writes `clicked:<isTrusted>` into its own text on
/// click, which the cross-frame snapshot reads back.
#[tokio::test]
#[ignore]
async fn e2e_iframe_button_click_is_trusted() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": native_test_fixture_url("iframe_button_probe") }),
&mut state,
)
.await;
assert_success(&resp);
// Snapshot (interactive) — the button lives in the iframe and must appear with
// a ref; that ref carries the frame_id so the click resolves into the frame.
let resp = execute_command(
&json!({ "id": "3", "action": "snapshot", "interactive": true }),
&mut state,
)
.await;
assert_success(&resp);
let snapshot = get_data(&resp)["snapshot"].as_str().unwrap_or("");
let ref_id = snapshot
.lines()
.find(|l| l.contains("button \"save\""))
.and_then(|l| l.split("ref=").nth(1))
.map(|r| r.trim_end_matches(']').trim())
.unwrap_or_else(|| panic!("iframe button not found in snapshot:\n{snapshot}"));
// Click it by ref.
let resp = execute_command(
&json!({ "id": "4", "action": "click", "selector": ref_id }),
&mut state,
)
.await;
assert_success(&resp);
tokio::time::sleep(tokio::time::Duration::from_millis(300)).await;
// The button rewrote its own text with the click's isTrusted flag; read it
// back across frames.
let resp = execute_command(
&json!({ "id": "5", "action": "snapshot", "interactive": true }),
&mut state,
)
.await;
assert_success(&resp);
let after = get_data(&resp)["snapshot"].as_str().unwrap_or("");
assert!(
after.contains("clicked:true"),
"iframe button click must be trusted (isTrusted:true); snapshot:\n{after}"
);
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
// ---------------------------------------------------------------------------
// Screenshot
// ---------------------------------------------------------------------------
+90 -10
View File
@@ -56,19 +56,34 @@ pub async fn click(
.await;
}
// Over the extension relay we drive the user's real, in-use Chrome, where a
// coordinate `Input.dispatchMouseEvent` is NOT reliably confined to our target
// tab — it can be delivered to whatever tab is in the foreground, and an OOPIF
// element's box can't be mapped to a top-viewport point at all. This twice
// opened an unrelated tab on the user's busy Chrome (issues #31/#36). So on the
// relay, never use coordinates for a normal left click: DOM-dispatch invokes
// the element's click in its own (frame) session, always hitting the right
// element in the right tab. Double/right clicks still need true pointer
// semantics, and `coord` mode is an explicit opt-out.
// An element INSIDE an iframe needs a TRUSTED activation: a DOM `.click()` is
// `isTrusted:false`, which security-sensitive embedded forms reject — Google
// Payments' enabled `保存` button silently no-ops on a synthetic click (issue
// #39). A coordinate `Input.dispatchMouseEvent` can't help either: `getBoxModel`
// for a sub-frame node returns frame-local coordinates that don't compose the
// iframe's offset, so the click lands in the wrong place. The frame-agnostic
// trusted path is keyboard activation — focus the element in its own frame, then
// dispatch a real Enter on the page session; Chrome routes the key to the
// focused element regardless of frame (same as `type --focused`), and Enter on a
// focused button/link fires a trusted `click`. `coord` mode opts out.
let in_iframe = ref_map.ref_is_in_iframe(selector_or_ref);
if mode != "coord" && button == "left" && click_count == 1 && in_iframe {
return dom_activate(
client,
session_id,
ref_map,
selector_or_ref,
iframe_sessions,
)
.await;
}
// On the relay (the user's real Chrome) a TOP-document coordinate click used to
// drift onto the foreground tab; that root cause is fixed (#5: the agent drives
// its own pinned tab), but DOM-dispatch stays the conservative default here.
if mode != "coord"
&& button == "left"
&& click_count == 1
&& prefer_dom_dispatch(ref_map, selector_or_ref)
&& crate::connect::relay_url().is_some()
{
return dom_click(
client,
@@ -270,6 +285,71 @@ async fn dom_click(
Ok(())
}
/// Trusted activation of an element inside an iframe (issue #39). Focuses the
/// element in its own frame session, then dispatches a real Enter/Space on the
/// page session — Chrome routes the key to the focused element across frames, and
/// Enter/Space on a focused button/link/checkbox fires a `click` with
/// `isTrusted: true`, which security-sensitive embedded forms (Google Payments
/// `保存`) require. Non-activatable roles (a `div[onclick]`) can't be keyboard-
/// activated, so they fall back to a DOM `.click()`.
async fn dom_activate(
client: &CdpClient,
session_id: &str,
ref_map: &RefMap,
selector_or_ref: &str,
iframe_sessions: &HashMap<String, String>,
) -> Result<(), String> {
let role = parse_ref(selector_or_ref)
.and_then(|r| ref_map.get(&r).map(|e| e.role.clone()))
.unwrap_or_default();
// Space toggles checkbox-like controls; Enter activates buttons/links/menus.
let key = match role.as_str() {
"checkbox" | "radio" | "switch" | "option" | "menuitemcheckbox" | "menuitemradio" => {
Some("space")
}
"button" | "link" | "menuitem" | "tab" | "treeitem" => Some("enter"),
_ => None,
};
let Some(key) = key else {
// Not keyboard-activatable — best effort via DOM .click() (untrusted).
return dom_click(
client,
session_id,
ref_map,
selector_or_ref,
iframe_sessions,
)
.await;
};
let (object_id, effective_session_id) = resolve_element_object_id(
client,
session_id,
ref_map,
selector_or_ref,
iframe_sessions,
)
.await?;
// Focus the element in its OWN frame session so the keystroke lands on it.
client
.send_command_typed::<_, Value>(
"Runtime.callFunctionOn",
&CallFunctionOnParams {
function_declaration: "function() { this.focus(); }".to_string(),
object_id: Some(object_id),
arguments: None,
return_by_value: Some(true),
await_promise: Some(false),
},
Some(&effective_session_id),
)
.await?;
// Trusted key on the page session — routed to the focused (in-frame) element.
press_key(client, session_id, key).await?;
wait_for_paint_settled(client, &effective_session_id).await;
Ok(())
}
/// DOM-dispatch a double-click on the element in its own session (no coordinates)
/// — the relay/iframe-safe counterpart to a coordinate dblclick. Fires the full
/// click,click,dblclick sequence so handlers bound to any of them respond.
@@ -0,0 +1,28 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>iframe button probe</title>
</head>
<body>
<h1>iframe button probe</h1>
<iframe
id="frame"
width="320"
height="140"
srcdoc="
<!doctype html>
<html>
<body style='margin:24px'>
<button id='b' style='padding:24px;font-size:22px'>save</button>
<script>
document.getElementById('b').addEventListener('click', function (e) {
this.textContent = 'clicked:' + e.isTrusted;
});
</script>
</body>
</html>
"
></iframe>
</body>
</html>
+4
View File
@@ -3577,6 +3577,9 @@ iOS Simulator (requires Xcode and Appium):
chrome-use -p ios device list # List simulators
chrome-use -p ios swipe up # Swipe gesture
chrome-use -p ios tap @e1 # Touch element
Hit a bug or rough edge? A 30-second issue genuinely sharpens this tool:
https://github.com/leeguooooo/chrome-use/issues
"#
);
}
@@ -3659,6 +3662,7 @@ fn print_screenshot_diff(data: &serde_json::Map<String, serde_json::Value>) {
pub fn print_version() {
println!("chrome-use {}", env!("CARGO_PKG_VERSION"));
println!("report bugs / rough edges: https://github.com/leeguooooo/chrome-use/issues");
}
#[cfg(test)]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "chrome-use",
"version": "1.5.14",
"version": "1.5.18",
"description": "chrome-use — drive your real, logged-in Chrome from any AI agent, stealth by default",
"type": "module",
"packageManager": "pnpm@11.1.3",
+11 -1
View File
@@ -131,7 +131,17 @@ Each `--session` that connects gets its **own colored Chrome tab group** (named
after the session) and drives only its own tabs — multiple agents share the one
real browser without cross-talk, and the user's own tabs are never grouped. CDP
drives the page without moving the user's mouse/keyboard, so it doesn't fight
them for control. **Anti-detection ranking: this real logged-in Chrome (extension
them for control.
**Strict multi-agent isolation.** A session over the relay tracks and drives
**only the tabs it created** (its own group). It does **not** adopt the user's
existing tabs, other agents' tabs, or pop-ups (e.g. an OAuth/login window — that's
the user's), so several agents (and other tools opening tabs) can work in the same
real Chrome concurrently without ever dropping or stealing each other's tabs —
another agent's tab churn can't make your bound tab vanish or drift your commands
onto the wrong page. Consequence: `tab list` shows only *your* session's tabs; to
drive a specific page, navigate to it in your own tab instead of expecting a
pre-existing or popped-up tab to appear in the list. **Anti-detection ranking: this real logged-in Chrome (extension
connect) > a headed launched browser > headless (forbidden).** A genuine human
browser has no headless/automation tells at all, so prefer it for anything
anti-bot-sensitive.