← 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
8 files changed
+173−55
bridge/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.0.39+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"-}+ "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"+}