← Commit history

v0.16.31 (insiders): nbrowser_state {screenshot:true} and nbrowser_demo deliver their screenshot as a file too (fullPath promoted to the top of the reply, pulled to the caller by ab), so no nb verb sends a page image as base64 through the relay

John Lauer ·a16087338a ·1mo ago ·parent 8001ea0
7 files changed +30−25
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.30';           // keep in lockstep with package.json (pkg version)+const VERSION = '0.16.31';           // 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.30\ No newline at end of file+0.16.31\ 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.30",+  "version": "0.16.31",   "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.30",+  "version": "0.16.31",   "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+21−16
@@ -1780,8 +1780,8 @@ const VERB_META = {   nbrowser_taskbar: { hint: 'Paint progress/overlay/flash on the window taskbar button (e.g. paused=yellow for a human gate). The "Adom is driving" badge is AUTOMATIC: the bridge composites the profile\'s circle avatar + the Adom favicon at open/drive, and idle-expires it (~3 min) back to the plain native avatar - you rarely need to touch overlay yourself.', related: ['nbrowser_flash'], pitfalls: ['clear progress+flash at task end: {progress:{state:"none"},flash:"stop"} - the badge clears itself (idle-expiry / close)', 'overlay.badge:"none" RESTORES the native avatar look (the bridge never blanks the overlay slot - blanking would erase the user\'s profile avatar)'] },   nbrowser_describe: { hint: 'Returns per-verb schemas + hint/related/pitfalls for ab\'s Verbs tab.', related: ['nbrowser_help'], pitfalls: ['this is the catalog - a few extension-only verbs (e.g. nbrowser_login_form) are real but resolved in the extension, not listed here'] },   nbrowser_handle_dialog: { hint: 'Control native JS dialogs (alert/confirm/prompt). GROUND TRUTH: while nbe is driving, CHROME cancels dialogs the instant they open (confirm/prompt return the dismiss value, alerts close) - the page can never wedge, but a dialog can also never be caught open to answer later. To ANSWER one deliberately, arm a PRE-ANSWER first: {policy:"manual", accept:true|false, text?} makes the page answer the NEXT dialog(s) with your preset the moment they fire; do the action; then {policy:"auto"} disarms and returns `seen` (every dialog answered).', related: ['nbrowser_events', 'nbrowser_screenshot'], pitfalls: ['a bare {accept:true} AFTER the dialog fired is too late - Chrome already cancelled it; arm the pre-answer BEFORE the action, not after', 'the pre-answer is per TAB and does NOT survive a page navigation - re-arm after navigating', 'disarm with {policy:"auto"} when done - it also restores the page\'s own confirm/alert/prompt functions'] },-  nbrowser_state: { hint: 'ONE call for "what windows am I driving, and what do they look like": every session with its EXACT hwnd + tabId + live url/title, and (screenshot:true) the page bitmap inline as base64. Use this instead of matching a browser window by TITLE - a title is the ACTIVE TAB, it changes while loading, and several windows can share a substring (that is exactly what broke the ab web-control demo, issue #22).', related: ['nbrowser_open_window', 'nbrowser_screenshot', 'nbrowser_os_windows'], pitfalls: ['hwnd is null until the window is correlated (a page grouped into an existing window has no new hwnd) - check before passing it to desktop_* verbs', 'this reports only THIS bridge\'s sessions; for every browser window on the machine use nbrowser_os_windows', 'screenshot:true costs a real CDP capture - omit it when you only need the logical state'] },-  nbrowser_demo: { hint: 'ONE-CALL SHOWCASE of layer 2: picks a live profile, opens a representative page in the user\'s REAL signed-in browser (background + badged), and returns that exact window\'s hwnd/tabId plus an inline base64 screenshot with per-step progress. Built for embedding "drive my real browser" in a web demo without any window hunting.', related: ['nbrowser_state', 'nbrowser_open_window', 'nbrowser_close_window'], pitfalls: ['it OPENS a real window on the user\'s desktop - clean up with nbrowser_close_window {sessionId} (default "nbdemo") when the demo ends', 'idempotent: re-running closes its previous demo window first', 'needs nbe connected in some profile - if readiness says needs-extension it returns no drivable profile'] },+  nbrowser_state: { hint: 'ONE call for "what windows am I driving, and what do they look like": every session with its EXACT hwnd + tabId + live url/title, and (screenshot:true) the page bitmap as a file (fullPath at the top of the reply; ab pulls it to you). Use this instead of matching a browser window by TITLE - a title is the ACTIVE TAB, it changes while loading, and several windows can share a substring (that is exactly what broke the ab web-control demo, issue #22).', related: ['nbrowser_open_window', 'nbrowser_screenshot', 'nbrowser_os_windows'], pitfalls: ['hwnd is null until the window is correlated (a page grouped into an existing window has no new hwnd) - check before passing it to desktop_* verbs', 'this reports only THIS bridge\'s sessions; for every browser window on the machine use nbrowser_os_windows', 'screenshot:true costs a real CDP capture - omit it when you only need the logical state'] },+  nbrowser_demo: { hint: 'ONE-CALL SHOWCASE of layer 2: picks a live profile, opens a representative page in the user\'s REAL signed-in browser (background + badged), and returns that exact window\'s hwnd/tabId plus a screenshot delivered as a file (fullPath; ab pulls it to you) with per-step progress. Built for embedding "drive my real browser" in a web demo without any window hunting.', related: ['nbrowser_state', 'nbrowser_open_window', 'nbrowser_close_window'], pitfalls: ['it OPENS a real window on the user\'s desktop - clean up with nbrowser_close_window {sessionId} (default "nbdemo") when the demo ends', 'idempotent: re-running closes its previous demo window first', 'needs nbe connected in some profile - if readiness says needs-extension it returns no drivable profile'] },   nbrowser_uninstall: { hint: 'CASCADE HOOK, not for normal use: ab calls this itself during bridge_uninstall (ab 2.0.35+) with {cascade:true} BEFORE deleting the bridge cache, so nb can remove artifacts that live outside it - above all the HKCU native-messaging host registration, which would otherwise leave every profile dialing a deleted binary forever ("Native host has exited"). Also clears ~/.adom/bridges/native-browser, the activity trail, and the icon cache.', related: ['nbrowser_readiness'], pitfalls: ['you almost never call this directly - run ab\'s bridge_uninstall and let it cascade', 'it deliberately does NOT remove the nbe extension from the user\'s browser (their profile, their choice) - it only stops working; use uninstalling-the-extension to remove it properly', 'best-effort by contract: a failure here still lets ab delete the cache, so check removed[]/failed[] rather than assuming'] },   nbrowser_dev_reload: { hint: 'MAINTAINER-only: reload the extension to pick up on-disk code changes (2-pass, reconnects in ~10s).', related: ['nbrowser_status'], pitfalls: ['throttled to 1/90s (a reload restarts the SW + native host)', 'not for normal driving - only after editing the extension source', 'reloads ONE profile; other profiles pick up the new code on their own next reload'] },   nbrowser_clear_cookies: { hint: 'Background logout: drop a site\'s cookies (incl. httpOnly) for ONE registrable domain, leaving other sites (e.g. Google SSO) intact.', related: ['nbrowser_login_state'], pitfalls: ['scoped to one domain so it never nukes unrelated sessions', '⚠️ do NOT use this to log OUT of a site that ties device-trust to cookies (e.g. Gusto) - it forces a fresh 2FA; use the site\'s own Sign out'] },@@ -2110,8 +2110,8 @@ function dispatch(verb, args) {         d('nbrowser_clear_cookies', 'Background logout: drop one registrable domain\'s cookies (incl. httpOnly), leaving other sites (e.g. Google SSO) intact.', { host: 'required str (e.g. "gusto.com"; url/domain also accepted)' }, { ok: 'bool', removed: 'int', hosts: '[str]' }, 30, { example: { host: 'gusto.com' } }),         d('nbrowser_caption', 'Paint an on-page caption (subtitle, or red alert banner) for demos and narration.', { sessionId: S, text: 'required str (or action:"hide")', style: 'optional "subtitle"(default)|"alert"', position: 'optional str', fontSize: 'optional int', pulse: 'optional bool', durationMs: 'optional int (default 5000)', action: 'optional "hide"' }, { ok: 'bool', tabId: 'str', style: 'str' }, 20, { example: { sessionId: 's1', text: 'Signing in to DigiKey' } }),         d('nbrowser_cursor', 'Show or move the gliding Adom cursor with an intent label (demo narration). Visual only, it does not click.', { sessionId: S, action: 'optional "move"(default)|"hide"', x: 'optional int', y: 'optional int', label: 'optional str (or intent)' }, { ok: 'bool', tabId: 'str', action: 'str', label: 'str' }, 20, { example: { sessionId: 's1', x: 400, y: 300, label: 'Clicking Sign in' } }),-        d('nbrowser_state', 'The EXACT windows this bridge drives: per session hwnd + tabId + live url/title (+ inline base64 page bitmap with screenshot:true). Use instead of title-matching a browser window.', { sessionId: 'optional str (default: all my sessions)', screenshot: 'optional bool - include the page bitmap inline' }, { ok: 'bool', count: 'int', sessions: '[{sessionId,hwnd,windowId,tabId,url,title,profile,thread,purpose,osOpened}]', screenshot: '{sessionId,base64,width,height}?' }, 30, { example: { screenshot: true } }),-        d('nbrowser_demo', 'One-call showcase: open a representative page in the user\'s REAL signed-in browser (background, badged) and return that exact window + an inline base64 screenshot + step progress.', { url: 'optional str (default the nb-bridge wiki page)', profile: 'optional str (default: first live profile)', sessionId: 'optional str (default "nbdemo")', thread: 'optional str' }, { ok: 'bool', sessionId: 'str', profile: 'str', hwnd: 'int?', tabId: 'str?', base64: 'str', width: 'int', height: 'int', steps: '[{step,ok}]' }, 60, { example: {} }),+        d('nbrowser_state', 'The EXACT windows this bridge drives: per session hwnd + tabId + live url/title (+ the page bitmap as a pulled file with screenshot:true). Use instead of title-matching a browser window.', { sessionId: 'optional str (default: all my sessions)', screenshot: 'optional bool - include the page bitmap inline' }, { ok: 'bool', count: 'int', sessions: '[{sessionId,hwnd,windowId,tabId,url,title,profile,thread,purpose,osOpened}]', screenshot: '{sessionId,base64,width,height}?' }, 30, { example: { screenshot: true } }),+        d('nbrowser_demo', 'One-call showcase: open a representative page in the user\'s REAL signed-in browser (background, badged) and return that exact window + a screenshot file (fullPath, pulled to you) + step progress.', { url: 'optional str (default the nb-bridge wiki page)', profile: 'optional str (default: first live profile)', sessionId: 'optional str (default "nbdemo")', thread: 'optional str' }, { ok: 'bool', sessionId: 'str', profile: 'str', hwnd: 'int?', tabId: 'str?', base64: 'str', width: 'int', height: 'int', steps: '[{step,ok}]' }, 60, { example: {} }),         d('nbrowser_uninstall', 'CASCADE HOOK (ab 2.0.35+): ab calls this during bridge_uninstall, before deleting the cache, so nb removes its own out-of-cache artifacts (HKCU native-messaging host for Chrome + Edge, ~/.adom/bridges/native-browser, activity trail, icon cache). Never removes the extension from the user\'s browser.', { cascade: 'bool (set by ab)', name: 'str (set by ab)' }, { ok: 'bool', removed: '[str]', failed: '[str]', extensionStillInstalled: '[str]' }, 30, { example: { cascade: true, name: 'native-browser' } }),         d('nbrowser_dev_reload', 'MAINTAINER-only: reload the extension to pick up on-disk code changes (2-pass, reconnects in ~10s). Throttled to 1 per 90s.', { profile: P }, { ok: 'bool', reloaded: 'bool', throttled: 'bool?' }, 30, { example: { profile: 'chrome:[email protected]' } }),         d('nbrowser_describe', 'This verb: returns per-verb schemas for the ab Verbs tab.', {}, { success: 'bool', verbs: '[{name,summary,input,output,timeoutSeconds,statusVerb,longRunning,example}]' }, 15, { example: {} }),@@ -2495,14 +2495,17 @@ function dispatch(verb, args) {             callSelf('nbrowser_screenshot', { sessionId: shotSid }),             new Promise((r) => setTimeout(() => r({ __timeout: true }), Math.min(Number(sa.screenshotTimeoutMs) || 8000, 20000))),           ]);-          if (s && s.base64) shot = { sessionId: shotSid, base64: s.base64, width: s.width, height: s.height };+          // nb-bridge#49: the bitmap is a FILE (fullPath/safePath), promoted to the reply's top level so ab+          // pulls it to the caller. No base64 through the relay.+          if (s && s.fullPath) shot = { sessionId: shotSid, fullPath: s.fullPath, safePath: s.safePath, fullWidth: s.fullWidth, fullHeight: s.fullHeight, safeWidth: s.safeWidth, safeHeight: s.safeHeight, resizeMax: s.resizeMax, width: s.width, height: s.height };           else shotError = s && s.__timeout ? `screenshot of '${shotSid}' timed out - state returned without it` : (s && s.error) || 'screenshot unavailable';         }-        resolve({ success: true, output: JSON.stringify({ ok: true, count: sessions.length, sessions, screenshot: shot,+        const statePayload = { ok: true, count: sessions.length, sessions, screenshot: shot,           hiddenCount: hidden, screenshotError: shotError,           _hint: sessions.length             ? `The EXACT windows THIS bridge opened for YOU${hidden ? ` (${hidden} other/dead session(s) hidden - pass all:true to see every tracked session with owner + alive flags)` : ''} - hwnd is authoritative, so never match a browser window by TITLE (a title is the active tab, it changes while loading, and several windows share substrings - that is what broke the web-control demo, issue #22). The user's OWN windows are deliberately excluded (nbrowser_os_windows shows every browser window on the machine, with ownership attribution). screenshot:true returns the page bitmap inline in this same call.`-            : `This bridge is driving NO live windows for you right now${hidden ? ` (${hidden} tracked session(s) belong to other threads or are dead - all:true to inspect)` : ''}. Open one with nbrowser_open_window {sessionId, thread, purpose, profile, url} - it returns the hwnd + tabId of exactly that window.` }) });+            : `This bridge is driving NO live windows for you right now${hidden ? ` (${hidden} tracked session(s) belong to other threads or are dead - all:true to inspect)` : ''}. Open one with nbrowser_open_window {sessionId, thread, purpose, profile, url} - it returns the hwnd + tabId of exactly that window.` };+        resolve(deliveryEnv(statePayload, shot));       });       return;     }@@ -2540,12 +2543,12 @@ function dispatch(verb, args) {           callSelf('nbrowser_screenshot', { sessionId: sid }),           new Promise((r) => setTimeout(() => r({ __timeout: true }), Math.min(Number(da.screenshotTimeoutMs) || 12000, 25000))),         ]);-        steps.push({ step: 'screenshot the exact window', ok: !!shot.base64, bytes: shot.sizeKB, timedOut: !!shot.__timeout });-        resolve({ success: true, output: JSON.stringify({ ok: true, sessionId: sid, profile, url, hwnd: open.hwnd || null, tabId: open.tabId || null,-          base64: shot.base64 || null, width: shot.width, height: shot.height, steps,-          screenshotError: shot.base64 ? null : (shot.__timeout ? 'capture timed out - the window IS open (see hwnd); retry nbrowser_screenshot {sessionId} once it finishes loading' : (shot.error || null)),-          _hint: `Opened ${url} in the user's REAL signed-in browser (profile ${profile}) in the BACKGROUND, badged as AI-driven, and captured that EXACT window - no title matching anywhere. Render base64 inline. Clean up with nbrowser_close_window {sessionId:"${sid}"} when the demo is done.`,-          _next: 'nbrowser_close_window' }) });+        steps.push({ step: 'screenshot the exact window', ok: !!shot.fullPath, bytes: shot.fullBytes, timedOut: !!shot.__timeout });+        resolve(deliveryEnv({ ok: true, sessionId: sid, profile, url, hwnd: open.hwnd || null, tabId: open.tabId || null,+          fullPath: shot.fullPath || null, safePath: shot.safePath, fullWidth: shot.fullWidth, fullHeight: shot.fullHeight, safeWidth: shot.safeWidth, safeHeight: shot.safeHeight, resizeMax: shot.resizeMax, width: shot.width, height: shot.height, steps,+          screenshotError: shot.fullPath ? null : (shot.__timeout ? 'capture timed out - the window IS open (see hwnd); retry nbrowser_screenshot {sessionId} once it finishes loading' : (shot.error || null)),+          _hint: `Opened ${url} in the user's REAL signed-in browser (profile ${profile}) in the BACKGROUND, badged as AI-driven, and captured that EXACT window - no title matching anywhere. The capture is a file: ab pulls it to you as localSafePath/localFullPath (render that). Clean up with nbrowser_close_window {sessionId:"${sid}"} when the demo is done.`,+          _next: 'nbrowser_close_window' }));       });       return;     }@@ -3142,11 +3145,13 @@ async function onHostFrameInner(frame) { // ab's file-delivery contract reads the TOP level of a bridge reply (nb-bridge#49): fullPath/safePath nested // inside `output` are never pulled to the caller. Hoist them (with their sizes) beside `output`. const DELIVERY_KEYS = ['fullPath', 'safePath', 'fullWidth', 'fullHeight', 'safeWidth', 'safeHeight', 'resizeMax'];-function resolve_ok(resolve, payload) {+function deliveryEnv(payload, from) {   const env = { success: true, output: JSON.stringify(payload) };-  if (payload && payload.fullPath) for (const k of DELIVERY_KEYS) if (payload[k] != null) env[k] = payload[k];-  resolve(env);+  const src = from || payload;+  if (src && src.fullPath) for (const k of DELIVERY_KEYS) if (src[k] != null) env[k] = src[k];+  return env; }+function resolve_ok(resolve, payload) { resolve(deliveryEnv(payload)); }  // Loopback bind host. ab (v1.9.63+) passes ADOM_BIND_HOST (always 127.0.0.1) to every bridge it // spawns; bind to it so we never default to 0.0.0.0 and pop a Windows Firewall "allow access?"
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "nb-bridge",-  "version": "0.16.30",+  "version": "0.16.31",   "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+4−4
@@ -235,14 +235,14 @@ const T = {     const r = call('nbrowser_state', { sessionId: SID, screenshot: true });     const s = (r.sessions || [])[0];     if (!s) return [false, 'no session reported by state: ' + JSON.stringify(r).slice(0, 160)];-    const hasShot = !!(r.screenshot && r.screenshot.base64);-    return A(s.hwnd && String(s.url || '').includes('test-harness') && hasShot, `hwnd:${s.hwnd} url matched, inline shot ${hasShot ? 'present' : 'MISSING'}`, `session:${JSON.stringify(s).slice(0, 160)} shot:${hasShot}`);+    const hasShot = !!(r.screenshot && r.screenshot.fullPath) && !!r.fullPath;   // nb-bridge#49: a file, promoted for ab's pull+    return A(s.hwnd && String(s.url || '').includes('test-harness') && hasShot, `hwnd:${s.hwnd} url matched, shot file ${hasShot ? 'present' : 'MISSING'}`, `session:${JSON.stringify(s).slice(0, 160)} shot:${hasShot}`);   },   demo: () => {     const r = call('nbrowser_demo', { sessionId: 'nbbattery-demo', url: HARNESS, profile: PROFILE, thread: THREAD }, 120000);-    const ok = r.ok === true && !!r.base64 && !!r.hwnd;+    const ok = r.ok === true && !!r.fullPath && !r.base64 && !!r.hwnd;     call('nbrowser_close_window', { sessionId: 'nbbattery-demo' });-    return A(ok, `demo opened hwnd:${r.hwnd} + returned ${r.width}x${r.height} inline shot`, JSON.stringify(r).slice(0, 200));+    return A(ok, `demo opened hwnd:${r.hwnd} + returned a ${r.width}x${r.height} shot file`, JSON.stringify(r).slice(0, 200));   },   // guards-as-tests   guard_autologin: () => { const r = call('nbrowser_autologin', { sessionId: SID, host: 'wiki.adom.inc' }); return A(r.ok !== true && r.loggedIn !== true, 'refused without confirm:true', 'AUTOLOGIN RAN WITHOUT CONFIRM: ' + JSON.stringify(r).slice(0, 120)); },