From 284a60a54c022aa72329e2a5e0dfd34db4361ba4 Mon Sep 17 00:00:00 2001 From: leeguooooo Date: Wed, 17 Jun 2026 21:18:01 +0900 Subject: [PATCH] feat(adopt): read a pre-existing tab without opening a new one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New `chrome-use adopt `: drive a tab the user (or another session) already has open, with ZERO new tabs. After group-scoped isolation (#40) a session can't see foreign tabs, so adopt adds an explicit, opt-in path: - Relay (relay.rs): `ABRelay.getAllTargets` returns every attached target UNSCOPED (ignores group scoping), so the agent can find a specific tab by URL or targetId. +1 unit test. - Daemon (browser.rs): `collect_all_targets` (unscoped, falls back to scoped on older relays) + `adopt_existing_target` — matches by exact targetId or case-insensitive URL substring, attaches it (the relay re-tags it into the adopter's group, so isolation holds), pins it; never creates a tab. On no match it errors AND lists the open tabs it can see, rather than launching. discover_and_attach_targets honors AGENT_BROWSER_ADOPT at first connect, so no about:blank is ever created. - CLI (main.rs): `adopt` sets the env, forces a fresh daemon, and rewrites into `connect ` (like `extension connect`) so the daemon attaches to the user's real Chrome before parse_command. Extension (ab-connect 0.4.11): `reannounceAttachedTabs` now re-sends each tab's url/title (it previously sent neither) so the relay's target list stays matchable by URL after the MV3 service worker reconnects — otherwise reannounced tabs show a blank url and `adopt ` can't find them. Repacked upload zip + crx. Mechanism verified live (enumerated all 11 of the user's open tabs incl. the target). 862 tests pass. --- cli/Cargo.lock | 2 +- cli/Cargo.toml | 2 +- cli/src/commands.rs | 7 ++ cli/src/connection.rs | 2 +- cli/src/main.rs | 37 ++++++++++ cli/src/native/browser.rs | 111 ++++++++++++++++++++++++++++ cli/src/native/relay.rs | 41 ++++++++++ cli/src/output.rs | 4 + extensions/ab-connect.crx | Bin 33442 -> 33585 bytes extensions/ab-connect.zip | Bin 32871 -> 33012 bytes extensions/ab-connect/background.js | 15 +++- extensions/ab-connect/manifest.json | 2 +- package.json | 2 +- 13 files changed, 218 insertions(+), 7 deletions(-) diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 440e1fe..c6d0840 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -290,7 +290,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrome-use" -version = "1.5.22" +version = "1.5.23" dependencies = [ "aes", "aes-gcm", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 17764de..e9390ed 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chrome-use" -version = "1.5.22" +version = "1.5.23" edition = "2021" description = "Fast browser automation CLI for AI agents" license = "Apache-2.0" diff --git a/cli/src/commands.rs b/cli/src/commands.rs index dfb5cdb..ae0071d 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -80,6 +80,7 @@ const KNOWN_COMMANDS: &[&str] = &[ "upload", "site", "box", + "adopt", ]; /// Levenshtein distance, capped — small inputs only (command names). @@ -1317,6 +1318,12 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result`: the adoption happens at daemon connect (driven by + // the AGENT_BROWSER_ADOPT env main.rs set + a forced-fresh daemon), so by + // the time this command runs the tab is already attached. Resolve to a + // `url` read so the response confirms which tab got adopted. + "adopt" => Ok(json!({ "id": id, "action": "url" })), + // === Stealth self-check === "stealth" => { // `stealth [status]` — local stealth self-check: mode, live probes diff --git a/cli/src/connection.rs b/cli/src/connection.rs index af4c247..f4c63b6 100644 --- a/cli/src/connection.rs +++ b/cli/src/connection.rs @@ -595,7 +595,7 @@ fn query_current_url(session: &str) -> Option { } /// Kill a running daemon by reading its PID file and sending a kill signal. -fn kill_stale_daemon(session: &str) { +pub fn kill_stale_daemon(session: &str) { // Remove the socket first so no new connections reach the old daemon #[cfg(unix)] { diff --git a/cli/src/main.rs b/cli/src/main.rs index da91365..b3c051c 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -981,6 +981,43 @@ fn main() { } } + // `adopt `: read a PRE-EXISTING tab (the user's own, or another + // session's) WITHOUT opening a new one. Forces a fresh daemon and points it at + // the relay (like `extension connect`); the AGENT_BROWSER_ADOPT env makes the + // daemon's first connect ADOPT the matching tab instead of creating an + // about:blank. Rewrites into `connect ` BEFORE parse_command so the + // daemon attaches to the user's real Chrome. Must run before parse_command. + if clean.first().map(|s| s.as_str()) == Some("adopt") { + match clean.get(1) { + Some(spec) if !spec.trim().is_empty() => { + std::env::set_var("AGENT_BROWSER_ADOPT", spec.trim()); + connection::kill_stale_daemon(&flags.session); + match connect::relay_url() { + Some(url) => { + flags.cdp = Some(url.clone()); + flags.auto_connect = false; + clean = vec!["connect".to_string(), url]; + } + None => { + eprintln!( + "{} extension relay not connected — open Chrome with the ab-connect \ + extension first (this command reads an EXISTING tab, it won't launch one).", + color::error_indicator() + ); + exit(1); + } + } + } + _ => { + eprintln!( + "{} usage: chrome-use adopt (reads an existing tab, no new tab)", + color::error_indicator() + ); + exit(2); + } + } + } + // Handle session separately (doesn't need daemon) if clean.first().map(|s| s.as_str()) == Some("session") { run_session(&clean, &flags.session, flags.json); diff --git a/cli/src/native/browser.rs b/cli/src/native/browser.rs index 965f6cd..a32ba2c 100644 --- a/cli/src/native/browser.rs +++ b/cli/src/native/browser.rs @@ -824,6 +824,106 @@ impl BrowserManager { Ok(by_id.into_values().collect()) } + /// Every tab the relay knows, UNSCOPED (ignores group scoping) — for explicit + /// cross-group adoption (`chrome-use adopt`). Falls back to the scoped + /// `collect_page_targets` on a relay/browser that doesn't support the + /// unscoped query. Retries a few times over the relay (discovery is eventual). + async fn collect_all_targets(&self) -> Result, String> { + let rounds = if crate::connect::relay_url().is_some() { + 3 + } else { + 1 + }; + let mut by_id: HashMap = HashMap::new(); + let mut any_ok = false; + for i in 0..rounds { + if i > 0 { + tokio::time::sleep(Duration::from_millis(150)).await; + } + if let Ok(result) = self + .client + .send_command_typed::<_, GetTargetsResult>( + "ABRelay.getAllTargets", + &json!({}), + None, + ) + .await + { + any_ok = true; + for t in result.target_infos.into_iter().filter(should_track_target) { + by_id.entry(t.target_id.clone()).or_insert(t); + } + } + } + if any_ok { + Ok(by_id.into_values().collect()) + } else { + // Older relay without ABRelay.getAllTargets → best-effort scoped list. + self.collect_page_targets().await + } + } + + /// Adopt a specific pre-existing tab matched by `spec` (an exact CDP + /// `targetId`, or a case-insensitive substring of the tab URL) WITHOUT opening + /// a new tab — for `chrome-use adopt`. Attaches it (the relay tags it into our + /// group), tracks + pins it. Errors if nothing matches (never creates a tab). + async fn adopt_existing_target(&mut self, spec: &str) -> Result<(), String> { + let all = self.collect_all_targets().await?; + let spec_l = spec.to_lowercase(); + let target = all + .iter() + .find(|t| t.target_id == spec) + .or_else(|| all.iter().find(|t| t.url.to_lowercase().contains(&spec_l))) + .ok_or_else(|| { + let mut open: Vec = all + .iter() + .map(|t| { + let u = if t.url.len() > 80 { + &t.url[..80] + } else { + &t.url + }; + u.to_string() + }) + .collect(); + open.sort(); + open.dedup(); + format!( + "adopt: no open tab matching `{spec}` (by targetId or URL substring).\n\ + {} tab(s) the extension can see:\n {}", + open.len(), + open.join("\n ") + ) + })? + .clone(); + + let attach: AttachToTargetResult = self + .client + .send_command_typed( + "Target.attachToTarget", + &AttachToTargetParams { + target_id: target.target_id.clone(), + flatten: true, + }, + None, + ) + .await?; + let tab_id = self.assign_tab_id(); + self.pages.push(PageInfo { + tab_id, + label: None, + target_id: target.target_id.clone(), + session_id: attach.session_id.clone(), + url: target.url.clone(), + title: sanitize_title(&target.title), + target_type: target.target_type.clone(), + }); + self.active_page_index = self.pages.len() - 1; + self.pin_active_target(); + self.enable_domains(&attach.session_id).await?; + Ok(()) + } + async fn discover_and_attach_targets(&mut self) -> Result<(), String> { self.client .send_command_typed::<_, Value>( @@ -837,6 +937,17 @@ impl BrowserManager { // own tab group (issue #40). On a launched browser this is a no-op. let scoped = self.announce_group().await; + // `chrome-use adopt `: adopt a specific PRE-EXISTING tab instead of + // creating one — true zero-new-tab reading of the user's own tab. The + // directive rides in via env so it takes effect at first connect (before + // any about:blank would be made). If nothing matches, error out rather + // than fall back to creating a tab. + if let Ok(spec) = std::env::var("AGENT_BROWSER_ADOPT") { + if !spec.trim().is_empty() { + return self.adopt_existing_target(spec.trim()).await; + } + } + let page_targets: Vec = self.collect_page_targets().await?; if page_targets.is_empty() { diff --git a/cli/src/native/relay.rs b/cli/src/native/relay.rs index 3bbf41f..6339718 100644 --- a/cli/src/native/relay.rs +++ b/cli/src/native/relay.rs @@ -155,6 +155,20 @@ impl RelayState { "Target.setDiscoverTargets" | "Target.setAutoAttach" => { ClientRoute::Local(json!({ "id": id, "result": {} })) } + // Unscoped discovery for EXPLICIT cross-group adoption (`chrome-use + // adopt`): returns every target the extension has attached, ignoring + // group scoping, so an agent can find a specific pre-existing tab (the + // user's, another session's) by URL/targetId and adopt it. Isolation + // is preserved because the daemon only acts on the one tab it then + // attaches (which the relay re-tags into the adopter's group). + "ABRelay.getAllTargets" => { + let infos: Vec = self + .targets + .values() + .map(|t| t.target_info.clone()) + .collect(); + ClientRoute::Local(json!({ "id": id, "result": { "targetInfos": infos } })) + } "Target.getTargets" => { // Scope to the client's own group when it announced one; an // un-announced (legacy) client gets the full list (back-compat). @@ -772,6 +786,33 @@ mod tests { assert!(get_target_ids(&mut s, 2).is_empty()); } + #[test] + fn get_all_targets_is_unscoped() { + let mut s = RelayState::new(); + create_in_group(&mut s, 1, "agent-a", "ta", "sa"); + create_in_group(&mut s, 2, "agent-b", "tb", "sb"); + // Client 1's scoped getTargets sees only its own group... + assert_eq!(get_target_ids(&mut s, 1), vec!["ta"]); + // ...but ABRelay.getAllTargets returns EVERY target regardless of group + // (for explicit cross-group adoption). + let all = match s + .route_client_command(1, &json!({ "id": 1, "method": "ABRelay.getAllTargets" })) + { + ClientRoute::Local(v) => { + let mut ids: Vec = v["result"]["targetInfos"] + .as_array() + .unwrap() + .iter() + .map(|t| t["targetId"].as_str().unwrap().to_string()) + .collect(); + ids.sort(); + ids + } + _ => panic!("getAllTargets must be local"), + }; + assert_eq!(all, vec!["ta", "tb"]); + } + #[test] fn detach_clears_target_group() { let mut s = RelayState::new(); diff --git a/cli/src/output.rs b/cli/src/output.rs index b0f257b..580139c 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -3375,6 +3375,10 @@ Tabs: stable targetId, no reload — preserves in-page state open --reuse-tab Reuse an existing tab on that URL instead of spawning a duplicate (matches origin+path; preserves state) + adopt Read a PRE-EXISTING tab (the user's own, or another + session's) WITHOUT opening a new one — matches by URL + substring or stable targetId, then drives it. e.g. + `adopt "github.com/owner/repo"` Diff: diff snapshot Compare current vs last snapshot diff --git a/extensions/ab-connect.crx b/extensions/ab-connect.crx index 308338cc237f94a8461f79bd5808643666b2c596..36eb4a21834347b01623bc58c9396856be78021d 100644 GIT binary patch delta 8302 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 delta 8158 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 delta 2626 zcmZuzc{tQ<7ycPbwuVWXnMh+z4aSTuhU{fcM55vy`&ee|>zJ`5ntCO}C`-0Vk*G0> zEO}8r#$6HXg1aS#VKgSQUy3d*kq`!>7NTaB)rIf6JueqMqOJsw*BWok zVyD;DeoOO1#afJ-VfMQ$kn=(euHQQs_H{xltLAI^O<*9}pqx)Hc~ML8Jh3BP+WRiJ z-sRv9(L&*iOq#Gk_2Fw(`}iEqoZfd_k$Y_7Br*_15pn9r%00&p!k2~?NrF0hXr3ss zjIU4)wOw((CwbPY#SR*pjpUZR?95P*{)5KyJU4r|U0O}z3wB8qJaEqGp~&hFNJr|` zMpd%igp);X=7f<0ee}9gKfd4SB>KmWZ=c_y7RSu1;(W9dQ|(>}swJOhX!CJpFtSOD z>P!V03QXeRi+TK7`(x5u{mCa7Gc$S7D%of+h62Kedhk!%LSfH>IaK}7a{+bTVD0XU zv7|numUb{Qy1WQ4e!g&(Am6@y$etCQfevo0#QUi}wlY)tv=c5{M7!-bFG_8`$9}P` zX12ShthC>!`D6~1?RjR)-sWnh6omi0!eickQKO<&ZA}Y zIZa$_k$!XqzfgV)qBNLqZ($6u&B|o%+jGl?nH{<8BG#^IQi&eL#yd>duDGY*F?1zg z=VN`)l+m{2egZ74GrC+}V<78kg&v(ZN)j3M(jR*-}=Lht=t_)5DUroI&Sl3AFGHDsvEi>-f|%DghSgv*LLW{%~)CC4>?f1rt{(n1J$Y zD_pRVc@k1=C*Z#ec?GHRb#gM`Op!NbY3L-=vaAB9of`RO1(8x(vF`U`kUT$LtomVk zTUD!7?p(v1dtqst3+dXS$vqhUkWklE$#mKUQ#FU) zcM%seN@`|Hy=I+D+uwzP-4H{Bwa}=}|^u`UZGTu zzMS1r^1Ar@q&jKQyn>DuB=1iZgYQpma%_@K!sBx2Sl0?qr;2 z{0?E&LxaBS1#`g^?3nRLdWt|(sYv9SR4T=>D?9BbqA@la7<2RXUsFS{tloD0y)?aS z-JLz$#*_Cx^SyiLNPTU6D{ADvL(`PK{A4BMdF-m-NVtuMqT90s(;|vp)*)o3!R7Lp ztVUVQqR8Q5ahmF`TJ6QSnSMr^75QF;zSQd&3H=?PjHY9B#8TwO6LGmYnzvK}-XPz< z|6a_6OzaKx4{Dn&VUhHHR1Y?5DBl63%yHEHWYAXWp>@kh|I!}D7nuZJ5ImhmR^0Oo zq7^DiaF7PYeVb0xigXYMNfF#gA5*L6Y;JrLULN^4CNZ2BAw3=uY%Ca+=N4ll)wdSR>rCB}^UJ>5m zd7saw8QCg_4}YuPHqA5TX1%N1#HSRYz&m)#UR4N8y(kjaTIb*HOHRTDz7QNE_DDox zLep7PsWyQ``5oHH1O@d0k87l)Xcr9=ANK7G9*xBPU1qvc2JE)Dj^nVYyh^IV<%I)C znTeudinao}oNlh0h?+d5Y*`$o^5@4QvKMseO2CPi{*2uWhwsU$1{1Qe&rTFs%Ac;a z#wU)K7ER9tG#(j1s&tHOt8ciDkgGAw+1i`o1$+h%xQ9x?Z7 zU|gl&=f2b&rX~3_eug)31~Mb9<9w;CJ-?~%ndW%1V?VDzGdbg()C$Qp;7H=)DOBCJ zPd4yOn0nLobjggE3Xf~p8Y?o_Z`0jPLTnDsfE3Kd8AVmBG6>|gFjqkqW%?ki2+W|ljfNkt70I)GO;Cj5a3;+ZY1H%GUd_n{K9qf5ITggvm zEJYB-iT&yP>*D(r3jm_PjN$k`0KmHw2_dMhaT^tJYMduaa>>!a6xe1h8+_UvzI;b- z1Xc(DI7jrqe4JV)4{Y*rSk(`z1QNWrGH)<&nDZ!-k_64Iu4v5v^Zl9aiHFHq2p<4& z&hOc>L$*?=Vg8+MqYnX2y+8jr%{ERPwiE>b|N7zod#+z?=fz3xjE@0;qc-;D#-=I( Q9$On2jsx?-V>xg6A1{JtqW}N^ diff --git a/extensions/ab-connect/background.js b/extensions/ab-connect/background.js index d2d7591..82edb7f 100644 --- a/extensions/ab-connect/background.js +++ b/extensions/ab-connect/background.js @@ -471,8 +471,19 @@ async function reannounceAttachedTabs() { for (const [tabId, entry] of tabs.entries()) { // Re-send the group hint too (issue #40) so the relay can rebuild its // targetId→group map after its own restart (createTarget tagging won't - // re-run for tabs that are already open). + // re-run for tabs that are already open). Include the live url/title so the + // relay's target list stays matchable by URL after a reconnect (otherwise a + // reannounced tab shows a blank url and `adopt ` can't find it). const { openerTargetId, abGroup } = await tabScopeHints(tabId) + let url = '' + let title = '' + try { + const t = await chrome.tabs.get(tabId) + if (t) { + url = t.url || t.pendingUrl || '' + title = t.title || '' + } + } catch {} postToHost({ method: 'forwardCDPEvent', params: { @@ -480,7 +491,7 @@ async function reannounceAttachedTabs() { method: 'Target.attachedToTarget', params: { sessionId: entry.sessionId, - targetInfo: { targetId: entry.targetId, type: 'page', attached: true, openerTargetId, abGroup }, + targetInfo: { targetId: entry.targetId, type: 'page', url, title, attached: true, openerTargetId, abGroup }, }, }, }) diff --git a/extensions/ab-connect/manifest.json b/extensions/ab-connect/manifest.json index b492c0e..81d1283 100644 --- a/extensions/ab-connect/manifest.json +++ b/extensions/ab-connect/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "chrome-use", - "version": "0.4.10", + "version": "0.4.11", "description": "Let chrome-use drive your logged-in Chrome \u2014 install once, no token, no per-use confirmation.", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6vQIyscGIPYPZdSpPwPL0+0gxUROyRgCpmvCSDoc8XUm4qm97VbKnD9Ijc1lV22lNWZtE78gaRjt6BeSfuMgnBymnhLKjN1gU6AI5QUU0mrJyeHdWKvrKQR5FmsM2A7Xr1ykE2SiiS8zNUS3Y/6O5l+Nva7wrVy6E4a2dkBVQkOsu+DV+nEZvhIyuDY5D5SPXqNwUTWTaglwj5mjvHz36xSwCWlPmrtJ+ED0AUyrb2z4GIOmvk4kqtBVrh/UD058klLo4CkYOnIybB5aV6WYuwarfPY4bF/dLggPem+ewLNTUNBuwrxj/A4nUv0LJTuRO8rR7f8WR9qnRCY0Ic5saQIDAQAB", "icons": { diff --git a/package.json b/package.json index b8b38e8..1f25e98 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chrome-use", - "version": "1.5.22", + "version": "1.5.23", "description": "chrome-use — drive your real, logged-in Chrome from any AI agent, stealth by default", "type": "module", "packageManager": "pnpm@11.1.3",