← Commit history

v0.16.40 (insiders): macOS is no longer hard-blocked (nb-bridge#51): platforms.macos is supported+experimental, and install/install-mac.sh plus host/native-host.sh ship the registration Drew proved by hand; a CDP refusal now arrives as a structured errorCode and hint instead of Chrome's raw text; the host walks its parent chain with ps off Windows; readiness tells a Mac how to install; extension 0.2.18

John Lauer ·369f5e0a42 ·21d ago ·parent afc04e6
11 files changed +65−17
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.39';           // keep in lockstep with package.json (pkg version)+const VERSION = '0.16.40';           // 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.39\ No newline at end of file+0.16.40\ No newline at end of file
bridge/bridge.json+6−4
@@ -2,8 +2,8 @@   "manifest_version": 1,   "name": "native-browser",   "displayName": "Adom Native Browser",-  "version": "0.16.39",-  "expectedExtensionVersion": "0.2.17",+  "version": "0.16.40",+  "expectedExtensionVersion": "0.2.18",   "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",   "author": "Adom Inc.",@@ -129,8 +129,10 @@       "supported": true     },     "macos": {-      "supported": false,-      "reason": "Windows-first; Kyle ports to macOS as a fast-follow (NativeMessagingHosts live under ~/Library/Application Support/<browser>/NativeMessagingHosts; bridge/host are OS-agnostic Node)."+      "supported": true,+      "experimental": true,+      "reason": "The bridge, host and extension run on macOS (proven by Drew Owens, adom/nb-bridge#51). EXPERIMENTAL: the host registration is a manual step (install/install-mac.sh), background-window enforcement is Windows-only (#52), and nbrowser_credentials refuses here.",+      "install": "install/install-mac.sh"     },     "linux": {       "supported": false,
bridge/package.json+1−1
@@ -1,6 +1,6 @@ {   "name": "adom-native-browser-bridge",-  "version": "0.16.39",+  "version": "0.16.40",   "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+2−2
@@ -1975,7 +1975,7 @@ function dispatch(verb, args) {           ? `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.` + skewHint           : ready           ? `nbe (the Adom Native 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.`-          : `nbe (the Adom Native Browser Extension) is NOT connected. ${pend.length ? `(${pend.length} host socket(s) - ${pend.map(([k]) => k).join(', ')} - are open but FAILED a live ping probe: a native host is connect-looping with no working extension behind it. Do NOT wait on these; treat this box as needs-extension.) ` : ''}${NBE_PITCH}${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). '}${claudeExt.installed ? `NOTE: the user HAS the Claude for Chrome extension (${claudeExt.profiles.join(', ')}) - you cannot drive it (closed control plane), but the honest interim is real: they can ask Claude in their browser to work a page interactively, while you offer nbe for full automation from any AI thread. ` : ''}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.` + skewHint + hostHint) + offerHint,+          : `${process.platform === 'darwin' ? 'macOS (EXPERIMENTAL, nb-bridge#51): the bridge, host and extension run here, but the host registration is a MANUAL step - run install/install-mac.sh from this bridge\'s own folder (it registers the native-messaging host for Chrome and Edge and prints the Load-unpacked path). The Windows installing-the-extension flow (UIA clicks, PowerShell) does NOT apply. ' : ''}nbe (the Adom Native Browser Extension) is NOT connected. ${pend.length ? `(${pend.length} host socket(s) - ${pend.map(([k]) => k).join(', ')} - are open but FAILED a live ping probe: a native host is connect-looping with no working extension behind it. Do NOT wait on these; treat this box as needs-extension.) ` : ''}${NBE_PITCH}${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). '}${claudeExt.installed ? `NOTE: the user HAS the Claude for Chrome extension (${claudeExt.profiles.join(', ')}) - you cannot drive it (closed control plane), but the honest interim is real: they can ask Claude in their browser to work a page interactively, while you offer nbe for full automation from any AI thread. ` : ''}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.` + skewHint + hostHint) + offerHint,       }) });       });   // end probe.then     }@@ -3234,7 +3234,7 @@ async function onHostFrameInner(frame) {     }     // frame.ok === false: the extension refused or threw; pass its error and teaching hint through verbatim.     logActivity({ verb: p.verb, sessionId: null, profile: p.profileKey || null, ok: false, errorCode: 'error', error: String(frame.error || '').slice(0, 120) });-    p.resolve({ success: false, error: frame.error || 'unknown error', _hint: frame._hint });+    p.resolve({ success: false, error: frame.error || 'unknown error', errorCode: frame.errorCode || undefined, _hint: frame._hint });     return;   }   // Unsolicited events are surfaced to the AI via nbrowser_events (the extension buffers them).
extension/manifest.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 3,   "name": "Adom Native Browser Extension",   "short_name": "Adom",-  "version": "0.2.17",+  "version": "0.2.18",   "description": "Layer 2 of Adom Native Browser (nb): lets your Adom AI drive your real, signed-in Chrome or Edge, using your own logins.",   "minimum_chrome_version": "116",   "background": {
extension/src/cdp.js+18−1
@@ -43,7 +43,24 @@ async function attach(tabId) {   await new Promise((resolve, reject) => {     chrome.debugger.attach(debuggee(tabId), PROTO, () => {       const err = chrome.runtime.lastError;-      if (err && !/already attached/i.test(err.message)) reject(new Error(err.message));+      if (err && !/already attached/i.test(err.message)) {+        // Chrome's raw refusals are unreadable ("Cannot access a chrome-extension:// URL of different+        // extension"), and an AI cannot tell a transient failure from a page it will never drive this way+        // (Drew Owens, nb-bridge#51 item 8). Translate the known ones.+        const m = String(err.message || '');+        const e = new Error(m);+        if (/chrome-extension:\/\/ URL of different extension/i.test(m)) {+          e.errorCode = 'blocked_by_other_extension';+          e.hint = "Another extension has a frame on this page (a password manager on a sign-in page does this), and Chrome will not let the debugger attach through it. CDP verbs (eval, click by selector, screenshot) cannot run here. Use the native verbs instead (nbrowser_navigate, nbrowser_wait_for, nbrowser_list_tabs), or ask the user to finish this page themselves, or retry once past the sign-in.";+        } else if (/cannot attach to the page|cannot access a chrome:\/\/|devtools:\/\//i.test(m)) {+          e.errorCode = 'page_not_debuggable';+          e.hint = 'Chrome forbids the debugger on its own pages (chrome://, the Web Store, devtools). Drive a normal http(s) page, or use ab desktop verbs for browser chrome.';+        } else if (/another debugger|devtools is already/i.test(m)) {+          e.errorCode = 'debugger_in_use';+          e.hint = "Something else is already debugging this tab (an open DevTools window, or another automation tool). Close DevTools on that tab and retry.";+        }+        reject(e);+      }       else resolve();     });   });
extension/src/sw.js+4−3
@@ -173,9 +173,9 @@ function scheduleEmailRetry() {   }, Math.min(1500 * _emailTries, 10000)); } -function reply(id, ok, payload, error, hint) {+function reply(id, ok, payload, error, hint, errorCode) {   if (!port) return;-  port.postMessage(ok ? { id, ok: true, payload } : { id, ok: false, error, _hint: hint });+  port.postMessage(ok ? { id, ok: true, payload } : { id, ok: false, error, _hint: hint, errorCode }); }  async function onMessage(msg) {@@ -202,7 +202,8 @@ async function onMessage(msg) {   } catch (e) {     // GuardError (ownership/no-target refusals) carries a teaching hint — deliver it verbatim so     // the calling AI is corrected at the moment of need, not pointed at a skill it may never read.-    reply(id, false, undefined, String((e && e.message) || e), (e && e.hintText) || 'verb threw in extension');+    // A translated CDP refusal carries its own errorCode + hint (nb-bridge#51 item 8); pass both through.+    reply(id, false, undefined, String((e && e.message) || e), (e && (e.hintText || e.hint)) || 'verb threw in extension', (e && e.errorCode) || undefined);   } } 
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "nb-bridge",-  "version": "0.16.39",+  "version": "0.16.40",   "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.",
skills/installing-the-extension/SKILL.md+18
@@ -260,6 +260,24 @@ three people's signed-in profiles. Start to "Extension loaded" took about four m   with 0.2.15 on disk); one `nbrowser_dev_reload {profile}` brought it to the files on disk. - **The card's Reload button is not invokable by UI automation;** use `nbrowser_dev_reload` once connected. +## macOS (EXPERIMENTAL, nb-bridge#51)++This skill's Windows flow (UIA clicks, PowerShell, HKCU) does **not** apply on a Mac. The bridge, host and+extension all run there, but registration is a file and the install is a script the USER runs:++1. `install/install-mac.sh` from the bridge's own folder. It fetches the host and extension into+   `~/.adom/nb-bridge`, writes `host/native-host.sh` (which reads the Node the bridge published), and+   registers `inc.adom.native_browser` for Chrome and Edge under `~/Library/Application Support`.+2. The user then loads the extension: `chrome://extensions` -> Developer mode -> Load unpacked ->+   **Cmd+Shift+G** (the `~/.adom` folder is hidden in the picker) -> `~/.adom/nb-bridge/extension`.+3. `nbrowser_readiness` should report ready within seconds, and on a Mac its hint repeats step 1.++Why the user runs it: a cloud agent's safety classifier refused to write a native-messaging host remotely,+twice, even with the user's consent (Drew Owens). Hand them the one command instead of fighting that.++Still Windows-only on a Mac: background-window enforcement (#52 - open with `state:"minimized"`), and+`nbrowser_credentials`, which refuses with `unsupported_platform` rather than pretending there are no logins.+ ## Chrome AND Edge They are independent. Repeat the flow per browser (matching scheme: `chrome://` vs `edge://`; the toggle sits top-right on Chrome, bottom-left on Edge). The host is registered per browser, so each connects its
tools/verb-battery.mjs+12−2
@@ -73,6 +73,16 @@ const T = {   profiles: () => { const r = call('nbrowser_profiles'); const ps = r.profiles || []; ctx.profileCount = ps.length; const def = ps.find((p) => p.profile === r.active); const pups = ps.filter((p) => p.origin === 'pup').length; return A(ps.length > 0 && !(def && def.origin === 'pup'), `${ps.length} profiles, default ${r.active} (origin ${def ? def.origin : 'none'}), ${pups} pup connection(s) set aside`, `default is pup's browser or no profiles: ${JSON.stringify(r).slice(0, 160)}`); },   resolve_profiles: () => { const r = call('nbrowser_resolve_profiles'); return A(r.ok !== false, 'resolved', JSON.stringify(r).slice(0, 120)); },   // An Edge-only box is a PASS: nb drives whatever native browser is there (nb-bridge#46).+  // nb-bridge#51 item 8: a CDP refusal must arrive as a structured errorCode + hint, never Chrome's raw text.+  eval_on_browser_page: () => {+    const sid = SID + '-cdpblock';+    const o = call('nbrowser_open_window', { sessionId: sid, thread: THREAD, purpose: 'battery: a page CDP cannot attach to', profile: PROFILE, url: 'https://chrome.google.com/webstore/category/extensions' });+    const r = o.ok === true ? call('nbrowser_eval', { sessionId: sid, expression: '1+1' }) : {};+    if (o.ok === true) call('nbrowser_close_window', { sessionId: sid });+    // Either it attached (some builds allow it) or it refused WITH a code - never a bare Chrome string.+    const refusedWell = r.ok === false && !!r.errorCode && !!r._hint;+    return A(r.ok === true || refusedWell, r.ok === true ? 'page was attachable; nothing to translate' : `refused as ${r.errorCode} with a hint`, JSON.stringify({ ok: r.ok, errorCode: r.errorCode, err: String(r.error || '').slice(0, 90) }));+  },   // nb-bridge#53: with several signed-in identities connected, an unnamed open is REFUSED rather than   // acting as whichever profile happens to be active (an agent nearly posted as the wrong person).   guard_profile_required: () => {@@ -291,7 +301,7 @@ const T = { // ─── coverage map: EVERY VERB_META verb decides its fate here ─────────────── export const COVERAGE = {   status: 'live', readiness: 'live', ping: 'live', progress: 'live', scroll: 'live', input_unknown_type: 'guard', hover_raw_xy: 'live', apply_settings: 'skip:internal bridge-to-extension push, exercised by the settings test through nbrowser_settings', whoami: 'live', profiles: 'live',-  resolve_profiles: 'live', prewarm: 'live', guard_profile_required: 'guard', open_minimized: 'live', chrome_status: 'live', describe: 'live', activity: 'live',+  resolve_profiles: 'live', prewarm: 'live', eval_on_browser_page: 'live', guard_profile_required: 'guard', open_minimized: 'live', chrome_status: 'live', describe: 'live', activity: 'live',   os_windows: 'live', wake_profile: 'live', rescan: 'live', resync_sessions: 'live',   open_window: 'live', pin_session: 'live', use_profile: 'live', list_windows: 'live',   window_state: 'live', switch_window: 'live', navigate: 'live', reload: 'live',@@ -369,7 +379,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {   // are an ab lifecycle fact, not a verb regression.   call('nbrowser_wake_profile', { profile: PROFILE });   for (let i = 0; i < 10; i++) { const r = call('nbrowser_readiness'); if (r.ready === true) { console.log(`warm-up: ready after ${i} poll(s)\n`); break; } execFileSync('sleep', ['3']); }-  const order = ['status','readiness','ping','whoami','profiles','resolve_profiles','prewarm','guard_profile_required','open_minimized','chrome_status','describe','activity','os_windows','wake_profile','rescan','resync_sessions',+  const order = ['status','readiness','ping','whoami','profiles','resolve_profiles','prewarm','eval_on_browser_page','guard_profile_required','open_minimized','chrome_status','describe','activity','os_windows','wake_profile','rescan','resync_sessions',     'open_window','pin_session','use_profile','list_windows','window_state','switch_window',     'navigate','reload','back','forward','wait','wait_for','keepalive',     'eval','evaluate','screenshot','screenshot_full_res','errors','fetch_url',