← Commit history
Background-by-default hardening: switch_window/open_tab no longer steal focus; bridge+skill rules to capture chrome:// pages in the background (never launch browser foreground)
6 files changed
+78−17
bridge/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "native-browser", "displayName": "Adom Native Browser (Chrome + Edge)",- "version": "0.0.13",+ "version": "0.0.14", "description": "Drives the user's REAL signed-in Chrome/Edge via an installed extension (native-messaging transport). Mirrors the puppeteer bridge's verb surface as nbrowser_* but runs inside the actual logged-in profile, so cookies/SSO/saved-logins/captcha human-trust all work. The bridge binds an ephemeral loopback port (PORTS.md compliant) and writes a discovery file; the native-messaging host pumps stdio <-> this bridge; the extension never touches a TCP port.", "homepage": "https://wiki.adom.inc/adom/adom-browser-extension", "author": "Adom Inc.",
bridge/package.json+1−1@@ -1,6 +1,6 @@ { "name": "adom-native-browser-bridge",- "version": "0.0.13",+ "version": "0.0.14", "private": true, "description": "adom-desktop bridge for the Adom native-browser extension (Chrome + Edge)", "main": "server.js",
bridge/server.js+1@@ -240,6 +240,7 @@ const HELP = { ], bestPractices: [ '⛔ NEVER reuse a window the user already has open. Do not nbrowser_navigate / reload it, and do NOT desktop_navigate it either — that yanks their live page out from under them. For ANY page YOU need (driving, reading, or screenshotting a chrome://extensions / edge://extensions page for a guide), open a NEW window (nbrowser_open_window, or a fresh OS window you then desktop_navigate), work in THAT window, and close it when done. The only thing you may do to one of their existing windows is desktop_bring_to_front to surface it for them.',+ '⛔ BACKGROUND BY DEFAULT — NEVER foreground a window unless the user explicitly asked to watch. nbrowser_open_window opens UNFOCUSED, open_tab opens active:false, switch_window does NOT raise — keep it that way. To capture a chrome:// / edge:// page (e.g. chrome://extensions for a guide), DO NOT launch the browser yourself (msedge/chrome `--new-window` via launch/launch_process RAISES the new window to the foreground). Instead: nbrowser_open_window {url:"about:blank"} (background) → desktop_navigate that window\'s hwnd to the chrome:// URL (background, no foreground) → desktop_screenshot_window (WGC captures a background/occluded window) → nbrowser_close_window. Same for recording (WGC) and screenshots (nbrowser_screenshot CDP) — both capture occluded windows, so foregrounding is never needed. Foregrounding is the USER\'s choice; do it only on explicit request (desktop_bring_to_front).', 'Before driving, nbrowser_status to confirm connected. After a dev_reload, expect a ~3-6s reconnect.', '⭐ BACKGROUND IS THE WHOLE POINT — NEVER foreground the driven window to screenshot OR record. The user must keep working while a demo records behind their other windows; yanking Chrome forward makes the feature useless. nbrowser_screenshot (CDP) captures a background/occluded window unconditionally. WGC recording captures a background/occluded window TOO — but only when the recorded Chrome was launched with the occlusion-disable flags (see Recording); without them an occluded window paints blank. Foregrounding is the USER\'s choice, made by them when they want to watch — it is never something the AI should do on its own.', 'For demos: nbrowser_caption (in-page subtitle/alert) OR AD desktop_caption (fullscreen; use position:bottom — top covers the URL bar + Chrome warning).',
extension/manifest.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Adom for Chrome & Edge", "short_name": "Adom",- "version": "0.0.12",+ "version": "0.0.15", "description": "Drive your real, logged-in Chrome/Edge from the Adom ecosystem \u2014 the native-browser counterpart to pup.", "minimum_chrome_version": "116", "background": {
extension/src/native.js+52−14@@ -62,20 +62,51 @@ chrome.tabs.onRemoved.addListener((tabId, info) => { _emit({ event: 'tab_closed', tabId: String(tabId), windowId: info.windowId, windowClosing: !!info.isWindowClosing }); }); +// ⛔ FIRST-CLASS PWA GUARD — never hijack an installed app window.+// Installed PWAs / "Add to taskbar" apps (Google Chat, Google Messages, Gmail-as-app, etc.) run in+// their OWN window, which Chrome reports as type:'app' (standalone apps are 'app'; popups 'popup';+// ordinary browsing 'normal'). Driving or NAVIGATING a tab inside an 'app' window would seize the+// user's standalone application — exactly the regression we hit (the Google Chat PWA got navigated to+// Gusto). HARD RULE: refuse to target a non-'normal' window unless the caller EXPLICITLY opts in with+// allowPwa:true (i.e. the user specifically asked to drive that exact app window).+async function assertDriveableWindow(tabId, args) {+ const allow = !!(args && (args.allowPwa === true || args.allowAppWindow === true));+ if (allow) return;+ let tab, win;+ try {+ tab = await chrome.tabs.get(Number(tabId));+ win = await chrome.windows.get(tab.windowId);+ } catch { return; } // lookup failed for an unrelated reason — let the real verb surface it+ if (win && win.type && win.type !== 'normal') {+ const kind = win.type === 'app' ? 'PWA / installed-app' : `${win.type}`;+ throw new Error(+ `⛔ refusing to drive a ${kind} window (tab ${tabId}: "${(tab.title || '').slice(0, 60)}") — ` ++ `this would hijack the user's standalone app (e.g. a taskbar PWA like Google Chat/Messages). ` ++ `Open a dedicated browser window with nbrowser_open_window and target THAT. ` ++ `Pass allowPwa:true ONLY if the user explicitly asked to drive this specific app window.`);+ }+}+ // Resolve which tab a verb targets: explicit tabId > active tab of the session > active tab overall. async function resolveTabId(args) { await ensureLoaded();- if (args && args.tabId != null) return Number(args.tabId);- if (args && args.sessionId != null && !sessionWindows.has(args.sessionId)) {- const c = _closedSessions.get(args.sessionId);- if (c) throw new Error(`session '${args.sessionId}' was closed ${c.byUser ? 'BY THE USER' : 'by the agent'} — open a new window with nbrowser_open_window`);+ let tabId;+ if (args && args.tabId != null) {+ tabId = Number(args.tabId);+ } else {+ if (args && args.sessionId != null && !sessionWindows.has(args.sessionId)) {+ const c = _closedSessions.get(args.sessionId);+ if (c) throw new Error(`session '${args.sessionId}' was closed ${c.byUser ? 'BY THE USER' : 'by the agent'} — open a new window with nbrowser_open_window`);+ }+ let windowId;+ if (args && args.sessionId != null && sessionWindows.has(args.sessionId)) windowId = sessionWindows.get(args.sessionId);+ const q = windowId != null ? { active: true, windowId } : { active: true, lastFocusedWindow: true };+ const [tab] = await chrome.tabs.query(q);+ if (!tab) throw new Error('no target tab found');+ tabId = tab.id; }- let windowId;- if (args && args.sessionId != null && sessionWindows.has(args.sessionId)) windowId = sessionWindows.get(args.sessionId);- const q = windowId != null ? { active: true, windowId } : { active: true, lastFocusedWindow: true };- const [tab] = await chrome.tabs.query(q);- if (!tab) throw new Error('no target tab found');- return tab.id;+ await assertDriveableWindow(tabId, args); // ⛔ PWA guard on EVERY resolution path+ return tabId; } function resolveWindowId(args) {@@ -130,8 +161,12 @@ export async function switchWindow(args = {}) { await ensureLoaded(); const winId = resolveWindowId(args); if (winId == null) throw new Error('unknown sessionId');- await chrome.windows.update(winId, { focused: true });- return { ok: true };+ // BACKGROUND by default — switching which window the agent targets must NEVER raise it into the+ // user's face. Every verb already targets a window by its sessionId, so this is a no-op for+ // driving; only raise it when the USER explicitly asked to see it (focused:true), or use AD+ // desktop_bring_to_front for that.+ if (args.focused === true) await chrome.windows.update(winId, { focused: true });+ return { ok: true, focused: args.focused === true, _hint: args.focused === true ? 'Raised for the user (explicit focus).' : 'Targeted in the BACKGROUND, not raised — drive it by passing this sessionId. Surface it only on explicit user request (focused:true or desktop_bring_to_front).' }; } // Window state — fullscreen / maximized / normal / minimized, via chrome.windows.update.@@ -189,8 +224,11 @@ export async function rescan() { export async function openTab(args = {}) { await ensureLoaded(); const windowId = resolveWindowId(args);- const tab = await chrome.tabs.create(windowId != null ? { windowId, url: args.url } : { url: args.url });- return { ok: true, sessionId: args.sessionId, tabId: String(tab.id), url: tab.url, title: tab.title };+ // BACKGROUND by default — a new tab must not steal the user's focus or yank their active tab.+ // chrome.tabs.create defaults to active:true; force active:false unless the user asked (focused:true).+ const base = { url: args.url, active: args.focused === true };+ const tab = await chrome.tabs.create(windowId != null ? { windowId, ...base } : base);+ return { ok: true, sessionId: args.sessionId, tabId: String(tab.id), url: tab.url, title: tab.title, _hint: 'Opened in the BACKGROUND (active:false) — drive it by tabId. Pass focused:true only if the user asked to see it.' }; } export async function closeTab(args = {}) {
skills/driving-the-extension/SKILL.md+22@@ -23,6 +23,28 @@ then `desktop_navigate`), do your work and capture in THAT window, and **close i only thing you may do to one of their existing windows is `desktop_bring_to_front` to surface it for them — never to drive or navigate it. +## ⛔ Background by default — NEVER foreground unless the user asks++The user is working in other windows. **Never raise a window to the foreground** for your own needs.+The verbs are built for this: `nbrowser_open_window` opens **unfocused**, `open_tab` opens+`active:false`, `switch_window` does **not** raise — leave those defaults alone. Capture without+foregrounding: **`nbrowser_screenshot` (CDP) and `desktop_screenshot_window` (WGC) both capture a+background/occluded window**, and WGC records one at full fps (with the occlusion flags). So there is+never a reason to pull a window forward to grab a shot or a recording.++**Capturing a `chrome://` / `edge://` page** (e.g. `chrome://extensions` for a guide) is the classic+trap: do **NOT** launch the browser yourself — `msedge` / `chrome --new-window` (via `launch` /+`launch_process`) **raises the new window to the foreground.** Instead:++1. `nbrowser_open_window {url:"about:blank"}` → a fresh **background** window (note its `hwnd`).+2. `desktop_navigate {hwnd, url:"chrome://extensions"}` → loads the `chrome://` page **in the+ background**, no foreground.+3. `desktop_screenshot_window {hwnd}` → WGC captures it where it sits.+4. `nbrowser_close_window {sessionId}`.++Foregrounding is the **user's** choice — only `desktop_bring_to_front`, and only when they explicitly+ask to watch live.+ **Path:** `cloud → relay → adom-desktop (laptop) → native-browser bridge → native-messaging host → extension service worker → back`. You call `adom-desktop nbrowser_* '<json>'`.