From 6c0f5cbaa16006f7dc8eb14bc94c981bcf59a131 Mon Sep 17 00:00:00 2001 From: leeguooooo Date: Tue, 9 Jun 2026 17:11:59 +0900 Subject: [PATCH] feat(connect): attach existing tabs + `extension connect` one-command UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the zero-confirmation real-Chrome feature. - Drive the user's EXISTING logged-in tabs (not just newly-created ones): extension attachTab now treats "already attached" (a lingering chrome.debugger binding after a service-worker restart) as success and announces the tab anyway, instead of skipping it. The nm-host also sends {method:"attachAll"} when an agent-browser CDP client connects, so the daemon doesn't race an empty target list. - `agent-browser extension connect` auto-discovers the relay's CDP url (~/.agent-browser/relay-cdp-url) and attaches — no copying a ws URL. Rewrites into the normal `connect ` flow; `extension install/status/uninstall` unchanged. - Skill docs: a "drive your real, logged-in Chrome (extension)" section. Verified end-to-end: `extension connect` listed the user's real tabs (Lark, LINUX DO, Rakuten, Discord) and read a logged-in Lark doc's title — zero token, zero confirmation. Full suite 768 passed. --- cli/src/connect.rs | 16 ++++++++++++++ cli/src/main.rs | 28 ++++++++++++++++++++----- extensions/ab-connect/background.js | 20 +++++++++++++++++- skill-data/core/references/commands.md | 29 ++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 6 deletions(-) diff --git a/cli/src/connect.rs b/cli/src/connect.rs index 030d5a4..caf28f8 100644 --- a/cli/src/connect.rs +++ b/cli/src/connect.rs @@ -233,6 +233,19 @@ fn relay_url_path() -> PathBuf { .unwrap_or_else(|| PathBuf::from("/tmp/ab-relay-cdp-url")) } +/// The live relay CDP WebSocket URL, if the native-messaging host is running +/// (it writes the file on connect and removes it on exit). Used by +/// `agent-browser extension connect` to attach without the user copying a URL. +pub fn relay_url() -> Option { + let s = std::fs::read_to_string(relay_url_path()).ok()?; + let s = s.trim().to_string(); + if s.starts_with("ws://") { + Some(s) + } else { + None + } +} + /// Hidden `__nm-host` mode: launched by Chrome for the ab-connect extension. /// /// Bridges the extension (native-messaging stdio, envelope protocol) to a local @@ -390,6 +403,9 @@ async fn handle_cdp_client( Err(_) => return, }; nm_log("[nm-host] cdp client connected"); + // Ask the extension to (re)attach + announce every tab so this client + // discovers the user's existing tabs instead of racing an empty list. + let _ = to_ext.send(br#"{"method":"attachAll"}"#.to_vec()).await; let (mut tx, mut rx) = ws.split(); loop { tokio::select! { diff --git a/cli/src/main.rs b/cli/src/main.rs index 9de762a..fdf29d1 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -539,7 +539,7 @@ fn main() { let args: Vec = env::args().skip(1).collect(); let mut flags = parse_flags(&args); - let clean = clean_args(&args); + let mut clean = clean_args(&args); // Loudly warn when launching a fresh browser with no profile: it gets a // temporary EMPTY profile (no cookies / no login). For logged-in sites the @@ -650,11 +650,29 @@ fn main() { return; } - // Handle extension (doesn't need daemon): native-messaging host install/status - // for the ab-connect extension. (`connect ` stays the CDP-attach command.) + // Handle extension: native-messaging host install/status, and + // `extension connect` which attaches to the live relay (auto-discovers the + // CDP url the host wrote) by rewriting into the normal `connect ` flow. + // (`connect ` stays the plain CDP-attach command.) if clean.first().map(|s| s.as_str()) == Some("extension") { - connect::run_connect(&clean, flags.json); - return; + if clean.get(1).map(|s| s.as_str()) == Some("connect") { + match connect::relay_url() { + Some(url) => { + clean = vec!["connect".to_string(), url]; + // fall through to the normal connect handling below + } + None => { + eprintln!( + "{} extension not connected. Run `agent-browser extension install`, load the\n ab-connect extension in Chrome (chrome://extensions → Developer mode →\n Load unpacked → extensions/ab-connect), then retry.", + color::error_indicator() + ); + exit(1); + } + } + } else { + connect::run_connect(&clean, flags.json); + return; + } } // Handle session separately (doesn't need daemon) diff --git a/extensions/ab-connect/background.js b/extensions/ab-connect/background.js index 2a47ced..ed35ee0 100644 --- a/extensions/ab-connect/background.js +++ b/extensions/ab-connect/background.js @@ -76,6 +76,13 @@ async function onHostMessage(msg) { postToHost({ method: 'pong' }) return } + // Daemon (re)connected — (re)attach and announce every tab so it discovers + // the user's existing tabs rather than racing an empty target list. + if (msg.method === 'attachAll') { + reannounceAttachedTabs() + await attachAllTabs() + return + } if (typeof msg.id !== 'undefined' && msg.method === 'forwardCDPCommand') { try { const result = await handleForwardCdpCommand(msg) @@ -163,7 +170,18 @@ async function attachTab(tabId) { const existing = tabs.get(tabId) if (existing) return existing const dbg = { tabId } - await chrome.debugger.attach(dbg, '1.3') + try { + await chrome.debugger.attach(dbg, '1.3') + } catch (e) { + // After a service-worker restart, chrome.debugger may still be bound to + // this tab from the previous instance — "Another debugger is already + // attached". The tab is still controllable via {tabId}, so don't skip it + // (skipping is why existing tabs went un-announced and the daemon opened a + // blank tab instead). Re-announce it. Any other error (restricted page) is + // surfaced and the caller skips this tab. + const msg = String((e && e.message) || e) + if (!/already attached|already being debugged/i.test(msg)) throw e + } await chrome.debugger.sendCommand(dbg, 'Page.enable').catch(() => {}) const info = /** @type {any} */ (await chrome.debugger.sendCommand(dbg, 'Target.getTargetInfo')) const targetInfo = info?.targetInfo diff --git a/skill-data/core/references/commands.md b/skill-data/core/references/commands.md index 282b48c..1680645 100644 --- a/skill-data/core/references/commands.md +++ b/skill-data/core/references/commands.md @@ -318,6 +318,35 @@ agent-browser --version # Show version (-V) agent-browser --help # Show detailed help for a command ``` +## Drive your real, logged-in Chrome (extension — zero confirmation) + +Chrome 136 blocked `--remote-debugging-port` on the default profile, so to drive +the user's *existing* logged-in window, agent-browser uses a Chrome **extension** +over native messaging — no port, no token, no per-use confirmation (the +codex/claude approach). + +One-time setup: +```bash +agent-browser extension install # writes the native-messaging host manifest +# then in Chrome: chrome://extensions → Developer mode → Load unpacked → +# /extensions/ab-connect (load once) +``` + +Then, any time: +```bash +agent-browser extension connect # auto-attaches to the live, logged-in tabs +agent-browser tab # list the real tabs it now controls +agent-browser tab t3 # switch the session to one of them +agent-browser snapshot -i / eval / click ... # drive it like any session +agent-browser extension status # is the host installed? +agent-browser extension uninstall # remove the host manifest +``` + +Security: the extension↔host link is authenticated by Chrome (extension id); the +host↔agent-browser CDP link uses an unguessable URL in a 0600 file. Use this when +you need the user's real cookies/login on their actual machine. (`--extension +` is unrelated — that loads an extension into a *launched* browser.) + ## Debugging ```bash