← Commit history

bridge 0.0.40 / ext 0.0.22: enforce background-by-default + focus-steal auto-revert + focus_violation telemetry; userRequestedForeground gate; loud anti-foreground hints

John Lauer ·d3a7f85a04 ·4mo ago ·parent 5ad17c5
8 files changed +173−55
bridge/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.0.39\ No newline at end of file+0.0.40
bridge/bridge.json+2−2
@@ -2,8 +2,8 @@   "manifest_version": 1,   "name": "native-browser",   "displayName": "Adom Native Browser (Chrome + Edge)",-  "version": "0.0.39",-  "expectedExtensionVersion": "0.0.21",+  "version": "0.0.40",+  "expectedExtensionVersion": "0.0.22",   "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.39",+  "version": "0.0.40",   "private": true,   "description": "adom-desktop bridge for the Adom native-browser extension (Chrome + Edge)",   "main": "server.js",
bridge/server.js+15−12
@@ -269,7 +269,7 @@ const HELP = {     'Screenshots are large base64 and can TIME OUT over the relay - retry, or prefer AD desktop_screenshot_window for proof shots.',     'nbrowser_eval runs in the PAGE world: chrome.* APIs are NOT available there (only DOM/window).',     'ALWAYS clean up windows you open (nbrowser_close_window) - abandoned windows annoy the user.',-    'Poll nbrowser_events to learn if the USER closed your windows/tabs (window_closed {byUser:true}).',+    'Poll nbrowser_events to learn if the USER closed your windows/tabs (window_closed {byUser:true}), or if a foregrounding attempt was auto-reverted (focus_violation {verb, autoReverted:true}) - a focus_violation means you tried to shove a window in front of the user; stop foregrounding.',   ],   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.',@@ -288,7 +288,7 @@ const HELP = {     'fetch_url/download': 'fetch_url uses the user\'s REAL cookies (authed). download via chrome.downloads.',     'errors/events': 'errors = console/page errors for a tab. events = window/tab closes incl. byUser.',     'record_start/stop/status/list': 'method: wgc|getdisplaymedia|tabcapture. record_start is non-blocking for getdisplaymedia (poll record_status). base64 return is for SHORT clips only.',-    'caption/focus': 'caption{style:subtitle|alert,position}. focus activates the tab + flashes its taskbar button (does NOT raise the window - by design, never steals the user\'s focus). Capture in place; only desktop_bring_to_front when the user explicitly wants to watch live.',+    'caption/focus': 'caption{style:subtitle|alert,position}. focus selects the tab + flashes its taskbar button (does NOT raise the window - by design, never steals the user\'s focus). Capture in place; the window is raised ONLY on explicit user request (userRequestedForeground:true, or the core desktop_bring_to_front). NEVER foreground or OS-launch a window to surface it - foregrounding is the user\'s choice alone.',     'flash/flash_stop/flash_mode/taskbar': 'Paint glanceable status on the driven browser window\'s Windows TASKBAR button (hwnd captured at open_window; an "Adom is driving" badge goes on automatically). flash = orange "come look" cue (auto-clears on focus). flash_mode{quiet|chatty|off}: quiet(default)=flash on come-look moments (load/screenshot/error/done); chatty=every action (users who want to watch); off=no auto-flash. nbrowser_taskbar{progress:{state:"normal"|"paused"|"error"|"indeterminate"|"none",value?},overlay:{badge:"adom"|"none"}} = progress bar (green/YELLOW-for-gates/red/marquee) + overlay badge for long tasks. Use paused(yellow)+flash on captcha/2FA/login. Clears on close.',   }, };@@ -711,18 +711,21 @@ function _hostManifestPaths() { // every navigate/reload/close/window verb: only ever act on a window/tab YOU opened (nbrowser_open_window). // Never touch one the user already has open - that can wipe their live work and other AI threads' tabs. const OWN_ONLY = 'ONLY operate on a window/tab YOU opened via nbrowser_open_window. NEVER navigate/reload/close a window or tab the user already had open - the bridge drives their REAL browser, so you would destroy their live page, forms, sessions, or other AI threads.';+// The politeness mandate, repeated verbatim on every focus-relevant verb. The bridge ENFORCES it in code+// (background-by-default + focus-steal auto-revert + focus_violation events); this hint tells the AI why.+const NO_FG = 'Open in the BACKGROUND. NEVER raise or maximize a window over the user\'s other work - it is rude and disruptive. NEVER fall back to OS-level window launches (Start-Process/launch) or bring_to_front/maximize to open or surface windows. If a background open is blocked, return a shareable link instead of foregrounding. Foregrounding is the user\'s choice alone (pass userRequestedForeground:true only when the user explicitly asked to watch live). A driven window that lands in the foreground is auto-sent back and logged as a focus_violation.'; const VERB_META = {   nbrowser_status: { hint: 'Connectivity check: connected profiles + active browser. Call this first if any verb errors "not connected".', related: ['nbrowser_profiles', 'nbrowser_ping', 'nbrowser_open_window'], pitfalls: ['a profile\'s extension SLEEPS when that profile has no open windows - it then shows disconnected; open a window to wake it'] },   nbrowser_readiness: { hint: 'READ-ONLY cold-start probe: is the native-browser extension connected + ready to drive? NEVER spawns work or installs anything. state:"ready" = drive now; state:"needs-extension" = offer to install it (Level-2, see installing-the-extension) or it is asleep (open a window in that profile). Also reports chromeInstalled: if false, the user has no native Chrome and you can offer nbrowser_install_chrome.', related: ['nbrowser_status', 'nbrowser_open_window', 'nbrowser_install_chrome'], pitfalls: ['"needs-extension" can mean not-installed OR installed-but-asleep - nbrowser_status\'s tooltip distinguishes', 'this verb never installs; to install, run the installing-the-extension flow', 'for a machine-wide "what do I have" answer prefer AD\'s bridge_readiness (aggregates all bridges, spawns none)'] },   nbrowser_install_chrome: { hint: 'Install NATIVE Google Chrome for the user (Windows). Downloads Google\'s official installer + runs a QUIET user-scoped install (no admin on a normal PC). NON-BLOCKING - returns immediately; poll nbrowser_chrome_status. On a locked-down box it escalates to a notify-first UAC (fires a sticky "Approve now" toast, waits for the tap, then raises the prompt). This is step 1 of getting the AI onto the user\'s REAL browser - after Chrome is in, help them sign into Google + load the extension.', related: ['nbrowser_chrome_status', 'nbrowser_readiness', 'installing-the-extension'], pitfalls: ['non-blocking: do NOT expect Chrome to exist on return - POLL nbrowser_chrome_status until phase:"ready"', 'awaiting_uac means a prompt is on the user\'s desktop - relay it; do not re-trigger install', 'Windows only for now', 'this only INSTALLS Chrome; the user still signs into Google themselves (their credential) - guide them via setting-up-native-chrome'] },   nbrowser_chrome_status: { hint: 'READ-ONLY: is native Google Chrome installed, and how is an in-flight install going? Returns {installed, executablePath, phase, installing, awaitingUac}. phase: idle|installing|awaiting_uac|ready|failed. Poll this after nbrowser_install_chrome. Never installs or spawns.', related: ['nbrowser_install_chrome', 'nbrowser_readiness'], pitfalls: ['awaiting_uac + notifyChannel:true = a toast is on the desktop, keep polling; notifyChannel:false = relay the UAC to the user yourself', 'installed:true means Chrome.exe is present - next help the user sign into Google + load the extension'] },   nbrowser_ping: { hint: 'Round-trip liveness through to the extension.', related: ['nbrowser_status'], pitfalls: [] },-  nbrowser_open_window: { hint: 'Open a NEW background window YOU own and get a sessionId; do ALL your work through that sessionId. This is how you get a surface to drive WITHOUT touching the user\'s own windows.', related: ['nbrowser_navigate', 'nbrowser_screenshot', 'nbrowser_close_window'], next: 'nbrowser_navigate', pitfalls: ['opens UNFOCUSED - do NOT pass focused:true unless the user asked to watch', 'this is the safe way to work: always drive a window you opened, never the user\'s existing ones'] },+  nbrowser_open_window: { hint: 'Open a NEW background window YOU own and get a sessionId; do ALL your work through that sessionId. This is how you get a surface to drive WITHOUT touching the user\'s own windows. ' + NO_FG, related: ['nbrowser_navigate', 'nbrowser_screenshot', 'nbrowser_close_window'], next: 'nbrowser_navigate', pitfalls: ['opens UNFOCUSED in the BACKGROUND - a bare focused:true is IGNORED; only userRequestedForeground:true (user asked to watch) raises it', 'NEVER fall back to an OS-level launch (Start-Process/msedge/chrome --new-window) to open a window - that RAISES it to the foreground; this verb is the polite path and always works in the background', 'this is the safe way to work: always drive a window you opened, never the user\'s existing ones'] },   nbrowser_close_window: { hint: 'Close a window you opened. Clean up your own sessions when done.', related: ['nbrowser_list_windows'], pitfalls: ['⛔ only close a sessionId YOU opened - closing the user\'s window can wipe their live work and other AI threads'] },-  nbrowser_switch_window: { hint: 'Target a window for later verbs. Does NOT raise it.', related: ['nbrowser_list_windows'], pitfalls: ['does not foreground (by design); pass focused:true ONLY on explicit user request'] },+  nbrowser_switch_window: { hint: 'Target a window for later verbs. Does NOT raise it. ' + NO_FG, related: ['nbrowser_list_windows'], pitfalls: ['does not foreground (by design); only userRequestedForeground:true raises it, and only when the user explicitly asked to watch live'] },   nbrowser_list_windows: { hint: 'List live windows/sessions with bounds+state.', related: ['nbrowser_switch_window', 'nbrowser_rescan'], pitfalls: [] },-  nbrowser_window_state: { hint: 'Maximize/fullscreen/normal/minimize or move+resize.', related: ['nbrowser_fullscreen'], pitfalls: ['resizing a window the USER is in disrupts them; act on your own session windows'] },-  nbrowser_fullscreen: { hint: 'Fullscreen the window (hides tabs/URL bar) - great for clean recordings.', related: ['nbrowser_window_state'], pitfalls: ['F11-via-key does NOT work; use this verb'] },+  nbrowser_window_state: { hint: 'Maximize/fullscreen/normal/minimize or move+resize. ' + NO_FG, related: ['nbrowser_fullscreen'], pitfalls: ['maximize/fullscreen are USER-only foregrounding actions: they enlarge the window but it STAYS in the background (auto-reverted) unless userRequestedForeground:true - so recordings still work but you can never shove it in front of the user', 'resizing a window the USER is in disrupts them; act on your own session windows'] },+  nbrowser_fullscreen: { hint: 'Fullscreen the window (hides tabs/URL bar) - great for clean recordings of a BACKGROUND window. ' + NO_FG, related: ['nbrowser_window_state'], pitfalls: ['F11-via-key does NOT work; use this verb', 'stays in the background (auto-reverted) unless userRequestedForeground:true - it does not raise the window over the user\'s work'] },   nbrowser_login_state: { hint: 'Is the profile signed in to a URL right now (via chrome.cookies)?', related: ['nbrowser_credentials'], pitfalls: [] },   nbrowser_credentials: { hint: 'Read-only QUERY of the browser\'s own saved-login store (which vendors the user can auto-login to). Never returns passwords.', related: ['nbrowser_login_state', 'nbrowser_navigate'], pitfalls: ['if a vendor is not listed, the user has no saved login - ask them to save one once', 'never exposes the password; Chrome autofills it when you focus the field'] },   nbrowser_profiles: { hint: 'List every BROWSER x PROFILE you can drive + which is active. Drive several in parallel with profile:"<key>".', related: ['nbrowser_use_profile', 'nbrowser_profile_block'], pitfalls: ['active is only the default when no profile is passed'] },@@ -734,7 +737,7 @@ const VERB_META = {   nbrowser_profile_unblock: { hint: 'Remove a profile from the blocklist.', related: ['nbrowser_profile_block'], pitfalls: [] },   nbrowser_rescan: { hint: 'Reconcile the session map against live windows (drop/mark-closed the gone ones).', related: ['nbrowser_list_windows', 'nbrowser_resync_sessions'], pitfalls: [] },   nbrowser_resync_sessions: { hint: 'LIGHTWEIGHT rebuild of session tracking WITHOUT a dev_reload. Re-reads the persisted session→window map, merges it into memory, and re-validates every session against live windows. Use it the moment a session verb fails with session_untracked (e.g. after the user interacted with Chrome, or another tool moved/closed windows via SetForegroundWindow/WM_CLOSE) - it un-sticks tracking in ONE call. Returns the drivable sessions + which are closed.', related: ['nbrowser_open_window', 'nbrowser_profiles', 'nbrowser_rescan'], pitfalls: ['a session whose window was genuinely closed cannot be revived - open a fresh one with nbrowser_open_window', 'you do NOT need this in the normal path; sessions survive SW sleep on their own now - reach for it only on a session_untracked error'] },-  nbrowser_open_tab: { hint: 'Open a background tab in one of YOUR session windows.', related: ['nbrowser_close_tab', 'nbrowser_list_tabs'], pitfalls: ['opens active:false (background); open tabs in a window you created, not the user\'s'] },+  nbrowser_open_tab: { hint: 'Open a background tab in one of YOUR session windows. ' + NO_FG, related: ['nbrowser_close_tab', 'nbrowser_list_tabs'], pitfalls: ['opens active:false (background) - a bare focused:true/active:true is IGNORED; only userRequestedForeground:true activates it', 'open tabs in a window you created, not the user\'s'] },   nbrowser_close_tab: { hint: 'Close a tab you opened.', related: ['nbrowser_list_tabs'], pitfalls: ['⛔ only close a tab YOU opened - closing the user\'s tab destroys their live page/session'] },   nbrowser_switch_tab: { hint: 'Make a tab active within its window.', related: ['nbrowser_list_tabs'], pitfalls: [] },   nbrowser_list_tabs: { hint: 'List tabs (tabId/url/title/active).', related: ['nbrowser_switch_tab'], pitfalls: [] },@@ -871,12 +874,12 @@ function dispatch(verb, args) {         d('nbrowser_install_chrome', 'Install NATIVE Google Chrome for the user (Windows). Downloads Google\'s official installer + runs a quiet user-scoped install (no admin on a normal PC); escalates to a notify-first UAC on a locked-down box. NON-BLOCKING - poll nbrowser_chrome_status. Step 1 of putting the AI onto the user\'s real signed-in browser.', { elevate: 'optional "auto"(default) | true (skip quiet, straight to UAC) | false (quiet only, no prompt)' }, { ok: 'bool', installing: 'bool', installed: 'bool?', alreadyInstalled: 'bool?', executablePath: 'str?', statusVerb: 'str' }, 30, { statusVerb: 'nbrowser_chrome_status', example: {} }),         d('nbrowser_chrome_status', 'READ-ONLY: is native Google Chrome installed / how is an install going? Poll after nbrowser_install_chrome.', {}, { installed: 'bool', executablePath: 'str?', phase: '"idle"|"installing"|"awaiting_uac"|"ready"|"failed"', installing: 'bool', awaitingUac: 'bool', errorCode: 'str?' }, 15, { example: {} }),         d('nbrowser_ping', 'Round-trip liveness check through to the extension.', {}, { ok: 'bool', pong: 'bool' }, 15),-        d('nbrowser_open_window', 'Open a NEW window in the BACKGROUND (unfocused) and return a sessionId to drive it.', { url: 'required str', profile: P, focused: 'optional bool (default false = background)' }, { ok: 'bool', sessionId: 'str', tabId: 'str', windowId: 'int', url: 'str' }, 30, { example: { url: 'https://wiki.adom.inc' } }),+        d('nbrowser_open_window', 'Open a NEW window in the BACKGROUND (unfocused) and return a sessionId to drive it. Never steals focus; a driven window that lands topmost is auto-reverted + logged (focus_violation).', { url: 'required str', profile: P, userRequestedForeground: 'optional bool - raise it to the foreground ONLY when the USER explicitly asked to watch live (default false = background; a bare focused:true is ignored)' }, { ok: 'bool', sessionId: 'str', tabId: 'str', windowId: 'int', url: 'str', _warning: 'str? (present if a foregrounding attempt was auto-reverted)' }, 30, { example: { url: 'https://wiki.adom.inc' } }),         d('nbrowser_close_window', 'Close a window the agent opened.', { sessionId: 'required str', profile: P }, { ok: 'bool', closed: 'bool', sessionId: 'str' }, 20, { example: { sessionId: 'work' } }),-        d('nbrowser_switch_window', 'Target a window for subsequent verbs; does NOT raise it unless focused:true.', { sessionId: 'required str', focused: 'optional bool (default false)' }, { ok: 'bool', focused: 'bool' }, 15, { example: { sessionId: 'work' } }),+        d('nbrowser_switch_window', 'Target a window for subsequent verbs; does NOT raise it unless the user asked to watch live.', { sessionId: 'required str', userRequestedForeground: 'optional bool (default false = do not raise; only true, and only on explicit user request, raises it)' }, { ok: 'bool', focused: 'bool' }, 15, { example: { sessionId: 'work' } }),         d('nbrowser_list_windows', 'List the live windows/sessions.', { profile: P }, { ok: 'bool', sessions: '[{id,windowId,tabCount,active,state,bounds,url}]', count: 'int' }, 20, { example: {} }),-        d('nbrowser_window_state', 'Set window state (fullscreen/maximized/normal/minimized) or move/resize via left/top/width/height.', { sessionId: 'required str', state: 'optional fullscreen|maximized|normal|minimized', left: 'optional int', top: 'optional int', width: 'optional int', height: 'optional int' }, { ok: 'bool', windowId: 'int', state: 'str' }, 20, { example: { sessionId: 'work', state: 'maximized' } }),-        d('nbrowser_fullscreen', 'Toggle the window to fullscreen (hides tabs+URL bar).', { sessionId: 'required str' }, { ok: 'bool', state: '"fullscreen"' }, 20, { example: { sessionId: 'work' } }),+        d('nbrowser_window_state', 'Set window state (fullscreen/maximized/normal/minimized) or move/resize via left/top/width/height. maximize/fullscreen enlarge the window but keep it in the BACKGROUND (auto-reverted) unless userRequestedForeground:true.', { sessionId: 'required str', state: 'optional fullscreen|maximized|normal|minimized', left: 'optional int', top: 'optional int', width: 'optional int', height: 'optional int', userRequestedForeground: 'optional bool - allow maximize/fullscreen to raise the window, ONLY when the user asked to watch live (default false)' }, { ok: 'bool', windowId: 'int', state: 'str', _warning: 'str?' }, 20, { example: { sessionId: 'work', state: 'maximized' } }),+        d('nbrowser_fullscreen', 'Toggle the window to fullscreen (hides tabs+URL bar) for clean recordings; stays in the background unless userRequestedForeground:true.', { sessionId: 'required str', userRequestedForeground: 'optional bool (default false)' }, { ok: 'bool', state: '"fullscreen"' }, 20, { example: { sessionId: 'work' } }),         d('nbrowser_login_state', 'Report sign-in state for the profile.', { profile: P }, { ok: 'bool' }, 20, { example: {} }),         d('nbrowser_credentials', "Read-only query of the browser's saved-login store (host+username only; never passwords).", { match: 'optional vendor substring', browser: 'optional "chrome"|"edge"', profile: P }, { ok: 'bool', count: 'int', credentials: '[{browser,profile,host,username}]' }, 20, { example: { match: 'digikey' } }),         d('nbrowser_profiles', 'List connected BROWSER×PROFILE targets + which is active.', {}, { ok: 'bool', profiles: '[{profile,email,browser,active,blocked,live}]', active: 'str' }, 15, { example: {} }),@@ -888,7 +891,7 @@ function dispatch(verb, args) {         d('nbrowser_profile_unblock', 'Remove a profile from the blocklist.', { profile: 'required str' }, { ok: 'bool', blocklist: '[str]' }, 15, { example: { profile: 'edge:[email protected]' } }),         d('nbrowser_rescan', 'Reconcile the session map against live windows (drop/mark-closed the gone ones).', {}, { ok: 'bool', sessions: '[...]' }, 20, { example: {} }),         d('nbrowser_resync_sessions', 'Lightweight rebuild of session tracking (no dev_reload): re-read persisted map + re-validate vs live windows. Use on a session_untracked error.', {}, { ok: 'bool', tracked: '[{sessionId,windowId}]', trackedCount: 'int', closed: '[{sessionId,byUser,external}]' }, 15, { example: {} }),-        d('nbrowser_open_tab', 'Open a tab in the BACKGROUND (active:false) in a window.', { sessionId: S, url: 'required str', focused: 'optional bool (default false)' }, { ok: 'bool', sessionId: 'str', tabId: 'str', url: 'str', title: 'str' }, 30, { example: { sessionId: 'work', url: 'https://example.com' } }),+        d('nbrowser_open_tab', 'Open a tab in the BACKGROUND (active:false) in a window.', { sessionId: S, url: 'required str', userRequestedForeground: 'optional bool - activate the tab ONLY when the user asked to watch live (default false; a bare focused:true is ignored)' }, { ok: 'bool', sessionId: 'str', tabId: 'str', url: 'str', title: 'str' }, 30, { example: { sessionId: 'work', url: 'https://example.com' } }),         d('nbrowser_close_tab', 'Close a tab.', { sessionId: S, tabId: 'required str' }, { ok: 'bool', tabId: 'str' }, 20, { example: { sessionId: 'work', tabId: '123' } }),         d('nbrowser_switch_tab', 'Make a tab active within its window.', { sessionId: S, tabId: 'required str' }, { ok: 'bool', tabId: 'str' }, 15, { example: { sessionId: 'work', tabId: '123' } }),         d('nbrowser_list_tabs', 'List tabs.', { profile: P, sessionId: S }, { ok: 'bool', tabs: '[{tabId,url,title,active}]' }, 20, { example: { sessionId: 'work' } }),
extension/manifest.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 3,   "name": "Adom for Chrome & Edge",   "short_name": "Adom",-  "version": "0.0.21",+  "version": "0.0.22",   "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+141−29
@@ -56,7 +56,29 @@ const _events = [];                 // recent lifecycle events for nbrowser_even const _agentClosing = new Set();    // windowIds the AGENT is closing (so we don't mislabel as user-closed) const _closedSessions = new Map();  // sessionId -> {windowId, byUser, ts} function _emit(e) { e.ts = Date.now(); _events.push(e); if (_events.length > 300) _events.shift(); }-export function drainEvents() { const e = _events.splice(0); return { ok: true, events: e, count: e.length }; }+// focus_violations are PERSISTED (not just kept in the in-memory _events, which an MV3 service-worker+// suspension wipes) so the telemetry survives until polled - the user can always see when an AI tried to+// steal focus. Capped ring buffer in chrome.storage.local.+const VKEY = 'nb_violations';+async function persistViolation(v) {+  try {+    const got = await chrome.storage.local.get(VKEY);+    const arr = got[VKEY] || [];+    arr.push(v); while (arr.length > 20) arr.shift();+    await chrome.storage.local.set({ [VKEY]: arr });+  } catch {}+}+export async function drainEvents() {+  const e = _events.splice(0);+  let violations = [];+  try {+    const got = await chrome.storage.local.get(VKEY);+    violations = (got[VKEY] || []).map((v) => ({ event: 'focus_violation', ...v }));+    if (violations.length) await chrome.storage.local.set({ [VKEY]: [] });+  } catch {}+  const all = e.concat(violations);+  return { ok: true, events: all, count: all.length };+} export function recentClose(sessionId) { return _closedSessions.get(sessionId) || null; }  chrome.windows.onRemoved.addListener(async (windowId) => {@@ -137,6 +159,65 @@ async function assertOwned(tabId, verb) {   } } +// ── POLITENESS / ANTI-FOREGROUND ENFORCEMENT ──────────────────────────────────+// The bridge GUARANTEES it can never shove a window in front of the user, no matter how the driving AI+// behaves. Windows open in the BACKGROUND; a raise happens ONLY when the user EXPLICITLY asked to watch+// live (userRequestedForeground:true). Any other path that leaves a driven window topmost is auto-reverted+// to the background and logged as a focus_violation. This removes the rudeness AND the temptation to fall+// back to OS-level foreground launches (the old bug: a blocked background path pushed the AI to+// Start-Process/bring_to_front, which is exactly the disruptive behavior).+const POLITE_HINT =+  'Open in the BACKGROUND. NEVER raise or maximize a window over the user\'s other work - it is rude and ' ++  'disruptive. NEVER fall back to OS-level window launches (Start-Process/launch) or bring_to_front/maximize ' ++  'to open or surface windows. If a background open is blocked, return a shareable link instead of ' ++  'foregrounding. Foregrounding is the user\'s choice alone.';++// A raise/foreground is honored ONLY when the user explicitly asked to watch live. Legacy focused:true /+// active:true are NOT sufficient by themselves (an impatient AI sets them) - they are IGNORED unless+// userRequestedForeground:true is also present.+function userWantsForeground(args) { return !!(args && args.userRequestedForeground === true); }++function sessionForWindow(winId) { for (const [sid, wid] of sessionWindows) if (wid === winId) return sid; return null; }++// The chrome window currently focused (i.e. Chrome is in the OS foreground), or null when the user is in+// another app. Used to know which window to hand focus BACK to if a driven window steals it.+async function currentForeground() {+  try { const w = await chrome.windows.getLastFocused(); return w && w.focused ? w.id : null; } catch { return null; }+}++// Backstop invoked after any open/navigate/reload/switch/state change: if the driven window ended up+// topmost (focused) and the user did NOT ask to watch live, send it back behind the user's window and log+// a focus_violation. Chrome only exposes `focused` (not true OS z-order), so focused===true is our best+// proxy for "landed on top". We cannot unfocus a window directly (Chrome forbids windows.update{focused:+// false}), so we re-raise whatever window the user had, which drops ours behind it. Never throws.+async function enforceBackground(winId, args, verb, prevFocusedId) {+  if (userWantsForeground(args)) return null; // the user asked to see it - allowed+  if (winId == null) return null;+  let w;+  try { w = await chrome.windows.get(winId); } catch { return null; }+  if (!w || !w.focused) return null; // not topmost → nothing to correct+  let how = null;+  // Preferred: hand focus back to whatever window the user had before, which drops ours behind it.+  if (prevFocusedId != null && prevFocusedId !== winId) {+    try { await chrome.windows.update(prevFocusedId, { focused: true }); how = 'restored-previous'; } catch {}+  }+  // Fallback (the user was in a NON-Chrome app, so there is no Chrome window to hand focus back to):+  // we cannot unfocus a window directly (Chrome forbids windows.update{focused:false}), so MINIMIZE ours+  // to physically remove it from the user's foreground. This is the case that matches the user's actual+  // complaint (a window shoved in front while they multitask in another app) - it must never be left up.+  if (!how) {+    try { await chrome.windows.update(winId, { state: 'minimized' }); how = 'minimized'; } catch {}+  }+  await persistViolation({ verb, sessionId: sessionForWindow(winId), windowId: winId, autoReverted: true, how, ts: Date.now() });+  return {+    autoReverted: true, how,+    _warning: `⚠️ focus_violation: ${verb} left a driven window in the FOREGROUND over the user's work; it was auto-sent to the BACKGROUND (${how === 'restored-previous' ? "focus returned to the user's previous window" : how === 'minimized' ? 'minimized, because the user was in another app' : 'could not be corrected'}). ${POLITE_HINT}`,+  };+}++// Fold an enforceBackground() warning into a verb's result object (no-op if there was no violation).+function withWarn(res, warn) { if (warn && warn._warning) { res._warning = warn._warning; res.focusAutoReverted = true; } return res; }+ // Resolve which tab a verb targets. Keyed off the REAL session→window map, never an "active tab" // heuristic when a sessionId/tabId is given: //   explicit tabId   → that tab (ownership re-checked for mutating verbs)@@ -215,19 +296,24 @@ function waitForLoad(tabId, timeoutMs = 30000) { // ---- windows / sessions ---------------------------------------------------- export async function openWindow(args = {}) {   await ensureLoaded();-  // Open in the BACKGROUND by default — never steal the user's focus. The window is fully-  // drivable + capturable while occluded; pass focused:true ONLY when the user explicitly-  // asked to watch it live. (This is the whole point of native-browser driving: the user-  // keeps working in their foreground window while we operate one of their other windows.)-  const win = await chrome.windows.create({ url: args.url, focused: args.focused === true });+  // Open in the BACKGROUND by default — never steal the user's focus. The window is fully drivable ++  // capturable while occluded. It is raised ONLY when the user EXPLICITLY asked to watch live+  // (userRequestedForeground:true); a bare focused:true is IGNORED. (This is the whole point of+  // native-browser driving: the user keeps working in their foreground window while we operate another.)+  const fg = userWantsForeground(args);+  const prev = fg ? null : await currentForeground();+  const win = await chrome.windows.create({ url: args.url, focused: fg });   const sid = args.sessionId || newSessionId();   sessionWindows.set(sid, win.id);   await persist();+  const warn = await enforceBackground(win.id, args, 'nbrowser_open_window', prev); // backstop: never leaves it topmost   const tab = (win.tabs && win.tabs[0]) || {};-  return {+  const res = {     ok: true, sessionId: sid, tabId: String(tab.id), windowId: win.id, url: tab.url || args.url,-    _hint: 'New window opened in the BACKGROUND — the user\'s focus is deliberately NOT stolen. Screenshot it where it sits with nbrowser_screenshot (CDP) or AD desktop_screenshot_window (WGC); both capture a background/occluded window, so NEVER foreground it just to grab a shot. Only raise it (AD desktop_bring_to_front) if the user EXPLICITLY asked to watch live. Remember to nbrowser_close_window {sessionId:"' + sid + '"} when done so you don\'t leave abandoned windows.',+    _hint: 'New window opened in the BACKGROUND — the user\'s focus is deliberately NOT stolen. ' + POLITE_HINT ++      ' Screenshot it where it sits with nbrowser_screenshot (CDP) or AD desktop_screenshot_window (WGC); both capture a background/occluded window, so NEVER foreground it to grab a shot. Raise it ONLY if the user EXPLICITLY asked to watch live (pass userRequestedForeground:true). Remember to nbrowser_close_window {sessionId:"' + sid + '"} when done so you don\'t leave abandoned windows.',   };+  return withWarn(res, warn); }  export async function closeWindow(args = {}) {@@ -248,12 +334,15 @@ export async function switchWindow(args = {}) {   await ensureLoaded();   const winId = await resolveWindowId(args);   if (winId == null) throw new Error('unknown sessionId');-  // 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).' };+  // 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; raise it+  // ONLY when the USER explicitly asked to watch live (userRequestedForeground:true).+  const fg = userWantsForeground(args);+  const prev = fg ? null : await currentForeground();+  if (fg) await chrome.windows.update(winId, { focused: true });+  const warn = await enforceBackground(winId, args, 'nbrowser_switch_window', prev);+  const res = { ok: true, focused: fg, _hint: fg ? 'Raised for the user (explicit userRequestedForeground:true).' : 'Targeted in the BACKGROUND, not raised — drive it by passing this sessionId. ' + POLITE_HINT };+  return withWarn(res, warn); }  // Window state — fullscreen / maximized / normal / minimized, via chrome.windows.update.@@ -263,6 +352,8 @@ export async function windowState(args = {}) {   await ensureLoaded();   const winId = await resolveWindowId(args);   if (winId == null) throw new Error('unknown sessionId');+  const fg = userWantsForeground(args);+  const prev = fg ? null : await currentForeground();   // Move/resize path — any of left/top/width/height present. chrome.windows requires the   // window be in 'normal' state before bounds take, so force it first.   const hasBounds = ['left', 'top', 'width', 'height'].some((k) => args[k] != null);@@ -271,7 +362,8 @@ export async function windowState(args = {}) {     const b = {};     for (const k of ['left', 'top', 'width', 'height']) if (args[k] != null) b[k] = Math.round(args[k]);     const w = await chrome.windows.update(winId, b);-    return { ok: true, windowId: winId, bounds: { left: w.left, top: w.top, width: w.width, height: w.height }, _hint: 'Moved/resized via chrome.windows — no bar. Use list_windows to read work-area bounds for side-by-side tiling.' };+    const warn = await enforceBackground(winId, args, 'nbrowser_window_state', prev);+    return withWarn({ ok: true, windowId: winId, bounds: { left: w.left, top: w.top, width: w.width, height: w.height }, _hint: 'Moved/resized via chrome.windows — no bar. Use list_windows to read work-area bounds for side-by-side tiling.' }, warn);   }   let state = String(args.state || 'fullscreen').toLowerCase();   if (state === 'restore' || state === 'windowed') state = 'normal';@@ -279,7 +371,13 @@ export async function windowState(args = {}) {     return { ok: false, error: `bad state '${state}'`, _hint: 'state ∈ fullscreen | maximized | normal | minimized — or pass left/top/width/height to move/resize' };   }   await chrome.windows.update(winId, { state });-  return { ok: true, windowId: winId, state, _hint: 'Native window-state change — no debugger, no bar. fullscreen hides tabs+URL bar+taskbar; normal restores.' };+  // maximize/fullscreen are foregrounding actions (they make the window large + can pull it forward).+  // They are USER-only: the window still stays in the BACKGROUND (auto-reverted) unless the user asked to+  // watch live (userRequestedForeground:true). Resizing for a clean recording therefore still works, but+  // the AI can never use maximize/fullscreen to shove the window in front of the user.+  const warn = await enforceBackground(winId, args, 'nbrowser_window_state', prev);+  const raiseState = state === 'maximized' || state === 'fullscreen';+  return withWarn({ ok: true, windowId: winId, state, _hint: 'Native window-state change — no debugger, no bar. fullscreen hides tabs+URL bar+taskbar; normal restores.' + (raiseState ? ' This enlarges the window but keeps it in the BACKGROUND (auto-reverted) unless userRequestedForeground:true. ' + POLITE_HINT : '') }, warn); }  export async function listWindows() {@@ -332,10 +430,15 @@ export async function openTab(args = {}) {       'Pass the sessionId from nbrowser_open_window, or open a new window instead. ' + OPEN_WINDOW_HINT);   }   // 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 };+  // chrome.tabs.create defaults to active:true; force active:false unless the user EXPLICITLY asked to+  // watch live (userRequestedForeground:true). A bare focused:true/active:true is IGNORED.+  const fg = userWantsForeground(args);+  const prev = fg ? null : await currentForeground();+  const base = { url: args.url, active: fg };   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.' };+  const warn = await enforceBackground(windowId, args, 'nbrowser_open_tab', prev); // backstop: opening a tab must not raise the window+  const res = { 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. ' + POLITE_HINT };+  return withWarn(res, warn); }  export async function closeTab(args = {}) {@@ -361,17 +464,22 @@ export async function listTabs(args = {}) { // ---- navigation ------------------------------------------------------------ export async function navigate(args = {}) {   const tabId = await resolveTabId(args, { requireOwned: true, verb: 'nbrowser_navigate' });+  const prev = userWantsForeground(args) ? null : await currentForeground();   await chrome.tabs.update(tabId, { url: args.url });   await waitForLoad(tabId);   const t = await chrome.tabs.get(tabId);-  return { ok: true, url: t.url, tabId: String(tabId), sessionId: args.sessionId };+  const warn = await enforceBackground(t.windowId, args, 'nbrowser_navigate', prev); // navigating must never raise the window+  return withWarn({ ok: true, url: t.url, tabId: String(tabId), sessionId: args.sessionId }, warn); }  export async function reload(args = {}) {   const tabId = await resolveTabId(args, { requireOwned: true, verb: 'nbrowser_reload' });+  const prev = userWantsForeground(args) ? null : await currentForeground();   await chrome.tabs.reload(tabId);   await waitForLoad(tabId);-  return { ok: true, tabId: String(tabId), sessionId: args.sessionId };+  const t = await chrome.tabs.get(tabId);+  const warn = await enforceBackground(t.windowId, args, 'nbrowser_reload', prev);+  return withWarn({ ok: true, tabId: String(tabId), sessionId: args.sessionId }, warn); }  // Route back/forward through the PAGE's own history, not chrome.tabs.goBack/goForward. The tabs API@@ -604,18 +712,22 @@ export async function caption(args = {}) {   return { ok: true, tabId: String(tabId), text: args.text, style: args.style || 'subtitle' }; } -// Activate the tab + flash its taskbar button. NOTE: on Windows this does NOT raise the window-// above other apps (OS foreground-lock blocks a background app from stealing focus — you get a-// taskbar flash, by design, so the user's focus is never yanked). This is the desired behavior.+// Select the tab within its window + (politely) flash its taskbar button. It does NOT raise the window+// above the user's foreground app unless the user EXPLICITLY asked to watch live+// (userRequestedForeground:true). Without that flag we only drawAttention (a taskbar flash) + set the+// active tab — never a focus/raise — so the user's foreground work is never yanked. export async function focus(args = {}) {   const tabId = await resolveTabId(args, { requireOwned: true, verb: 'nbrowser_focus' });   const tab = await chrome.tabs.get(tabId);-  await chrome.windows.update(tab.windowId, { focused: true, drawAttention: true });-  await chrome.tabs.update(tabId, { active: true });-  const t = (tab.title || '').slice(0, 40);+  const fg = userWantsForeground(args);+  await chrome.tabs.update(tabId, { active: true }); // select the tab within its window (no raise)+  if (fg) await chrome.windows.update(tab.windowId, { focused: true }); // user asked → raise+  else { try { await chrome.windows.update(tab.windowId, { drawAttention: true }); } catch {} } // polite taskbar flash only   return {-    ok: true, tabId: String(tabId), windowId: tab.windowId, title: tab.title,-    _hint: `Activated the tab + flashed its taskbar button. chrome.windows focus does NOT raise the window above other apps on Windows — by design, so the user's focus is never stolen. Capture it in place with nbrowser_screenshot or AD desktop_screenshot_window (both work on a background window). Only raise it (AD desktop_bring_to_front {"titleContains":${JSON.stringify(t)}}) if the user EXPLICITLY asked to watch it live.`,+    ok: true, tabId: String(tabId), windowId: tab.windowId, title: tab.title, raised: fg,+    _hint: fg+      ? 'Raised for the user (explicit userRequestedForeground:true).'+      : `Did NOT raise the window — only selected the tab + flashed its taskbar button, so the user's focus is never stolen. ${POLITE_HINT} Capture it in place with nbrowser_screenshot (works on a background/occluded window). Raise it only on explicit user request (pass userRequestedForeground:true).`,   }; } 
extension/src/sw.js+6−3
@@ -128,9 +128,12 @@ async function onMessage(msg) {     // First-contact safety banner: teach the cardinal rule once per SW lifetime, in-band.     if (!_safetySent && payload && typeof payload === 'object' && !Array.isArray(payload)) {       _safetySent = true;-      payload._safety = '\u26d4 Cardinal rule: only drive windows YOU open (nbrowser_open_window \u2192 sessionId). ' +-        'Bare mutating verbs and targets in user-opened windows are refused (no_target / not_owned). ' +-        'Start with nbrowser_profiles to pick the profile matching the identity your task needs; work in the BACKGROUND.';+      payload._safety = '\u26d4 Cardinal rules: (1) only drive windows YOU open (nbrowser_open_window \u2192 sessionId); ' ++        'bare mutating verbs and targets in user-opened windows are refused (no_target / not_owned). ' ++        '(2) Work in the BACKGROUND. NEVER raise or maximize a window over the user\u2019s other work, and NEVER fall back ' ++        'to OS-level launches (Start-Process/launch) or bring_to_front/maximize to surface a window \u2014 it is rude and ' ++        'is auto-reverted + logged as a focus_violation. Foregrounding is the user\u2019s choice alone (userRequestedForeground:true). ' ++        'Start with nbrowser_profiles to pick the profile matching the identity your task needs.';     }     reply(id, true, payload);   } catch (e) {
native-browser-bridge-manifest.json+6−6
@@ -1,8 +1,8 @@ {   "manifest_version": 1,-  "version": "0.0.39",-  "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.0.39/native-browser-bridge-v0.0.39.bin",-  "sha256": "56d4a9e8ebcc8c20f941b67760bb040c4b9b1f7b7cb67bb39d96e260e752fc55",-  "size": 44645,-  "released_at": "2026-07-06T00:00:00Z"-}\ No newline at end of file+  "version": "0.0.40",+  "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.0.40/native-browser-bridge-v0.0.40.bin",+  "sha256": "e6e1103824a024513d7ca48219625b1858171dbd597927f96b318174423c4e73",+  "size": 45506,+  "released_at": "2026-07-08T00:00:00Z"+}