← Commit history

native-browser v0.0.33: readiness flags stale extension (expectedExtensionVersion + per-profile version compare)

John Lauer ·68b9d0b6e4 ·4mo ago ·parent 50728c2
5 files changed +49−14
bridge/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.0.32\ No newline at end of file+0.0.33\ No newline at end of file
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" }\ No newline at end of file