Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc2aa4cade | ||
|
|
7085f3bf36 | ||
|
|
29815ff5f3 | ||
|
|
2a338d4c29 |
Generated
+1
-1
@@ -290,7 +290,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chrome-use"
|
||||
version = "1.5.4"
|
||||
version = "1.5.6"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "chrome-use"
|
||||
version = "1.5.4"
|
||||
version = "1.5.6"
|
||||
edition = "2021"
|
||||
description = "Fast browser automation CLI for AI agents"
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -449,6 +449,47 @@ pub fn relay_url() -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Append a one-line record of how a CDP connection was established, to
|
||||
/// `~/.chrome-use/connect-mode.log`. This is the smoking-gun detector for the
|
||||
/// "Allow remote debugging?" consent modal: that modal ONLY appears on a raw
|
||||
/// remote-debugging attach / a browser we launched with a debug port — NEVER on
|
||||
/// the extension relay. When the modal reappears, this log says which session
|
||||
/// took which path and when, so we can tell a code regression (`raw-port` /
|
||||
/// `launched` while the relay was up) from Chrome's own extension-debugger
|
||||
/// consent UX. Low volume (one line per connection); best-effort, never fails a
|
||||
/// connection.
|
||||
pub fn log_connect_mode(ws_url: &str, launched: bool, session: &str) {
|
||||
let relay = relay_url();
|
||||
let relay_up = relay.is_some();
|
||||
let mode = if launched {
|
||||
"launched(debug-port)"
|
||||
} else if relay.as_deref() == Some(ws_url) {
|
||||
"relay"
|
||||
} else if ws_url.contains("127.0.0.1") || ws_url.contains("localhost") {
|
||||
"raw-port-attach"
|
||||
} else {
|
||||
"remote-ws"
|
||||
};
|
||||
// A raw-port attach or a self-launch while the relay was available is the
|
||||
// exact thing that pops the consent modal — flag it loudly in the line.
|
||||
let suspect = (mode == "raw-port-attach" || launched) && relay_up;
|
||||
let line = format!(
|
||||
"session={session} mode={mode} relay_up={relay_up}{} ws={ws_url}\n",
|
||||
if suspect { " CONSENT-MODAL-RISK" } else { "" }
|
||||
);
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
let path = home.join(".chrome-use").join("connect-mode.log");
|
||||
use std::io::Write;
|
||||
if let Ok(mut f) = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
{
|
||||
let _ = f.write_all(line.as_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sidecar recording the connected extension's version, written by the host when
|
||||
/// it receives the extension's `hello` (sibling of `relay-cdp-url`). Lets
|
||||
/// `doctor` surface which extension build is live without a CDP round-trip.
|
||||
|
||||
@@ -166,6 +166,27 @@ fn resolve_active_index(
|
||||
active_page_index
|
||||
}
|
||||
|
||||
/// Target ids to prune after a `Target.getTargets` resync: tracked pages whose
|
||||
/// target is no longer in the live set — EXCEPT the explicitly-pinned active
|
||||
/// target, which is protected. The relay against a busy real Chrome occasionally
|
||||
/// returns a different window's tabs for a single `getTargets` call ("tab list
|
||||
/// hops windows", issue #31); pruning on that transient snapshot would drop the
|
||||
/// agent's adopted tab and drift subsequent eval/click onto a foreign tab. A
|
||||
/// genuine close still arrives as `Target.targetDestroyed` (handled in the event
|
||||
/// drain), which removes the pin properly — so protecting it here only guards
|
||||
/// against flaky snapshots, not real closures.
|
||||
fn prunable_target_ids(
|
||||
pages: &[PageInfo],
|
||||
live_ids: &HashSet<String>,
|
||||
pinned: Option<&str>,
|
||||
) -> Vec<String> {
|
||||
pages
|
||||
.iter()
|
||||
.map(|p| p.target_id.clone())
|
||||
.filter(|tid| !live_ids.contains(tid) && pinned != Some(tid.as_str()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -490,6 +511,13 @@ impl BrowserManager {
|
||||
}
|
||||
};
|
||||
|
||||
// A launched browser carries a debug port → it's the other path that can
|
||||
// pop Chrome's consent modal; record it for #31 diagnosis.
|
||||
crate::connect::log_connect_mode(
|
||||
&ws_url,
|
||||
true,
|
||||
DAEMON_SESSION.get().map(String::as_str).unwrap_or("default"),
|
||||
);
|
||||
let manager = if engine == "lightpanda" {
|
||||
initialize_lightpanda_manager(ws_url, process).await?
|
||||
} else {
|
||||
@@ -585,6 +613,13 @@ impl BrowserManager {
|
||||
headers: Option<Vec<(String, String)>>,
|
||||
) -> Result<Self, String> {
|
||||
let ws_url = resolve_cdp_url(url).await?;
|
||||
// Record the transport so a reappearing "Allow remote debugging?" modal
|
||||
// can be traced to a raw-port attach vs the consent-free relay (#31).
|
||||
crate::connect::log_connect_mode(
|
||||
&ws_url,
|
||||
false,
|
||||
DAEMON_SESSION.get().map(String::as_str).unwrap_or("default"),
|
||||
);
|
||||
let client = Arc::new(CdpClient::connect_with_headers(&ws_url, headers).await?);
|
||||
let mut manager = Self {
|
||||
client,
|
||||
@@ -1411,13 +1446,10 @@ 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.
|
||||
let gone: Vec<String> = self
|
||||
.pages
|
||||
.iter()
|
||||
.map(|p| p.target_id.clone())
|
||||
.filter(|tid| !live_ids.contains(tid))
|
||||
.collect();
|
||||
// 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);
|
||||
}
|
||||
@@ -2582,6 +2614,25 @@ mod tests {
|
||||
assert!(!active_index_is_owned(&[], None, 0, &created));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prune_protects_pinned_target_on_transient_snapshot() {
|
||||
// The relay returned a getTargets snapshot missing the pinned tab "A"
|
||||
// (it hopped to another window). "B" is also absent. Without protection
|
||||
// both would be pruned and the next command would drift; with the pin
|
||||
// protected, only the genuinely-unpinned "B" is dropped (issue #31).
|
||||
let pages = vec![page("A"), page("B")];
|
||||
let live: HashSet<String> = HashSet::new(); // snapshot returned neither
|
||||
let gone = prunable_target_ids(&pages, &live, Some("A"));
|
||||
assert_eq!(gone, vec!["B".to_string()]);
|
||||
// With no pin, both are prunable (unchanged behavior).
|
||||
let gone_unpinned = prunable_target_ids(&pages, &live, None);
|
||||
assert_eq!(gone_unpinned.len(), 2);
|
||||
// A pinned target that IS in the live set is simply not prunable anyway.
|
||||
let mut live2 = HashSet::new();
|
||||
live2.insert("A".to_string());
|
||||
assert_eq!(prunable_target_ids(&pages, &live2, Some("A")), vec!["B".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_active_index_pin_survives_passive_background_tab() {
|
||||
// A foreign tab ("Z") gets appended by passive discovery after we pinned
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chrome-use",
|
||||
"version": "1.5.4",
|
||||
"version": "1.5.6",
|
||||
"description": "chrome-use — drive your real, logged-in Chrome from any AI agent, stealth by default",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@11.1.3",
|
||||
|
||||
Reference in New Issue
Block a user