← Commit history

v0.16.24 (insiders): A profile with no signed-in account connects under an id key (edge:p_...); nbrowser_wake_profile and nbrowser_open_os_window now accept that key back (mapped to the browser's one anonymous profile, ambiguity refused with the candidates named) instead of answering no local browser profile matches. Found by the verb battery on a fresh Edge-only VM. Battery: an Edge-only box passes chrome_status, and the upload staging and download cleanup use the target machine's own user folder instead of a hard-coded one.

John Lauer ·f940cfabe4 ·1mo ago ·parent 054d7bc
7 files changed +41−14
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.23';           // keep in lockstep with package.json (pkg version)+const VERSION = '0.16.24';           // 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.23\ No newline at end of file+0.16.24\ No newline at end of file
bridge/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "native-browser",   "displayName": "Adom Native Browser",-  "version": "0.16.23",+  "version": "0.16.24",   "expectedExtensionVersion": "0.2.15",   "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",
bridge/package.json+1−1
@@ -1,6 +1,6 @@ {   "name": "adom-native-browser-bridge",-  "version": "0.16.23",+  "version": "0.16.24",   "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+26−5
@@ -2189,8 +2189,8 @@ function dispatch(verb, args) {       const want = String(a.profile || a.email || '');       const wantEmail = (want.includes(':') ? want.split(':').slice(1).join(':') : want).toLowerCase();       const wantBrowser = want.includes(':') ? want.split(':')[0] : (a.browser || null);-      const kp = knownProfiles().find((pr) => (pr.email.toLowerCase() === wantEmail || pr.profileDir.toLowerCase() === wantEmail) && (!wantBrowser || pr.browser === wantBrowser));-      if (!kp) return resolve({ success: false, error: `no local browser profile matches '${want}'`, _hint: 'nbrowser_profiles lists every profile on this machine (live + asleep) with emails and displayNames - pick the identity the task needs.' });+      const _rk = resolveKnownProfile(want, wantBrowser); const kp = _rk.kp;+      if (!kp) return resolve({ success: false, errorCode: _rk.ambiguous ? 'profile_ambiguous' : 'profile_not_found', candidates: _rk.candidates, error: `no local browser profile matches '${want}'`, _hint: (_rk.ambiguous ? `'${want}' is an id key for a profile with no signed-in account, and this machine has several candidates (${(_rk.candidates || []).join(', ')}): pass one of those directory keys instead. ` : '') + 'nbrowser_profiles lists every profile on this machine (live + asleep) with emails and displayNames - pick the identity the task needs.' });       if (isBlocked(kp.key)) return resolve({ success: false, error: `profile '${kp.key}' is blocklisted`, _hint: 'The user blocked this profile from being driven.' });       if (a.foreground === true && !a.foregroundReason) {         return resolve({ success: false, errorCode: 'foreground_reason_required', legacyErrorCode: 'foreground_unjustified', windowEtiquette: 'background', error: 'foreground:true requires foregroundReason (and you should notify_user first).', _hint: 'Background is the default and is almost always right: the window opens, minimizes, and its taskbar button FLASHES orange - the user clicks it when THEY are ready. Only pass foreground:true + foregroundReason for a flow that genuinely breaks when unfocused.' });@@ -2505,10 +2505,10 @@ function dispatch(verb, args) {       const wantEmail = (want.includes(':') ? want.split(':').slice(1).join(':') : want).toLowerCase();       const wantBrowser = want.includes(':') ? want.split(':')[0] : ((args && args.browser) || null);       const findLive = () => { for (const [k, c] of conns) if (c.sock && !c.sock.destroyed && String(c.profile.email || '').toLowerCase() === wantEmail && (!wantBrowser || c.profile.browser === wantBrowser)) return k; return null; };-      const already = findLive();+      const already = findLive() || (() => { for (const [k, c] of conns) if (c.sock && !c.sock.destroyed && k.toLowerCase() === want.toLowerCase()) return k; return null; })();       if (already) return resolve({ success: true, output: JSON.stringify({ ok: true, alreadyLive: true, profile: already, _hint: 'Profile is already connected - drive it now (open_window still opens in the BACKGROUND by default).' }) });-      const kp = knownProfiles().find((pr) => (pr.email.toLowerCase() === wantEmail || pr.profileDir.toLowerCase() === wantEmail) && (!wantBrowser || pr.browser === wantBrowser));-      if (!kp) return resolve({ success: false, error: `no local browser profile matches '${want}'`, _hint: 'nbrowser_profiles lists every profile on this machine (live + asleep). Check the email/browser.' });+      const _rk2 = resolveKnownProfile(want, wantBrowser); const kp = _rk2.kp;+      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;       const exes = kp.browser === 'edge'@@ -3199,6 +3199,27 @@ function knownProfiles() {   }   return out; }+// RESOLVE a caller's profile string to a profile ON DISK (winvm fresh-box battery, 2026-09-20). A profile+// with no signed-in account connects under an ID key (`edge:p_wkl4xlp9`): that is the key nbrowser_profiles,+// readiness and `active` hand out, so every verb has to take it back. Disk profiles are keyed by email, or+// by directory when anonymous (`edge:Default`). Accepted, in order: email, directory, then an id key of a+// LIVE connection mapped to the browser's one anonymous profile (or its only profile). Ambiguity is refused.+function resolveKnownProfile(want, browserHint) {+  const w = String(want || '');+  const tail = (w.includes(':') ? w.split(':').slice(1).join(':') : w).toLowerCase();+  const br = w.includes(':') ? w.split(':')[0] : (browserHint || null);+  const all = knownProfiles().filter((pr) => !br || pr.browser === br);+  let kp = all.find((pr) => pr.email && pr.email.toLowerCase() === tail) || all.find((pr) => pr.profileDir.toLowerCase() === tail);+  if (kp) return { kp };+  if (/^p_[a-z0-9]+$/i.test(tail)) {+    const live = [...conns.keys()].some((k) => k.toLowerCase() === w.toLowerCase());+    const anon = all.filter((pr) => pr.anonymous);+    if (anon.length === 1) return { kp: anon[0], viaIdKey: true, live };+    if (all.length === 1) return { kp: all[0], viaIdKey: true, live };+    return { kp: null, ambiguous: true, candidates: all.map((pr) => pr.key) };+  }+  return { kp: null, candidates: all.map((pr) => pr.key) };+} // DIAGNOSE the Adom extension's state inside a profile that is NOT connected, by reading what the // browser itself recorded on disk (Preferences / Secure Preferences -> extensions.settings). This is // what turns "native browser extension not connected" from a MYSTERY into an actionable answer: not
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "nb-bridge",-  "version": "0.16.23",+  "version": "0.16.24",   "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+10−4
@@ -25,6 +25,10 @@ const TARGET = argv.target || 'AdomLapper'; let PROFILE = argv.profile || null; const THREAD = 'nb verb battery'; const SID = 'nbbattery';+// The target machine's own ~/.adom/native-browser, read from the bridge. It was hard-coded to John's user+// folder, which made set_file_input fail on every other machine (found on winvm, 2026-09-20).+let NB_DIR = 'C:/Users/Public';+function resolveNbDir() { try { const r = call('nbrowser_settings', {}); const f = String(r.file || ''); if (f) NB_DIR = f.replace(/\\/g, '/').replace(/\/settings\.json$/i, ''); } catch {} return NB_DIR; } const HARNESS = 'https://wiki.adom.inc/blob/app/nb-bridge/demo/test-harness.html'; const CB = Date.now(); @@ -67,7 +71,8 @@ const T = {   whoami: () => { const r = call('nbrowser_whoami'); return A(r.ok !== false, `keys:${Object.keys(r).slice(0, 5)}`, JSON.stringify(r).slice(0, 120)); },   profiles: () => { const r = call('nbrowser_profiles'); const ps = r.profiles || []; ctx.profileCount = ps.length; return A(ps.length > 0, `${ps.length} profiles`, JSON.stringify(r).slice(0, 140)); },   resolve_profiles: () => { const r = call('nbrowser_resolve_profiles'); return A(r.ok !== false, 'resolved', JSON.stringify(r).slice(0, 120)); },-  chrome_status: () => { const r = call('nbrowser_chrome_status'); return A(r.installed === true, `chrome installed, phase:${r.phase}`, JSON.stringify(r).slice(0, 140)); },+  // An Edge-only box is a PASS: nb drives whatever native browser is there (nb-bridge#46).+  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)); },   describe: () => { const r = call('nbrowser_describe'); const n = (r.verbs || []).length; const meta = metaCount(); return A(n === meta, `describe:${n} == VERB_META:${meta}`, `describe:${n} != VERB_META:${meta} — regenerate!`); },   scroll: () => { const r = call('nbrowser_scroll', { sessionId: SID, x: 200, y: 200, deltaY: 300 }); return A(r.ok === true && r.dispatched === 'wheel' && r.scrollAfter && typeof r.pageScrolled === 'boolean', `wheel dispatched; pageScrolled=${r.pageScrolled}`, JSON.stringify(r).slice(0, 160)); },   input_unknown_type: () => { const r = call('nbrowser_input_dispatch', { sessionId: SID, type: 'zoom', x: 200, y: 200, deltaY: -300 }); return A(r.ok === false && r.errorCode === 'unknown_input_type' && r.dispatched === false, 'unknown type refused, nothing dispatched', JSON.stringify(r).slice(0, 160)); },@@ -142,9 +147,10 @@ const T = {   set_date: () => { const r = call('nbrowser_set_date', { sessionId: SID, selector: '#seg-m', date: '2026-08-14' }); settle(); const v = evalIn('document.getElementById("date-note").textContent'); return A(String(v.result) === '2026-08-14', 'segmented date typed (MDY keydowns)', `r:${JSON.stringify(r).slice(0,100)} note:${v.result}`); },   input_dispatch: () => { const r = call('nbrowser_input_dispatch', { sessionId: SID, type: 'click', selector: '#btn-one' }); return A(r.ok !== false, 'low-level dispatch ok', JSON.stringify(r).slice(0, 140)); },   set_file_input: () => {-    const up = call('send_files', { reason: 'verb battery: staging the sample upload file', filePaths: [join(root, 'demo', 'sample-upload.txt')], dest: 'C:/Users/john/.adom/native-browser' });+    resolveNbDir();+    const up = call('send_files', { reason: 'verb battery: staging the sample upload file', filePaths: [join(root, 'demo', 'sample-upload.txt')], dest: NB_DIR });     if (up.success === false) return [false, 'send_files staging failed: ' + JSON.stringify(up).slice(0, 120)];-    call('nbrowser_set_file_input', { sessionId: SID, selector: '#file-input', filePaths: ['C:/Users/john/.adom/native-browser/sample-upload.txt'] });+    call('nbrowser_set_file_input', { sessionId: SID, selector: '#file-input', filePaths: [NB_DIR + '/sample-upload.txt'] });     const v = evalIn('document.getElementById("file-note").textContent');     return A(String(v.result).includes('sample-upload'), 'file attached to input', `note: ${v.result}`);   },@@ -173,7 +179,7 @@ const T = {    // downloads   download: () => { const r = call('nbrowser_download', { sessionId: SID, url: 'https://wiki.adom.inc/blob/app/nb-bridge/demo/sample-download.txt', filename: 'nb-battery-download.txt' }); return A(r.ok !== false, `downloadId:${r.downloadId}`, JSON.stringify(r).slice(0, 140)); },-  download_wait: () => { const r = call('nbrowser_download_wait', { sessionId: SID, sinceMs: 90000, timeoutMs: 25000 }); const files = r.files || []; for (const f of files) if (/battery|sample-download/.test(f)) call('delete_file', { reason: 'verb battery cleanup: removing the test download from Downloads', path: `C:/Users/john/Downloads/${f}` }); return A((r.count || files.length) >= 1, `landed: ${files.join(', ').slice(0, 80)} (cleaned)`, JSON.stringify(r).slice(0, 160)); },+  download_wait: () => { const r = call('nbrowser_download_wait', { sessionId: SID, sinceMs: 90000, timeoutMs: 25000 }); const files = r.files || []; for (const f of files) if (/battery|sample-download/.test(f)) call('delete_file', { reason: 'verb battery cleanup: removing the test download from Downloads', path: `${resolveNbDir().replace(/\/\.adom\/native-browser$/i, '')}/Downloads/${f}` }); return A((r.count || files.length) >= 1, `landed: ${files.join(', ').slice(0, 80)} (cleaned)`, JSON.stringify(r).slice(0, 160)); },    // spawn tracking + events + dialogs   events_and_owned: () => {