From 5a61a6455958d4d0f85f46330a21d79cf7066c79 Mon Sep 17 00:00:00 2001 From: leeguooooo Date: Thu, 11 Jun 2026 20:11:22 +0900 Subject: [PATCH] =?UTF-8?q?feat(stealth):=20silent=20operation=20=E2=80=94?= =?UTF-8?q?=20never=20steal=20the=20user's=20foreground=20tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Driving the user's real Chrome should not yank their view around. Now the agent operates entirely in the background: - New tabs are created with `background: true` (CreateTargetParams) so opening one never foregrounds it (the ab-connect extension already used active:false; this covers the raw-CDP path too). - Dropped the two AUTO `Page.bringToFront` calls (auto-connect fresh tab, and the internal active-page switch). The explicit `bringToFront` command is untouched — surfacing a tab stays opt-in. - enable_domains now sets `Emulation.setFocusEmulationEnabled(true)` so a backgrounded agent tab still renders (screenshots work), isn't render-throttled, and reports document.hasFocus()/visibilityState='visible' — which also removes the "tab is hidden the whole session" bot tell. Verified headless: hasFocus=true/visible while backgrounded; click + screenshot still work. Default behaviour, no flag. --- cli/src/native/actions.rs | 7 +++---- cli/src/native/browser.rs | 26 +++++++++++++++++++++----- cli/src/native/cdp/types.rs | 6 ++++++ cli/src/native/state.rs | 1 + 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/cli/src/native/actions.rs b/cli/src/native/actions.rs index de95c9e..d2ab992 100644 --- a/cli/src/native/actions.rs +++ b/cli/src/native/actions.rs @@ -1511,12 +1511,11 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value { /// subsequent navigations don't hijack the user's existing tabs. async fn connect_auto_with_fresh_tab() -> Result { let mut mgr = BrowserManager::connect_auto().await?; + // tab_new creates the tab in the background (CreateTargetParams.background), + // so attaching to the user's Chrome never steals their foreground tab. We + // deliberately do NOT bring it to front — silent operation. mgr.tab_new(None, None).await?; let session_id = mgr.active_session_id()?.to_string(); - let _ = mgr - .client - .send_command("Page.bringToFront", None, Some(&session_id)) - .await; // Liveness probe: confirm the CDP session can actually round-trip // before returning success. Without this, a zombie CDP socket (process diff --git a/cli/src/native/browser.rs b/cli/src/native/browser.rs index 5b0a837..06f75d3 100644 --- a/cli/src/native/browser.rs +++ b/cli/src/native/browser.rs @@ -581,6 +581,7 @@ impl BrowserManager { &CreateTargetParams { url: "about:blank".to_string(), agent_group, + background: None, }, None, ) @@ -687,6 +688,20 @@ impl BrowserManager { Some(session_id), ) .await; + // Silent operation: agent tabs are driven in the background (we never + // force them to the foreground), so emulate focus. Without this a + // backgrounded tab is render-throttled and reports `document.hidden` / + // `!document.hasFocus()` — which both breaks timing-sensitive pages and + // is itself a bot signal (a real user looks at the page). Best-effort; + // ignored on engines without Emulation support. + let _ = self + .client + .send_command( + "Emulation.setFocusEmulationEnabled", + Some(json!({ "enabled": true })), + Some(session_id), + ) + .await; Ok(()) } @@ -973,6 +988,7 @@ impl BrowserManager { &CreateTargetParams { url: "about:blank".to_string(), agent_group, + background: None, }, None, ) @@ -1137,6 +1153,7 @@ impl BrowserManager { &CreateTargetParams { url: target_url.to_string(), agent_group, + background: Some(true), }, None, ) @@ -1192,11 +1209,10 @@ impl BrowserManager { let session_id = self.pages[index].session_id.clone(); self.enable_domains(&session_id).await?; - // Bring tab to front - let _ = self - .client - .send_command("Page.bringToFront", None, Some(&session_id)) - .await; + // Silent: switching the agent's *internal* active page must not yank the + // user's foreground tab. The page is driven in the background (focus is + // emulated in enable_domains); the explicit `bringToFront` command is the + // only way a tab is deliberately surfaced. let url = self.get_url().await.unwrap_or_default(); let title = self.get_title().await.unwrap_or_default(); diff --git a/cli/src/native/cdp/types.rs b/cli/src/native/cdp/types.rs index 81992a1..52922e1 100644 --- a/cli/src/native/cdp/types.rs +++ b/cli/src/native/cdp/types.rs @@ -153,6 +153,12 @@ pub struct CreateTargetParams { /// endpoint never receives an unknown parameter. #[serde(skip_serializing_if = "Option::is_none")] pub agent_group: Option, + /// Create the tab in the background so opening it never steals the user's + /// foreground tab (silent operation). Standard CDP param; the ab-connect + /// extension creates its tabs `active: false` regardless, so this only + /// affects the raw-CDP (no extension) path. + #[serde(skip_serializing_if = "Option::is_none")] + pub background: Option, } #[derive(Debug, Deserialize)] diff --git a/cli/src/native/state.rs b/cli/src/native/state.rs index 01e9483..1698cb1 100644 --- a/cli/src/native/state.rs +++ b/cli/src/native/state.rs @@ -121,6 +121,7 @@ async fn collect_storage_via_temp_target( url: "about:blank".to_string(), // Transient internal target (storage collection) — never grouped. agent_group: None, + background: None, }, None, )