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