← Commit history
v0.16.52 (insiders): nb-bridge#55: the export is detected by the FILE the browser writes, not by seeing the save dialog (a modal another agent on the same desktop can dismiss between two scans, which is exactly what happened); nb fills the dialog when it can see it, and otherwise tells the user to just press Save and picks the file up from Documents, Downloads or Desktop, then deletes it
6 files changed
+45−13
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.51'; // keep in lockstep with package.json (pkg version)+const VERSION = '0.16.52'; // 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.51+0.16.52
bridge/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "native-browser", "displayName": "Adom Native Browser",- "version": "0.16.51",+ "version": "0.16.52", "expectedExtensionVersion": "0.2.22", "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.51",+ "version": "0.16.52", "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+40−8@@ -3150,6 +3150,26 @@ const EXPORT_CAPTION = 'nb-export-logins'; const exportSay = (text, opts2) => abCommand(null, 'desktop_caption', { text, id: EXPORT_CAPTION, position: 'top', size: 'medium', persist: true, ...(opts2 || {}) }).catch(() => {}); const exportSayClear = () => abCommand(null, 'desktop_caption', { id: EXPORT_CAPTION, action: 'hide' }).catch(() => {});+// WATCH FOR THE FILE, NOT THE WINDOW (John, 2026-10-29). Window enumeration is not reliable here: the+// save dialog is a modal another agent on the same desktop can dismiss between two scans, and we saw it+// present in the user's screenshot and absent in ours seconds later. The file Chrome writes cannot be+// raced like that. The bridge runs ON the machine, so it just looks for it. Either nb fills the dialog+// with its own path, or the user presses Save with Chrome's default - both land here.+function newLoginCsvSince(sinceMs, ownPath) {+ const dirs = [path.dirname(ownPath), path.join(os.homedir(), 'Downloads'), path.join(os.homedir(), 'Documents'), path.join(os.homedir(), 'Desktop')];+ const hits = [];+ for (const d of dirs) {+ let names = []; try { names = fs.readdirSync(d); } catch { continue; }+ for (const n of names) {+ if (!/\.csv$/i.test(n)) continue;+ if (!/password/i.test(n) && path.join(d, n) !== ownPath) continue;+ const fp = path.join(d, n);+ try { const st = fs.statSync(fp); if (st.size > 0 && st.mtimeMs >= sinceMs - 1500) hits.push({ path: fp, mtime: st.mtimeMs, size: st.size }); } catch {}+ }+ }+ hits.sort((a, b) => b.mtime - a.mtime);+ return hits[0] || null;+} async function exportSavedLogins(opts) { const { profileKey, browser, sites, reason, timeoutSec } = opts; const scheme = browser === 'edge' ? 'edge' : 'chrome';@@ -3251,11 +3271,21 @@ async function exportSavedLogins(opts) { // should ever see here is the Hello prompt. So poll FAST and fill it the moment it exists, and keep // waiting the whole budget - the dialog only appears AFTER they answer Hello, which can take a while. await exportSay(`${brand} is asking you to confirm it is you (Windows Hello). Approve it and Adom finishes the rest - do not close anything.`);+ const watchSince = Date.now(); const deadline = Date.now() + timeoutSec * 1000;- let saved = false;+ let saved = false, foundFile = null, toldToSave = false; let sawDialogAt = 0, dialogVisibleMs = null; while (Date.now() < deadline) { await _sleep(300);+ // THE AUTHORITATIVE SIGNAL: did a passwords CSV appear? Whoever pressed Save, we are done.+ foundFile = newLoginCsvSince(watchSince, csvPath);+ if (foundFile) { saved = true; if (sawDialogAt) dialogVisibleMs = Date.now() - sawDialogAt; break; }+ // After a few seconds with the dialog still up, stop pretending and give the user the one useful+ // instruction: press Save. nb takes it from there wherever Chrome puts it.+ if (sawDialogAt && !toldToSave && Date.now() - sawDialogAt > 4000) {+ toldToSave = true;+ await exportSay(`${brand} is asking where to save. Just press Save - Adom picks the file up from wherever it lands and deletes it straight after.`);+ } const wins = await abCommand(null, 'desktop_list_windows', {}).catch(() => null); const list = (() => { try { return (wins.body.data || wins.body).windows || []; } catch { return []; } })(); const dlg = list.find((w) => (w.className === '#32770' || /^#32770/.test(String(w.className || '')))@@ -3304,25 +3334,26 @@ async function exportSavedLogins(opts) { await exportSay(`Adom stopped waiting for the ${brand} confirmation. Nothing was exported.`, { persist: false, duration: 8000 }); } return done({ ok: false, errorCode: 'awaiting_user', steps, dialogLeftOnScreen: !!stuck,- error: `the export was started but no save dialog appeared within ${timeoutSec}s - the Windows Hello prompt was probably not answered`,- _hint: 'RELAY TO THE USER: a Windows Hello prompt is (or was) on their PC asking them to confirm the password export; it cannot be answered for them. Once they approve, retry this verb.' });+ error: `no exported file appeared within ${timeoutSec}s - the Windows Hello prompt or the save dialog was probably never answered`,+ _hint: 'RELAY TO THE USER: the browser asked them to confirm it is them (Windows Hello) and then where to save; nb cannot answer either for them. nb watches for the file wherever it lands, so "just press Save" is enough. Nothing was exported.' }); } - // Wait for Chrome to finish writing.+ // Wait for the bytes to settle (Chrome writes, then releases).+ const srcPath = (foundFile && foundFile.path) || csvPath; let bytes = 0; const wdl = Date.now() + 45000;- while (Date.now() < wdl) { await _sleep(800); try { const st = fs.statSync(csvPath); if (st.size > 0) { bytes = st.size; break; } } catch {} }+ while (Date.now() < wdl) { await _sleep(400); try { const st = fs.statSync(srcPath); if (st.size > 0) { bytes = st.size; break; } } catch {} } if (!bytes) { await exportSay(`Nothing was exported - ${brand} did not write the file. You can close this.`, { persist: false, duration: 8000 }); return done({ ok: false, errorCode: 'no_export_file', steps, error: 'the save dialog was answered but no file appeared', _hint: 'The user may have cancelled, or Hello failed. Nothing was exported; retry when they are at the machine.' }); }- steps.push(step('browser wrote the export', true, `${bytes} bytes`));+ steps.push(step('browser wrote the export', true, `${bytes} bytes at ${srcPath === csvPath ? 'the path nb asked for' : 'Chrome\'s own default path'}`)); // Parse for COUNTS ONLY, drop the note column, and write the handoff the local tool will read. let rows = []; try {- const text = fs.readFileSync(csvPath, 'utf8');+ const text = fs.readFileSync(srcPath, 'utf8'); rows = parseLoginCsv(text); } catch (e) { cleanup();@@ -3333,7 +3364,8 @@ async function exportSavedLogins(opts) { const hosts = [...new Set(keep.map((r) => { try { return new URL(r.url).hostname; } catch { return String(r.url || '').replace(/^https?:\/\//, '').split('/')[0]; } }))].filter(Boolean); const h = writeSecretHandoff('logins', { verb: 'nbrowser_export_saved_logins', browser, profile: profileKey, reason, count: keep.length }, { logins: keep.map((r) => ({ url: r.url, username: r.username, password: r.password })) }); // note column deliberately absent- cleanup(); // the browser's own plaintext copy never survives this verb+ cleanup();+ try { if (srcPath !== csvPath) fs.unlinkSync(srcPath); } catch {} // the browser's own plaintext copy never survives this verb, wherever it landed steps.push(step('handed it to the local tool and deleted the browser\'s copy', true)); await exportSay(`Done: ${keep.length} logins handed to pup. ${brand}'s copy of the file was deleted.`, { persist: false, duration: 6000 }); logActivity({ verb: 'nbrowser_export_saved_logins', profile: profileKey, ok: true, ops: `${keep.length} login(s) for ${hosts.length} site(s)` });
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "nb-bridge",- "version": "0.16.51",+ "version": "0.16.52", "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.",