← Commit history
native-browser v0.0.33: readiness flags stale extension (expectedExtensionVersion + per-profile version compare)
5 files changed
+49−14
bridge/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.0.32+0.0.33
bridge/bridge.json+2−1@@ -2,7 +2,8 @@ "manifest_version": 1, "name": "native-browser", "displayName": "Adom Native Browser (Chrome + Edge)",- "version": "0.0.32",+ "version": "0.0.33",+ "expectedExtensionVersion": "0.0.18", "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.32",+ "version": "0.0.33", "private": true, "description": "adom-desktop bridge for the Adom native-browser extension (Chrome + Edge)", "main": "server.js",
bridge/server.js+40−6@@ -34,6 +34,20 @@ try { VERSION = require('./package.json').version || VERSION; } catch {} // per release, so the code's self-reported VERSION can never drift from the release version. try { VERSION = require('./bridge.json').version || VERSION; } catch {} +// The extension version this bridge release expects. Bumped in bridge.json alongside every extension+// change. readiness compares each connected profile's reported version against this so a stale extension+// (loaded-unpacked builds do NOT auto-update - only the bridge does) is FLAGGED loudly instead of+// surfacing later as a confusing "verb not implemented yet". null = don't check (older bridge.json).+let EXPECTED_EXT = null;+try { EXPECTED_EXT = require('./bridge.json').expectedExtensionVersion || null; } catch {}+// semver-lite numeric compare (a<b -> -1, a==b -> 0, a>b -> 1). Non-numeric/missing parts count as 0.+function cmpVer(a, b) {+ const pa = String(a || '0').split('.').map((n) => parseInt(n, 10) || 0);+ const pb = String(b || '0').split('.').map((n) => parseInt(n, 10) || 0);+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) { const d = (pa[i] || 0) - (pb[i] || 0); if (d) return d < 0 ? -1 : 1; }+ return 0;+}+ const HOSTFILE = path.join(os.homedir(), '.adom', 'bridges', BRIDGE, 'host.json'); // The Chrome-launched native-messaging host needs a Node to run its stdio pump, but Chrome does NOT // inherit AD's managed-runtime PATH (AD only puts its Node on the PATH of processes IT spawns). THIS@@ -186,7 +200,7 @@ function queryCredentials({ browser, profile, match } = {}) { // (Path-A bridge-side password decryption removed - v20 app-bound passwords can't be decrypted by any // external process; nbrowser_autologin defers legacy-v10 decryption to the in-Chrome host. See skill.) -function registerConn(sock, profile, browser) {+function registerConn(sock, profile, browser, version) { // Normalize to an object + tag the browser so chrome:me and edge:me are DISTINCT coexisting conns. const p = (profile && typeof profile === 'object') ? { ...profile } : { email: String(profile || ''), id: String(profile || '') }; p.browser = browser || p.browser || 'chrome';@@ -196,7 +210,9 @@ function registerConn(sock, profile, browser) { for (const [k, c] of [...conns]) if (c.sock === sock && k !== key) { conns.delete(k); if (activeProfile === k) activeProfile = null; } const prev = conns.get(key); if (prev && prev.sock !== sock) { try { prev.sock.destroy(); } catch {} } // newest host wins per profile- conns.set(key, { sock, profile: profile || {}, since: Date.now() });+ // Keep the version the extension reported in its hello (used by readiness to flag a stale build).+ // Fall back to a same-profile prior value if this hello somehow omitted it.+ conns.set(key, { sock, profile: profile || {}, since: Date.now(), version: version || (prev && prev.version) || null }); if (!activeProfile || !liveConns().some(([k]) => k === activeProfile)) activeProfile = isBlocked(key) ? activeProfile : key; dlog(`hello: profile ${key} registered (${conns.size} total)`); }@@ -730,16 +746,34 @@ function dispatch(verb, args) { const live = liveConns(); const ready = live.length > 0; const chExe = detectChrome();+ // Per-profile extension version + stale check. A loaded-unpacked extension does NOT auto-update+ // (only the bridge does), so a box can sit on an old build whose new verbs return "not implemented+ // yet". Flag it here (loudly, with the fix) instead of letting the agent trip over it mid-task.+ const extensionVersions = {};+ const staleProfiles = [];+ for (const [k, c] of live) {+ const v = c.version || 'unknown';+ extensionVersions[k] = v;+ if (EXPECTED_EXT && (!c.version || cmpVer(c.version, EXPECTED_EXT) < 0)) staleProfiles.push(k);+ }+ const extensionStale = staleProfiles.length > 0;+ const staleDetail = staleProfiles.map((k) => `${k} has ${extensionVersions[k]}`).join(', '); return resolve({ success: true, output: JSON.stringify({ ready, state: ready ? 'ready' : 'needs-extension', installing: false, extensionConnected: ready, profiles: live.map(([k]) => k),+ extensionVersions, // { "chrome:you@x": "0.0.18", ... } as reported in each hello+ expectedExtensionVersion: EXPECTED_EXT, // what THIS bridge release ships against (null = not checked)+ extensionStale, // true if any connected profile runs an older build than expected+ staleProfiles, // the profile keys that are behind (pass one to nbrowser_dev_reload) chromeInstalled: !!chExe, // is native Google Chrome present on the box? chromeInstallPhase: chExe ? 'ready' : chromeState.phase, // idle|installing|awaiting_uac|ready|failed- _hint: ready- ? `The Adom browser extension is connected (${live.length} profile(s)); drive it with nbrowser_open_window. Read-only probe - it never spawns work or installs.`+ _hint: extensionStale+ ? `STALE EXTENSION: ${staleDetail}; this bridge expects ${EXPECTED_EXT}. New verbs (wait_for/keepalive/download_wait/set_date/totp/robust-click) will return "verb not implemented yet" on the stale profile(s). SELF-HEAL: (1) refresh the unpacked extension dir to the current build (installing-the-extension -> Updating, or re-sync the extension/ folder), then (2) nbrowser_dev_reload {"profile":"<staleProfile>"} - it re-reads disk in 2 passes and reconnects in ~10s. If the on-disk build is ALREADY current, step (2) alone heals it.`+ : ready+ ? `The Adom browser extension is connected (${live.length} profile(s)) and current (v${EXPECTED_EXT || '?'}); drive it with nbrowser_open_window. Read-only probe - it never spawns work or installs.` : `The Adom browser extension is NOT connected. ${chExe ? '' : 'This box has NO native Google Chrome (Edge may be present). If the user wants the AI to drive their REAL Chrome, offer nbrowser_install_chrome (installs Google Chrome, mostly no prompt). '}It may be not-yet-installed (a Level-2 proactively-offered install: OFFER to install it, then run installing-the-extension - never make them do it) OR installed-but-asleep (the SW sleeps when a profile has no open window). Full onboarding: the setting-up-native-chrome skill. Read-only: never installs or spawns.`, }) }); }@@ -776,7 +810,7 @@ function dispatch(verb, args) { }; const verbs = [ d('nbrowser_status', 'Is the extension connected? Reports ext id/version + active browser.', {}, { ok: 'bool', ext: 'str', version: 'str', engine: 'str', browser: '"chrome"|"edge"' }, 15),- d('nbrowser_readiness', 'READ-ONLY cold-start probe: is the native-browser extension connected + ready to drive? Never spawns work or installs. Use for "is it ready / do I have the browser extension" instead of nbrowser_status.', {}, { ready: 'bool', state: '"ready"|"needs-extension"', installing: 'bool', extensionConnected: 'bool', profiles: '[str]', chromeInstalled: 'bool', chromeInstallPhase: 'str' }, 15, { example: {} }),+ d('nbrowser_readiness', 'READ-ONLY cold-start probe: is the native-browser extension connected + ready to drive? Never spawns work or installs. Use for "is it ready / do I have the browser extension" instead of nbrowser_status. Also flags a STALE extension (loaded build older than this bridge expects) so you fix it up-front instead of hitting "verb not implemented yet" mid-task.', {}, { ready: 'bool', state: '"ready"|"needs-extension"', installing: 'bool', extensionConnected: 'bool', profiles: '[str]', extensionVersions: '{profile:version}', expectedExtensionVersion: 'str|null', extensionStale: 'bool', staleProfiles: '[str]', chromeInstalled: 'bool', chromeInstallPhase: 'str' }, 15, { example: {} }), 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),@@ -1170,7 +1204,7 @@ const hostSrv = net.createServer((sock) => { if (!line.trim()) continue; let frame; try { frame = JSON.parse(line); } catch { continue; }- if (frame && frame.type === 'hello') { registerConn(sock, frame.profile, frame.browser); continue; }+ if (frame && frame.type === 'hello') { registerConn(sock, frame.profile, frame.browser, frame.version); continue; } onHostFrame(frame); } });
native-browser-bridge-manifest.json+5−5@@ -1,8 +1,8 @@ { "manifest_version": 1,- "version": "0.0.32",- "url": "native-browser-bridge-v0.0.32.bin",- "sha256": "65405dd017434bf905d4fcc20b8729ba2889aa169eecc1b0b8f15d833cf6c958",- "size": 39815,- "released_at": "2026-07-05T18:08:37Z"+ "version": "0.0.33",+ "url": "native-browser-bridge-v0.0.33.bin",+ "sha256": "80f9c9301a5efcda8cbae6a386776ac5555546d7cfff131f93f9f72bbdaec543",+ "size": 41033,+ "released_at": "2026-07-06T00:00:00Z" }