← Commit history

v0.16.35 (insiders): nb-bridge#53: an open with no profile is refused when several signed-in identities are connected (profile_required, listing them), window and tab replies say which identity they acted as, nbrowser_status lists connected profiles as its hint always promised, and unlabelled profile rows explain themselves; nb-bridge#51 cross-platform half: the Node path is no longer Windows-mangled on macOS, browser and profile detection and launching know the macOS layout, and nbrowser_credentials refuses with unsupported_platform instead of answering 'no saved logins'; nb-bridge#52: nbrowser_open_window accepts state:'minimized' to open out of sight (extension 0.2.16)

John Lauer ·7eaae3568d ·28d ago ·parent 6235f7d
9 files changed +132−30
bin/nb-dashboard+1−1
@@ -20,7 +20,7 @@ const { execFile, spawn } = require('child_process'); const fs = require('fs'); const crypto = require('crypto'); -const VERSION = '0.16.34';           // keep in lockstep with package.json (pkg version)+const VERSION = '0.16.35';           // keep in lockstep with package.json (pkg version) // SEND TO AI IS UN-PARKED (John: "yeah, we are re-enabling the send to ai. it seems to be good now"). // The ah build thread certified the crash family dead on the gate-fixed build and the delivery leg is // proven end to end. The park const is DELETED rather than flipped, so no dead branch can resurrect it.
bridge/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.16.34\ No newline at end of file+0.16.35\ No newline at end of file
bridge/bridge.json+2−2
@@ -2,8 +2,8 @@   "manifest_version": 1,   "name": "native-browser",   "displayName": "Adom Native Browser",-  "version": "0.16.34",-  "expectedExtensionVersion": "0.2.15",+  "version": "0.16.35",+  "expectedExtensionVersion": "0.2.16",   "description": "Drives the user's REAL signed-in Chrome/Edge, in two layers. Layer 1 (nb) needs NOTHING installed and works on any machine: nbrowser_open_os_window (open a URL in the user's real profile, background+flashing, the OAuth/consent path), nbrowser_os_windows, nbrowser_profiles, nbrowser_wake_profile, nbrowser_force_close. Layer 2 (nbe, the Adom Native Browser Extension) unlocks the rest: CDP driving (navigate/click/read/screenshot) inside their logged-in profile. The bridge binds an ephemeral loopback port and the native-messaging host pumps stdio <-> bridge.",   "homepage": "https://wiki.adom.inc/adom/nb-bridge",   "author": "Adom Inc.",
bridge/package.json+1−1
@@ -1,6 +1,6 @@ {   "name": "adom-native-browser-bridge",-  "version": "0.16.34",+  "version": "0.16.35",   "private": true,   "description": "Native-browser bridge for Bridge (ab): layer 1 (nb) plus layer 2 via nbe, the Adom Native Browser Extension (Chrome + Edge).",   "main": "server.js",
bridge/server.js+98−21
@@ -269,7 +269,13 @@ function registerProvisional(sock) { // Edge autofill them at login time). A browser EXTENSION can't read this (sandbox); the bridge runs // natively, so it reads the `Login Data` SQLite for BOTH Chromium browsers. Dependency-free leaf reader. const LOCALAPP = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');-const BROWSER_UD = {+// Where each browser keeps its profiles. macOS puts them under Application Support with no "User Data"+// level; the Local State / Preferences JSON inside is the same shape (Drew Owens, nb-bridge#51).+const MAC_SUPPORT = path.join(os.homedir(), 'Library', 'Application Support');+const BROWSER_UD = process.platform === 'darwin' ? {+  chrome: path.join(MAC_SUPPORT, 'Google', 'Chrome'),+  edge: path.join(MAC_SUPPORT, 'Microsoft Edge'),+} : {   chrome: path.join(LOCALAPP, 'Google', 'Chrome', 'User Data'),   edge: path.join(LOCALAPP, 'Microsoft', 'Edge', 'User Data'), };@@ -908,9 +914,8 @@ function avatarForProfile(profileKey) {     const i = profileKeyResolved.indexOf(':');     const browser = i >= 0 ? profileKeyResolved.slice(0, i) : 'chrome';     const email = i >= 0 ? profileKeyResolved.slice(i + 1) : profileKeyResolved;-    const local = process.env.LOCALAPPDATA;-    if (local && email.includes('@')) {-      const root = browser === 'edge' ? path.join(local, 'Microsoft', 'Edge', 'User Data') : path.join(local, 'Google', 'Chrome', 'User Data');+    if (BROWSER_UD[browser] && email.includes('@')) {+      const root = BROWSER_UD[browser];       const j = JSON.parse(fs.readFileSync(path.join(root, 'Local State'), 'utf8'));       const cache = (j.profile && j.profile.info_cache) || {};       for (const [dir, info] of Object.entries(cache)) {@@ -1359,6 +1364,12 @@ let _chromeInFlight = null; // a fresh machine it is the browser; Chrome is optional and is only worth installing when the user asks for // Chrome by name or when NO supported browser exists at all. function detectEdge() {+  if (process.platform === 'darwin') {+    for (const c of ['/Applications/Microsoft Edge.app', path.join(os.homedir(), 'Applications', 'Microsoft Edge.app')]) {+      try { if (fs.existsSync(c)) return path.join(c, 'Contents', 'MacOS', 'Microsoft Edge'); } catch {}+    }+    return null;+  }   for (const p of ['C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', 'C:/Program Files/Microsoft/Edge/Application/msedge.exe']) { try { if (fs.existsSync(p)) return p; } catch {} }   return null; }@@ -1370,6 +1381,12 @@ function nativeBrowserSummary() {   return { chromeInstalled: !!chrome, edgeInstalled: !!edge, nativeBrowserPresent: !!(chrome || edge), chromeNeeded: !(chrome || edge), edgeProfiles: edgeKeys, chromeProfiles: chromeKeys }; } function detectChrome() {+  if (process.platform === 'darwin') {+    for (const c of ['/Applications/Google Chrome.app', path.join(os.homedir(), 'Applications', 'Google Chrome.app')]) {+      try { if (fs.existsSync(c)) return path.join(c, 'Contents', 'MacOS', 'Google Chrome'); } catch {}+    }+    return null;+  }   if (process.platform !== 'win32') return null;   const home = process.env.USERPROFILE || os.homedir();   const pf = process.env['ProgramFiles'] || 'C:\\Program Files';@@ -2048,7 +2065,7 @@ function dispatch(verb, args) {         d('nbrowser_prewarm', 'READ-ONLY tally of browsers, profiles and where nbe is installed / off / missing (ab runs it after an install). readiness recomputes it live.', {}, { ok: 'bool', summary: 'str', asOf: 'str', browsers: 'obj', tally: 'obj', installedIn: 'arr', disabledIn: 'arr', missingIn: 'arr', profiles: 'arr', offer: 'str?' }, 60, { example: {} }),         d('nbrowser_chrome_status', 'READ-ONLY: is native Google Chrome installed / how is an install going? Poll after nbrowser_install_chrome.', {}, { notifications: '{checked,toastsEnabled,adomBridgeEnabled,toastMayBeMuted,_hint} - Windows toast switches, so a caller knows when the Approve-now toast could not have been seen', canRetryUac: 'bool', 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. REGISTRATION REQUIRED: sessionId + thread + purpose, or refused (session_unregistered). Never steals focus; a topmost driven window is auto-reverted (focus_violation).', { left: 'optional int (logical px)', top: 'optional int', width: 'optional int (default: the newWindowSize setting; fill = the work area inset 10 px, pup R1)', height: 'optional int', state: 'optional "maximized"|"normal"', sessionId: 'required str (your short task id)', thread: 'required str (which AI thread you are)', purpose: 'required str (human-readable, e.g. "finishing John\'s IRS signup")', url: 'required str', profile: P, userRequestedForeground: 'optional bool - raise to foreground ONLY when the user asked or a browser/auth feature requires it (default false)', foregroundReason: 'REQUIRED with userRequestedForeground - human-readable why; without it the window STAYS background. Warn the user via ab notify_user FIRST.' }, { ok: 'bool', sessionId: 'str', tabId: 'str', windowId: 'int', hwnd: 'int (OS window handle - use with desktop_screenshot_window to capture the window+chrome+child popups)', url: 'str', _warning: 'str?', keptBackground: 'bool?' }, 30, { example: { sessionId: 'irs', thread: 'thread-7', purpose: 'finishing John IRS signup', url: 'https://wiki.adom.inc' } }),+        d('nbrowser_open_window', 'Open a NEW window in the BACKGROUND (unfocused) and return a sessionId to drive it. REGISTRATION REQUIRED: sessionId + thread + purpose, or refused (session_unregistered). Never steals focus; a topmost driven window is auto-reverted (focus_violation).', { left: 'optional int (logical px)', top: 'optional int', width: 'optional int (default: the newWindowSize setting; fill = the work area inset 10 px, pup R1)', height: 'optional int', state: 'optional "maximized"|"normal"', sessionId: 'required str (your short task id)', thread: 'required str (which AI thread you are)', purpose: 'required str (human-readable, e.g. "finishing John\'s IRS signup")', url: 'required str', profile: P, userRequestedForeground: 'optional bool - raise to foreground ONLY when the user asked or a browser/auth feature requires it (default false)', foregroundReason: 'REQUIRED with userRequestedForeground - human-readable why; without it the window STAYS background. Warn the user via ab notify_user FIRST.' }, { ok: 'bool', profile: 'str (the identity this window is in)', email: 'str', displayName: 'str', sessionId: 'str', tabId: 'str', windowId: 'int', hwnd: 'int (OS window handle - use with desktop_screenshot_window to capture the window+chrome+child popups)', url: 'str', _warning: 'str?', keptBackground: 'bool?' }, 30, { example: { sessionId: 'irs', thread: 'thread-7', purpose: 'finishing John IRS signup', url: 'https://wiki.adom.inc' } }),         d('nbrowser_close_window', 'Close a window the agent opened. includeSpawned:true also closes everything that session SPAWNED (composes/popups).', { sessionId: 'required str', includeSpawned: 'optional bool (also close this session\'s spawned windows)', profile: P }, { ok: 'bool', closed: 'bool', sessionId: 'str', alsoClosedSpawned: '[int]' }, 20, { example: { sessionId: 'work', includeSpawned: true } }),         d('nbrowser_owned', 'List everything a session owns: opened windows + adopted SPAWNED windows/tabs (compose/popup/target=_blank a driven page opened). Acknowledges spawns.', { sessionId: 'optional str (all sessions if omitted)', profile: P }, { ok: 'bool', opened: '[{sessionId,windowId,url,title,purpose,ageMs}]', spawned: '[{kind,windowId,tabId,url,ageMs,spawnedBy,opener}]' }, 20, { example: { sessionId: 'irs' } }),         d('nbrowser_cleanup', 'Close ONLY what a session SPAWNED (leaked composes/popups). Never the user\'s windows, never your primary opened window.', { sessionId: 'required str', profile: P }, { ok: 'bool', closed: '[{windowId,url}]', count: 'int', keptOpenedWindow: 'int?' }, 20, { example: { sessionId: 'irs' } }),@@ -2145,13 +2162,17 @@ function dispatch(verb, args) {       const list = [...conns.entries()].map(([k, c]) => {         const row = { profile: k, label: profileLabel(c.profile), email: c.profile.email || '', browser: c.profile.browser || 'chrome', active: k === defKey, blocked: isBlocked(k), live: !!(c.sock && !c.sock.destroyed) };         row.origin = c.origin ? c.origin.owner : 'checking';+        // What a row with no email/displayName IS (nb-bridge#53 ask 4): either pup's own browser, or a+        // profile whose identity has not resolved yet. The SAME email can appear twice legitimately: once+        // per browser (chrome:you@x and edge:you@x are different windows with different cookies).+        if (!row.email) row.note = isPupConn(c) ? "pup's own browser window, NOT the user's browser. Never the default target."+          : 'identity not resolved yet (the profile is signed out, or its hello has not landed). It is still drivable, but do not assume whose account it is.';         if (isPupConn(c)) row.note = "pup's own browser window, NOT the user's browser. Never the default target; name it explicitly only if you really mean pup's window (and prefer pup_* verbs for that).";         const av = avatarForProfile(k);         if (av) { row.profileDir = av.profileDir; row.hasAvatar = !!av.avatarPath; }         try {           if (row.email) {-            const local = process.env.LOCALAPPDATA;-            const root = row.browser === 'edge' ? path.join(local, 'Microsoft', 'Edge', 'User Data') : path.join(local, 'Google', 'Chrome', 'User Data');+            const root = BROWSER_UD[row.browser] || BROWSER_UD.chrome;             const jj = JSON.parse(fs.readFileSync(path.join(root, 'Local State'), 'utf8'));             for (const [dir, info] of Object.entries((jj.profile && jj.profile.info_cache) || {})) {               if (String((info && info.user_name) || '').toLowerCase() === row.email.toLowerCase()) { row.displayName = String((info && info.name) || ''); row.profileDir = dir; break; }@@ -2320,9 +2341,10 @@ function dispatch(verb, args) {       for (const w of curWins) { const t = String(w.title || ''); if (BRAND.test(t) && WORKSPACE.test(t)) protectedHwnds.add(w.hwnd); }       for (const [sid, pk] of sessionProfile) { if (pk === kp.key && sid !== a.sessionId && !osOpened.has(sid)) { const h = sessionHwnd.get(sid); if (h) protectedHwnds.add(h); } }       const local = process.env.LOCALAPPDATA;+      // detectChrome/detectEdge know the macOS app bundles too, so a Mac can launch its own browser (nb-bridge#51).       const exes = kp.browser === 'edge'-        ? ['C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', 'C:/Program Files/Microsoft/Edge/Application/msedge.exe']-        : ['C:/Program Files/Google/Chrome/Application/chrome.exe', 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe', local ? path.join(local, 'Google', 'Chrome', 'Application', 'chrome.exe') : ''];+        ? [detectEdge(), 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', 'C:/Program Files/Microsoft/Edge/Application/msedge.exe']+        : [detectChrome(), 'C:/Program Files/Google/Chrome/Application/chrome.exe', 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe', local ? path.join(local, 'Google', 'Chrome', 'Application', 'chrome.exe') : ''];       const exe = exes.find((e) => e && fs.existsSync(e));       if (!exe) return resolve({ success: false, error: `${kp.browser} executable not found at a standard path` });       // AUTHORITATIVE ORDER: a LIVE native-messaging connection for this profile proves the extension is@@ -2623,9 +2645,10 @@ function dispatch(verb, args) {       if (!kp) return resolve({ success: false, errorCode: _rk2.ambiguous ? 'profile_ambiguous' : 'profile_not_found', candidates: _rk2.candidates, error: `no local browser profile matches '${want}'`, _hint: (_rk2.candidates && _rk2.candidates.length ? `Profiles on this machine: ${_rk2.candidates.join(', ')}. ` : '') + 'nbrowser_profiles lists every profile on this machine (live + asleep). Check the email/browser.' });       if (isBlocked(kp.key)) return resolve({ success: false, error: `profile '${kp.key}' is blocklisted`, _hint: 'The user blocked this profile. nbrowser_profile_unblock first (with the user\'s consent).' });       const local = process.env.LOCALAPPDATA;+      // detectChrome/detectEdge know the macOS app bundles too, so a Mac can launch its own browser (nb-bridge#51).       const exes = kp.browser === 'edge'-        ? ['C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', 'C:/Program Files/Microsoft/Edge/Application/msedge.exe']-        : ['C:/Program Files/Google/Chrome/Application/chrome.exe', 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe', local ? path.join(local, 'Google', 'Chrome', 'Application', 'chrome.exe') : ''];+        ? [detectEdge(), 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', 'C:/Program Files/Microsoft/Edge/Application/msedge.exe']+        : [detectChrome(), 'C:/Program Files/Google/Chrome/Application/chrome.exe', 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe', local ? path.join(local, 'Google', 'Chrome', 'Application', 'chrome.exe') : ''];       const exe = exes.find((e) => e && fs.existsSync(e));       if (!exe) return resolve({ success: false, error: `${kp.browser} executable not found at a standard path` });       // --no-startup-window loads the profile (and its extensions) in BACKGROUND mode: NO window is@@ -2682,6 +2705,13 @@ function dispatch(verb, args) {       const match = args && (args.match || args.vendor || args.host);       const browser = args && args.browser;   // optional: "chrome" | "edge"       const profile = args && (args.profile || args.email); // optional: a specific profile email+      // On a platform whose credential store nb cannot read, an empty list READS AS "this user has no saved+      // logins" and the AI plans around a false fact. Refuse explicitly instead (Drew Owens, nb-bridge#51).+      if (process.platform !== 'win32') {+        return resolve({ success: false, errorCode: 'unsupported_platform',+          error: `nbrowser_credentials reads the browser's saved-login store, which nb can only read on Windows today (this box is ${process.platform}).`,+          _hint: 'This is NOT "the user has no saved logins" - nb simply cannot see them here. Ask the user whether they have a login for the site, or just drive the site and let the browser autofill it: their real browser still fills its own saved passwords.' });+      }       const rows = queryCredentials({ browser, profile, match });       return resolve({ success: true, output: JSON.stringify({ ok: true, count: rows.length, credentials: rows, _hint: 'Read-only query of the BROWSER\'s own saved-login store (Chrome + Edge) - we do NOT store or replace passwords; the browser stays the source of truth. Each row = {browser, profile, host, username} you CAN auto-login to (password is NOT exposed - Chrome/Edge autofill it when you focus the login field). Flow: credentials {match:"digikey"} -> if present, navigate to the sign-in page -> click the user/email field (autofills) -> click submit -> login_state. Filters: match:"<vendor>", browser:"chrome"|"edge", profile:"<email>".' }) });     }@@ -2966,6 +2996,27 @@ function dispatch(verb, args) {         logActivity({ verb: '_cross_thread', sessionId: args.sessionId, ok: true, ops: `${nowCaller} -> ${owner}`, internal: true });       }     }+    // WRONG-ACCOUNT GUARD (John, nb-bridge#53, 2026-10-14): an agent opened a window with no `profile`,+    // landed in whichever identity happened to be active ([email protected]), and got as far as Google's+    // account chooser for a sign-in meant to be John's. Driving the wrong account is a worse mistake than+    // an untracked window, which nb already refuses. So: with MORE THAN ONE identity connected, a window+    // or tab opened with no profile and no session pin is REFUSED, naming the exact values to pass. One+    // identity connected = defaulting is unambiguous and still allowed.+    if ((verb === 'nbrowser_open_window' || verb === 'nbrowser_open_tab') && !(args && (args.profile || args.profileEmail))+        && !(args && args.sessionId && sessionProfile.get(args.sessionId))) {+      const ids = userConns();+      if (ids.length > 1) {+        const choices = ids.map(([k, c]) => {+          const av = avatarForProfile(k) || {};+          return { profile: k, email: (c.profile && c.profile.email) || '', displayName: profileLabel(c.profile) || null, profileDir: av.profileDir || null };+        });+        return resolve({ success: false, errorCode: 'profile_required',+          error: `${ids.length} signed-in identities are connected, so '${verb}' will not guess which one to act as: ${choices.map((x) => x.profile).join(', ')}. Pass profile:"<one of these>".`,+          profiles: choices,+          _hint: 'Acting as the wrong account is expensive to undo (posting, buying, signing in). Ask the user which identity this task belongs to, or infer it from the task (work vs personal), then retry with profile:"<browser>:<email>". nbrowser_profiles lists them with their browser labels.',+          _next: choices.map((x) => `retry with profile:"${x.profile}"${x.displayName ? ` (${x.displayName})` : ''}`) });+      }+    }     // For open_window, snapshot the Chrome window list BEFORE forwarding so the post-open diff reliably     // catches the new window (the old fire-and-forget snapshot raced the window appearing).     if (verb === 'nbrowser_open_window') {@@ -2995,8 +3046,33 @@ function downscalePng(src, dst, max) {     });   }); }+// Who a reply acted AS. nb-bridge#53: an agent should see "[email protected]" in the open_window reply, not+// discover it at a sign-in page three calls later.+function identityOf(key) {+  const c = conns.get(key); if (!c) return null;+  const av = avatarForProfile(key) || {};+  return { profile: key, email: (c.profile && c.profile.email) || '', displayName: profileLabel(c.profile) || null, profileDir: av.profileDir || null, browser: (c.profile && c.profile.browser) || 'chrome' };+} async function decorateReply(p, frame, payloadIn, setPayload) {   let payload = payloadIn;+      // The identity this call acted as, on every window/tab open (nb-bridge#53).+      if ((p.verb === 'nbrowser_open_window' || p.verb === 'nbrowser_open_tab' || p.verb === 'nbrowser_open_os_window') && payload && payload.ok !== false) {+        const id = identityOf(p.profileKey);+        if (id) payload = { ...payload, profile: id.profile, email: id.email, displayName: id.displayName, actingAs: `${id.displayName || id.profile}${id.email ? ` <${id.email}>` : ''}` };+      }+      // nbrowser_status promises "connected profiles + active browser" in its own hint but never listed+      // them; only nbrowser_profiles did (nb-bridge#53 ask 2). The bridge owns this knowledge, so add it.+      if (p.verb === 'nbrowser_status' && payload && payload.ok !== false) {+        try {+          const def = defaultConnKey();+          const rows = [...conns.entries()].filter(([, c]) => c.sock && !c.sock.destroyed).map(([k, c]) => {+            const id = identityOf(k) || { profile: k };+            return { ...id, active: k === def, origin: c.origin ? c.origin.owner : 'checking', version: c.version || null };+          });+          const asleep = knownProfiles().filter((kp) => !conns.has(kp.key)).map((kp) => ({ profile: kp.key, email: kp.email || '', displayName: kp.displayName || null, profileDir: kp.profileDir, asleep: true }));+          payload = { ...payload, profiles: rows, asleepProfiles: asleep, activeProfile: def };+        } catch {}+      }       // Screenshot file-save: huge base64 over the relay times out, so when the caller passes       // toFile:true we write the PNG to a laptop temp file here and return its path instead of the       // base64. Pull it with `adom-bridge pull_file` (binary chunked) - fast + no relay timeout.@@ -3280,7 +3356,9 @@ function writeHostFile(hostPort) {   // Publish the Node ab gave us so the Chrome-launched host can reuse it (see NODEPATHFILE note above).   // Normalize to native Windows backslashes - node-path.txt is consumed by a .bat, and a quoted   // forward-slash exe path is not reliably executable in cmd.exe.-  try { fs.writeFileSync(NODEPATHFILE, process.execPath.split('/').join('\\')); } catch {}+  // Backslashes ONLY on Windows, where a .bat consumes this. On macOS the launcher is a shell script and+  // a converted path cannot be exec'd (Drew Owens, nb-bridge#51).+  try { fs.writeFileSync(NODEPATHFILE, process.platform === 'win32' ? process.execPath.split('/').join('\\') : process.execPath); } catch {} } // PID-aware cleanup: ONLY delete host.json if it still points at *this* process. Otherwise a churning // instance (ab respawn, a manual run, a crash-restart) deletes the discovery file that another LIVE@@ -3380,9 +3458,10 @@ function nbeInventory() { } function knownProfiles() {   const out = [];-  const local = process.env.LOCALAPPDATA;-  if (!local) return out;-  const roots = [['chrome', path.join(local, 'Google', 'Chrome', 'User Data')], ['edge', path.join(local, 'Microsoft', 'Edge', 'User Data')]];+  // BROWSER_UD knows each OS's layout (Windows User Data vs macOS Application Support), so profiles are+  // found on a Mac too (nb-bridge#51).+  const roots = [['chrome', BROWSER_UD.chrome], ['edge', BROWSER_UD.edge]];+  if (!roots.some(([, r]) => r)) return out;   for (const [browser, root] of roots) {     try {       const j = JSON.parse(fs.readFileSync(path.join(root, 'Local State'), 'utf8'));@@ -3433,9 +3512,8 @@ function resolveKnownProfile(want, browserHint) { // the AI deserves to know which one it is (real gripe: an ah thread flailed at Edge for a whole session). function adomExtStateForProfile(browser, profileDir) {   try {-    const local = process.env.LOCALAPPDATA;-    if (!local || !profileDir) return { checked: false };-    const root = browser === 'edge' ? path.join(local, 'Microsoft', 'Edge', 'User Data') : path.join(local, 'Google', 'Chrome', 'User Data');+    const root = BROWSER_UD[browser] || BROWSER_UD.chrome;+    if (!root || !profileDir) return { checked: false };     for (const f of ['Secure Preferences', 'Preferences']) {       let j; try { j = JSON.parse(fs.readFileSync(path.join(root, profileDir, f), 'utf8')); } catch { continue; }       const settings = (j.extensions && j.extensions.settings) || {};@@ -3467,9 +3545,8 @@ function adomExtStateForProfile(browser, profileDir) { // "your Claude extension can work this page interactively; nbe adds full automation for any AI." function claudeExtStateForProfile(browser, profileDir) {   try {-    const local = process.env.LOCALAPPDATA;-    if (!local || !profileDir) return { checked: false };-    const root = browser === 'edge' ? path.join(local, 'Microsoft', 'Edge', 'User Data') : path.join(local, 'Google', 'Chrome', 'User Data');+    const root = BROWSER_UD[browser] || BROWSER_UD.chrome;+    if (!root || !profileDir) return { checked: false };     for (const f of ['Secure Preferences', 'Preferences']) {       let j; try { j = JSON.parse(fs.readFileSync(path.join(root, profileDir, f), 'utf8')); } catch { continue; }       const settings = (j.extensions && j.extensions.settings) || {};
extension/manifest.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 3,   "name": "Adom Native Browser Extension",   "short_name": "Adom",-  "version": "0.2.15",+  "version": "0.2.16",   "description": "Layer 2 of Adom Native Browser (nb): lets your Adom AI drive your real, signed-in Chrome or Edge, using your own logins.",   "minimum_chrome_version": "116",   "background": {
extension/src/native.js+7
@@ -529,6 +529,13 @@ export async function openWindow(args = {}) {   const createArgs = { url: args.url, focused: fg };   for (const k of ['left', 'top', 'width', 'height']) { const v = Number(args[k]); if (args[k] != null && Number.isFinite(v) && v >= 0) createArgs[k] = Math.round(v); }   if (['maximized', 'normal'].includes(args.state) && !('width' in createArgs)) createArgs.state = args.state;+  // state:"minimized" opens OUT OF SIGHT in one step (Drew Owens, nb-bridge#52). On macOS nb cannot yet+  // order a window behind the user's app, so a new window is DRAWN OVER their work even though it never+  // takes keyboard focus; opening minimized is the way to stay out of the way there. A minimized window+  // is fully drivable (eval, navigate, wait_for, screenshots). Chrome refuses state with bounds, so the+  // bounds are dropped for this case and applied later only if the caller restores the window.+  const wantMin = String(args.state || '').toLowerCase() === 'minimized';+  if (wantMin) { for (const k of ['left', 'top', 'width', 'height']) delete createArgs[k]; createArgs.state = 'minimized'; }   const win = await chrome.windows.create(createArgs);   const sid = args.sessionId;   sessionWindows.set(sid, win.id);
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "nb-bridge",-  "version": "0.16.34",+  "version": "0.16.35",   "type": "app",   "title": "NB - Native Browser Bridge",   "description": "This package installs the SKILLS into your container so your AI knows how to drive your real signed-in Chrome and Edge; Bridge loads the bridge runtime itself from the release zip.",
tools/verb-battery.mjs+20−2
@@ -73,6 +73,24 @@ const T = {   profiles: () => { const r = call('nbrowser_profiles'); const ps = r.profiles || []; ctx.profileCount = ps.length; const def = ps.find((p) => p.profile === r.active); const pups = ps.filter((p) => p.origin === 'pup').length; return A(ps.length > 0 && !(def && def.origin === 'pup'), `${ps.length} profiles, default ${r.active} (origin ${def ? def.origin : 'none'}), ${pups} pup connection(s) set aside`, `default is pup's browser or no profiles: ${JSON.stringify(r).slice(0, 160)}`); },   resolve_profiles: () => { const r = call('nbrowser_resolve_profiles'); return A(r.ok !== false, 'resolved', JSON.stringify(r).slice(0, 120)); },   // An Edge-only box is a PASS: nb drives whatever native browser is there (nb-bridge#46).+  // nb-bridge#53: with several signed-in identities connected, an unnamed open is REFUSED rather than+  // acting as whichever profile happens to be active (an agent nearly posted as the wrong person).+  guard_profile_required: () => {+    const live = (call('nbrowser_profiles').profiles || []).filter((p) => p.live && p.origin !== 'pup');+    const r = call('nbrowser_open_window', { sessionId: SID + '-noprof', thread: THREAD, purpose: 'battery: unnamed open must be refused when several identities are connected', url: HARNESS });+    if (live.length < 2) { if (r.ok === true) call('nbrowser_close_window', { sessionId: SID + '-noprof' }); return A(true, `[skip] only ${live.length} identity connected - defaulting is unambiguous, guard not applicable`, ''); }+    if (r.ok === true) { call('nbrowser_close_window', { sessionId: SID + '-noprof' }); return [false, 'unnamed open was ALLOWED with several identities connected']; }+    return A(r.errorCode === 'profile_required' && Array.isArray(r.profiles) && r.profiles.length === live.length, `refused: ${r.errorCode}, listed ${(r.profiles || []).length} identities`, JSON.stringify(r).slice(0, 200));+  },+  // nb-bridge#52: a window can be opened OUT OF SIGHT in one step and stays drivable there.+  open_minimized: () => {+    const sid = SID + '-min';+    const o = call('nbrowser_open_window', { sessionId: sid, thread: THREAD, purpose: 'battery: minimized open', profile: PROFILE, url: HARNESS, state: 'minimized' });+    const st = o.ok === true ? call('nbrowser_window_state', { sessionId: sid }) : {};+    const ev = o.ok === true ? call('nbrowser_eval', { sessionId: sid, expression: '1+1' }) : {};+    if (o.ok === true) call('nbrowser_close_window', { sessionId: sid });+    return A(o.ok === true && String(st.state || '').toLowerCase() === 'minimized' && (ev.result === 2 || ev.ok === true), `opened minimized (state:${st.state}) and still drivable`, JSON.stringify({ o: String(o.error || o.ok), st: st.state, ev: ev.result }).slice(0, 200));+  },   // The nbe tally (nb-bridge#50): browsers, profiles, where nbe is. Must be fresh: compare against readiness's live copy.   prewarm: () => { const r = call('nbrowser_prewarm'); const rd = call('nbrowser_readiness'); const t = r.tally || {}; const live = (rd.nbe && rd.nbe.tally) || {}; const fresh = Date.now() - Date.parse(r.asOf || 0) < 60000; const same = JSON.stringify(t) === JSON.stringify(live); return A(r.ok === true && t.profiles > 0 && fresh && same && Array.isArray(r.installedIn), `${r.summary}; readiness agrees`, `tally ${JSON.stringify(t)} vs readiness ${JSON.stringify(live)} fresh:${fresh}`); },   chrome_status: () => { const r = call('nbrowser_chrome_status'); const ok = r.installed === true || (r.nativeBrowserPresent === true && r.chromeNeeded === false && r.edgeInstalled === true); return A(ok, r.installed ? `chrome installed, phase:${r.phase}` : 'edge-only box: chromeNeeded:false, Chrome reported optional', JSON.stringify(r).slice(0, 140)); },@@ -271,7 +289,7 @@ const T = { // ─── coverage map: EVERY VERB_META verb decides its fate here ─────────────── export const COVERAGE = {   status: 'live', readiness: 'live', ping: 'live', progress: 'live', scroll: 'live', input_unknown_type: 'guard', hover_raw_xy: 'live', apply_settings: 'skip:internal bridge-to-extension push, exercised by the settings test through nbrowser_settings', whoami: 'live', profiles: 'live',-  resolve_profiles: 'live', prewarm: 'live', chrome_status: 'live', describe: 'live', activity: 'live',+  resolve_profiles: 'live', prewarm: 'live', guard_profile_required: 'guard', open_minimized: 'live', chrome_status: 'live', describe: 'live', activity: 'live',   os_windows: 'live', wake_profile: 'live', rescan: 'live', resync_sessions: 'live',   open_window: 'live', pin_session: 'live', use_profile: 'live', list_windows: 'live',   window_state: 'live', switch_window: 'live', navigate: 'live', reload: 'live',@@ -349,7 +367,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {   // are an ab lifecycle fact, not a verb regression.   call('nbrowser_wake_profile', { profile: PROFILE });   for (let i = 0; i < 10; i++) { const r = call('nbrowser_readiness'); if (r.ready === true) { console.log(`warm-up: ready after ${i} poll(s)\n`); break; } execFileSync('sleep', ['3']); }-  const order = ['status','readiness','ping','whoami','profiles','resolve_profiles','prewarm','chrome_status','describe','activity','os_windows','wake_profile','rescan','resync_sessions',+  const order = ['status','readiness','ping','whoami','profiles','resolve_profiles','prewarm','guard_profile_required','open_minimized','chrome_status','describe','activity','os_windows','wake_profile','rescan','resync_sessions',     'open_window','pin_session','use_profile','list_windows','window_state','switch_window',     'navigate','reload','back','forward','wait','wait_for','keepalive',     'eval','evaluate','screenshot','screenshot_full_res','errors','fetch_url',