← Commit history
nb 0.2.0: manifest parity (12 verbs ab could not see), nbe/ab naming cutover, adom-bridge 2.x pin
44 files changed
+720−492
AB-REQUESTS.md+41PRIVACY.md+2−2README.md+37−37TASKBAR-FEATURE.md+5−5bridge/BRIDGE_VERSION+1−1bridge/bridge.json+37−24bridge/package.json+2−2bridge/server.js+142−141dev-skills/cross-platform-dev/SKILL.md+4−4dev-skills/developing-the-bridge/SKILL.md+95−41docs/DEVELOPING.md+3−3docs/INSTALL-FLOW.md+39−39docs/UNINSTALL-FLOW.md+6−6extension/manifest.json+4−4extension/src/cdp.js+2−2extension/src/native.js+6−6extension/src/popup.js+2−2extension/src/record.js+1−1extension/src/sw.js+3−3host/host-manifest.template.json+1−1host/native-host.bat+3−3host/native-host.js+3−3install.sh+1−1install/README.md+8−8install/ab-safety-check.ps1+67install/install.ps1+8−8native-browser-bridge-manifest.json+5−5package.json+7−3page.json+17−6privacy.html+2−2publish-skills/publishing-the-bridge/SKILL.md+33−19skills/adom-native-browser/SKILL.md+10−10skills/driving-sites/SKILL.md+2−2skills/driving-the-desktop/SKILL.md+13−13skills/driving-the-extension-demo/SKILL.md+1−1skills/driving-the-extension/SKILL.md+41−41skills/installing-the-extension/SKILL.md+18−18skills/native-browser-recording/SKILL.md+5−5skills/native-browser-safety/SKILL.md+5−5skills/uninstalling-the-extension/SKILL.md+4−4store/PERMISSION-JUSTIFICATIONS.md+1−1store/STORE-LISTING.md+3−3tools/check-guard-coverage.mjs+19−1wiki/page.json+11−6AB-REQUESTS.mdadded+41@@ -0,0 +1,41 @@+# Bridge features the native-browser bridge needs++Running tracker of ab fixes/features surfaced while building this bridge. Status as of 2026-06-19.++## Shipped+- **1.8.148** — WGC per-window recorder (`desktop_record_window_*`); foreground-steal fix;+ spawn-storm backoff; `send_files` honors `dest`; `pull_file` path resolution.+- **1.8.149** — control-API discovery contract (`ADOM_DIRECT_API_URL` env injected into spawned+ bridges; `~/.adom/direct-api-port` = `host:port`); `bridge_install` cache-lock retry.+- Containment **Phase 1** green-lit (kill-switch + `--safe-mode` + crash-loop guard + headroom gate).++## Open — first-class verbs to kill `shell_execute` inline-quoting friction+The #1 time-sink integrating this bridge is shell-quoting through JSON→bash→cmd→PowerShell.+Each verb below replaces a class of quoting pain hit repeatedly this session:+1. **`run_script`** {interpreter: powershell|cmd|node|python, body, args?, cwd?} → {stdout,stderr,exitCode}.+ ab writes `body` to a temp file + runs it. THE big one — I currently `send_files` a `.ps1` then+ `shell_execute -File` it as a workaround, every time.+2. **`read_file` / `write_file`** (text or base64, absolute path, resolved in ab's own context —+ fixes intermittent `%USERPROFILE%` non-expansion in `shell_execute`).+3. **`registry_get/set/delete`** (HKCU/HKLM) — replaces `reg query/add/delete` backslash-escaping+ (native-messaging host registration was painful + error-prone).+4. **`process_list`** {nameFilter?, pidFilter?} → [{pid,name,commandLine,parentPid,handles,memory}].+ Replaces `tasklist`/`wmic`/`Get-Process`; `wmic` CommandLine returned empty (privilege). Needed+ the whole incident debug. (Also feeds the Phase-2 watchdog.)+5. **`process_kill`** {pid|name}.+6. **`net_stats`** → {tcpTotal, listening, established, timeWait, ephemeralHeadroom, perPid:{...}}.+ Replaces `netstat`/`Get-NetTCPConnection` counting; identical to what the containment watchdog needs.+7. **`launch_process`** {path, args:[], env?, cwd?, window?} → {pid}. Args as an ARRAY (no quoted-string+ parsing); replaces `Start-Process` arg-array pain.++## Open — bridge lifecycle question+- Does ab keep an on-demand-spawned bridge **alive**, or tear it down when idle with no connected+ client? The `native-browser` bridge must **persist** so the Chrome-launched native host can dial it+ (host.json port). Observed: after an `nbrowser_*` call the bridge process is gone (node=0), which+ may be why the extension's host can't establish a steady connection. Need the contract.++## Bridge-side TODO (mine, not ab)+- Wire `ADOM_DIRECT_API_URL` env discovery (1.8.149 contract) into the bridge's `discoverAdDirectPort`.+- Harden the host singleton: a stale `host.pid` whose PID was **reused** by a non-host process caused+ every new host to exit (false positive). Verify the incumbent PID is actually a node/native-host+ process before deferring; otherwise take over.
PRIVACY.md+2−2@@ -38,7 +38,7 @@ them. ## Where information goes, and how it's used The Extension connects, through a Chrome **native-messaging host on your own computer**, to your local-**Adom Desktop** application. Commands flow to the Extension and results — including screenshots and page+**Bridge** application. Commands flow to the Extension and results — including screenshots and page data — flow back to **your own active Adom AI session** so it can complete the task you asked for. Depending on how you run Adom, that session may execute in **Adom cloud infrastructure you control**, in which case the data needed for your task transits to that session to fulfill your request — the same way@@ -65,7 +65,7 @@ drive it. Connections out to your Adom session use authenticated, encrypted tran ## Your controls - Remove the Extension at any time (Chrome/Edge → Extensions → Remove) to end all access.-- The Extension only acts while your Adom Desktop and an Adom AI session are connected — with them+- The Extension only acts while your Bridge and an Adom AI session are connected — with them closed, it's idle. - For any question about your data, email **[email protected]**.
README.md+37−37@@ -26,12 +26,12 @@ At this layer the AI can open and manage windows but cannot see inside them: no no screenshots. You do the in-page work. That is enough for a surprising number of jobs, above all finishing a sign-in. -**Layer 2, abe: let the AI actually drive. The upgrade.**-Install the Adom browser extension into a profile and that profile becomes fully drivable: navigate,+**Layer 2, nbe: let the AI actually drive. The upgrade.**+Install nbe, the Adom Native Browser Extension, into a profile and that profile becomes fully drivable: navigate, read the page, click by visible text, type with real `isTrusted` input, upload files with no OS picker, run JavaScript, screenshot and record in the background, wait on selectors and downloads, use saved logins and TOTP behind strict gates. This is the counterpart to `pup`, with the same command-surface (`nbrowser_*` mirrors `browser_*`), except every verb runs inside your *actual* logged-in+surface (`nbrowser_*` mirrors `pup_*`), except every verb runs inside your *actual* logged-in profile, so it sails through login walls and captchas that block pup's empty controlled Chromium. Layer 2 is also where the trust surface lives. Every window an AI opens is registered with a stated@@ -42,7 +42,7 @@ that rule is enforced in code, not policy. Every layer-1 response tells the AI honestly what it could not do and what layer 2 would add, so the upgrade suggestion arrives at a natural moment instead of as a wall. -Cloud containers, Hydrogen, and the `adom-desktop` CLI all reach this the same way they reach pup:+Cloud containers, Hydrogen, and the `adom-bridge-cli` CLI all reach this the same way they reach pup: `relay -> Bridge -> bridge`. ## Who we are, and why this extension needs broad browser capabilities@@ -163,11 +163,11 @@ point it at, via the debugger/scripting APIs - never by loading external scripts auto-route there); `nbrowser_resolve_profiles` eagerly resolves each profile's email so keys are `chrome:[email protected]` up front instead of `pending-N`. - **The AI installs it for you, on a fresh machine, with nothing pre-installed** - proven end-to-end:- a brand-new adom-desktop with no bridge and **no system Node** goes from zero to a drivable, real- signed-in browser (the bridge reuses adom-desktop's managed Node so the native host always starts).+ a brand-new Bridge install with no bridge and **no system Node** goes from zero to a drivable, real+ signed-in browser (the bridge reuses Bridge's managed Node so the native host always starts). See **[The AI installs it for you](#the-ai-installs-it-for-you-auto-install)**. - **Cold-start aware** - `nbrowser_readiness` is a read-only probe (`ready` / `needs-extension`) so the- AI knows to offer the install exactly when you need a login, and the AD-level `bridge_readiness`+ AI knows to offer the install exactly when you need a login, and the ab-level `bridge_readiness` answers "what tools do I have" without spawning anything. - **Drive ALL your browser profiles** (your work + personal Chrome/Edge profiles, …), not just one - `nbrowser_profiles` / `nbrowser_use_profile` switch instantly, with a persistent **security blocklist** to exclude any.@@ -180,7 +180,7 @@ point it at, via the debugger/scripting APIs - never by loading external scripts ## Fork & develop your own -Third parties are welcome to build their own version. The **[Fork & develop guide →](https://wiki.adom.inc/adom/adom-native-browser/files/docs/DEVELOPING.md)** walks through getting the source, loading it in **Chrome and Edge developer mode** (with screenshots), having **adom-desktop load it for you**, and the edit → reload dev loop.+Third parties are welcome to build their own version. The **[Fork & develop guide →](https://wiki.adom.inc/adom/adom-native-browser/files/docs/DEVELOPING.md)** walks through getting the source, loading it in **Chrome and Edge developer mode** (with screenshots), having **Bridge load it for you**, and the edit → reload dev loop. ## Sub-skills @@ -191,11 +191,11 @@ it connect" dead end: | Stream | What it is | Where you see it | Current | |---|---|---|---|-| **Bridge runtime** | the local server Bridge runs (`bridge.json` version) | the **Download** card | 0.1.50 |-| **Extension + host** | the MV3 extension + native host loaded into Chrome/Edge (`extension/manifest.json`) | `chrome://extensions` | 0.1.3 |-| **Skills package** | the docs your AI reads (`pkg install`, root `package.json`) | the **Install** card | 0.4.x |+| **Bridge runtime** | the local server Bridge runs (`bridge.json` version) | the **Download** card | 0.2.0 |+| **Extension + host** | the MV3 extension + native host loaded into Chrome/Edge (`extension/manifest.json`) | `chrome://extensions` | 0.2.0 |+| **Skills package** | the docs your AI reads (`pkg install`, root `package.json`) | the **Install** card | 0.5.x | -The bridge and the extension are BOTH `0.1.x` but are **different numbers** and update independently.+The bridge and the extension are BOTH `0.2.x` but are **different numbers** and update independently. The skew that matters for connectivity is **extension vs `bridge.json` `expectedExtensionVersion`** (the extension the running bridge speaks to), NOT the `0.4.x` skills-package number. If the native host keeps exiting ("Native host has exited"), your extension/host is too old for the running bridge: re-sync the@@ -224,7 +224,7 @@ to its full readme. |---|---| | [`driving-the-extension`](https://wiki.adom.inc/adom/adom-native-browser/files/skills/driving-the-extension/SKILL.md) | The full `nbrowser_*` surface: the CDP-vs-native "debugging" bar, `isTrusted` input + xy-finding, multi-profile driving, the credential-library query + login automation, the taskbar status feature. | | [`native-browser-safety`](https://wiki.adom.inc/adom/adom-native-browser/files/skills/native-browser-safety/SKILL.md) | The cardinal safety rules: only drive windows YOU opened, background-by-default, the credential gating, and the blocklist. Read before driving. |-| [`installing-the-extension`](https://wiki.adom.inc/adom/adom-native-browser/files/skills/installing-the-extension/SKILL.md) | How WE install the extension into the user's Chrome/Edge over adom-desktop's generic verbs (dev-mode Load-unpacked, one-click consent, verify-by-screenshot). |+| [`installing-the-extension`](https://wiki.adom.inc/adom/adom-native-browser/files/skills/installing-the-extension/SKILL.md) | How WE install the extension into the user's Chrome/Edge over Bridge's generic verbs (dev-mode Load-unpacked, one-click consent, verify-by-screenshot). | | [`uninstalling-the-extension`](https://wiki.adom.inc/adom/adom-native-browser/files/skills/uninstalling-the-extension/SKILL.md) | Clean removal - filter to the Adom card so Remove is unambiguous, the confirm-bubble beat, verify by screenshot + dropped profile key. | | [`native-browser-recording`](https://wiki.adom.inc/adom/adom-native-browser/files/skills/native-browser-recording/SKILL.md) | Full-framerate **background** recording of the user's real, logged-in Chrome - the occlusion-flag mode, the restart-into-background-recording flow, and the offer-to-revert-for-battery step (the unique capability). | | [`driving-the-desktop`](https://wiki.adom.inc/adom/adom-native-browser/files/skills/driving-the-desktop/SKILL.md) | OS-level control via `desktop_*` when CDP can't reach (extension lifecycle, `chrome://` pages, native dialogs, popups) - including the ⛔ never-drive-a-PWA-window guard and the v20 saved-password ban. |@@ -261,7 +261,7 @@ use just work. Saved passwords stay with the browser; its own autofill enters th New to this and wondering what happens the first time, from a cloud / Hydrogen container all the way to your real browser doing something useful? The **[First-time install flow →](https://wiki.adom.inc/adom/adom-native-browser/files/docs/INSTALL-FLOW.md)**-walks every step and endpoint: how the container reaches adom-desktop over the relay, who installs the+walks every step and endpoint: how the container reaches Bridge over the relay, who installs the bridge / host / extension (and who does NOT), how the bridge knows the extension connected, how skills propagate, and how third parties add sub-skills so the extension keeps getting better. @@ -280,12 +280,12 @@ about a minute, you click one **Start** button, that's it. How the AI drives it (the flow, proven end-to-end on a fresh machine): 1. **Detect** - a plain request like *"log into DigiKey for me"* surfaces this tool via wiki discovery;- the AI checks `adom-desktop nbrowser_readiness`.-2. **Install the bridge if missing** - `bridge_install {manifestUrl}`. adom-desktop provisions Node+ the AI checks `adom-bridge-cli nbrowser_readiness`.+2. **Install the bridge if missing** - `bridge_install {manifestUrl}`. Bridge provisions Node itself (system, or a portable no-UAC copy) and spawns the bridge - **you are never asked to install Node or anything else.** 3. **Offer + load the extension** - if readiness says `needs-extension`, the AI asks once (a- `notify_user` Start button), then drives your browser's dev-mode **Load unpacked** over adom-desktop's+ `notify_user` Start button), then drives your browser's dev-mode **Load unpacked** over Bridge's generic OS verbs (push the `extension/` folder, register the native-messaging host in HKCU, toggle Developer mode, pick the folder), and **verifies by screenshot** that the Adom card loaded enabled with no errors.@@ -299,23 +299,23 @@ Cardinal rule throughout: **the AI does everything it can itself - it never make | Dependency | Required? | Why | |---|---|---|-| **[adom-desktop](https://wiki.adom.inc/adom/adom-desktop)** (`>= 1.9.47`) | **Yes** | This is an adom-desktop **bridge**. The container drives it entirely through the `adom-desktop` CLI (`nbrowser_*` verbs relayed to the bridge). adom-desktop also spawns the bridge, provisions its Node runtime, and auto-updates it. The one hard dependency. |-| **Node** (in adom-desktop) | Yes, **auto-provided** | The bridge + native host are Node. **You never install it** - adom-desktop provisions Node 22 (system if present, else a portable no-UAC copy) and the bridge publishes that path so the Chrome-launched host reuses it. (SDK runtime contract, v1.9.63+.) |+| **[Bridge](https://wiki.adom.inc/adom/adom-bridge)** (`>= 2.0.3`) | **Yes** | This is a **bridge that rides Bridge**. The container drives it entirely through the `adom-bridge-cli` CLI (`nbrowser_*` verbs relayed to the bridge). Bridge also spawns the bridge, provisions its Node runtime, and auto-updates it. The one hard dependency. |+| **Node** (in Bridge) | Yes, **auto-provided** | The bridge + native host are Node. **You never install it** - Bridge provisions Node 22 (system if present, else a portable no-UAC copy) and the bridge publishes that path so the Chrome-launched host reuses it. (SDK runtime contract.) | | **Chrome and/or Edge** | Yes | The extension loads into your real browser. Either works (one codebase); the extension ID is pinned so the native-host registration is deterministic. | | The **MV3 extension + native-messaging host** | Yes, **installed by us** | Shipped in this repo (`extension/`, `host/`); the AI deploys + registers them during auto-install. | | npm packages (for the bridge) | **None** | The bridge uses only Node core (`http`/`net`); its Release zip is source-only, no `node_modules`. | -The **skills package** declares `adom/adom-desktop` as its one dependency; everything else is either-provided by adom-desktop or bundled in this repo.+The **skills package** declares `adom/adom-bridge` as its one dependency; everything else is either+provided by Bridge or bundled in this repo. ## Architecture (why it's shaped this way) An MV3 extension can't host a listening port, and per-[`adom-desktop/skills/PORTS.md`](../adom-desktop/skills/PORTS.md) **no fixed/scan port survives+[`adom-bridge/skills/PORTS.md`](../adom-bridge/skills/PORTS.md) **no fixed/scan port survives Windows** (HNS silently reserves contiguous chunks). So the transport is **native messaging**: ```-AD ──HTTP /command (ephemeral, discovery file)──► bridge/server.js+ab ──HTTP /command (ephemeral, discovery file)──► bridge/server.js ▲ raw-TCP loopback (ephemeral) │ host/native-host.js (Chrome-launched, stdio↔TCP pump)@@ -326,8 +326,8 @@ AD ──HTTP /command (ephemeral, discovery file)──► bridge/server.js └─ chrome.tabs/downloads/cookies → native-API verbs ``` -- **bridge/** - AD-spawned Node server. Binds `127.0.0.1:0` (ephemeral, PORTS.md-compliant), writes a- discovery file, exposes `/status` + `/command` to AD, and a raw-TCP loopback the host dials.+- **bridge/** - ab-spawned Node server. Binds `127.0.0.1:0` (ephemeral, PORTS.md-compliant), writes a+ discovery file, exposes `/status` + `/command` to ab, and a raw-TCP loopback the host dials. - **host/** - tiny native-messaging host. Chrome/Edge launch it on `connectNative('inc.adom.native_browser')`; it reads the bridge's discovery file and pumps stdio ↔ the bridge loopback. The extension never touches a TCP port.@@ -339,7 +339,7 @@ AD ──HTTP /command (ephemeral, discovery file)──► bridge/server.js | Path | What | |---|---| | `extension/` | MV3 extension (manifest, `src/sw.js`, `src/cdp.js`, `src/native.js`, popup) |-| `bridge/` | AD bridge (`bridge.json` manifest + `server.js` + `package.json`) |+| `bridge/` | ab bridge (`bridge.json` manifest + `server.js` + `package.json`) | | `host/` | native-messaging host (`native-host.js`, `.bat`, manifest template) | | `install/` | bridges-cache copy + HKCU host registration + force-install policy | | `skills/` | repo-local skills (incl. the Windows-first → Kyle-ports-Mac dev flow) |@@ -361,7 +361,7 @@ Driving the user's real browser, verified end-to-end: - **Taskbar status** - `flash`/`taskbar` paint a "come look" flash, a progress bar (green/yellow-for-human-gates/red), and an **"Adom is driving" overlay badge** on the real browser's taskbar button (modes: `quiet`/`chatty`/`off`). See [`TASKBAR-FEATURE.md`](TASKBAR-FEATURE.md).-- **Recording** - `record_start {method:"wgc"}` (Windows, via adom-desktop - no picker, background) or+- **Recording** - `record_start {method:"wgc"}` (Windows, via Bridge - no picker, background) or `getdisplaymedia` (cross-platform). **The full verb catalog is self-describing.** The list above is the 80% surface; the bridge exposes@@ -379,7 +379,7 @@ show it. ```bash # 1. Run the bridge standalone (binds an ephemeral port + writes the discovery file): node bridge/server.js-# -> prints: [native-browser] bridge up: AD http :<P> host tcp :<H>+# -> prints: [native-browser] bridge up: ab http :<P> host tcp :<H> # GET http://127.0.0.1:<P>/status -> {"ok":true,"bridge":"native-browser",...} # 2. Load extension/ unpacked in Chrome AND Edge (chrome://extensions → Developer mode → Load unpacked)@@ -391,8 +391,8 @@ node bridge/server.js # HKCU\Software\Google\Chrome\NativeMessagingHosts\inc.adom.native_browser -> manifest path # (Edge: ...\Microsoft\Edge\NativeMessagingHosts\...) -# 4. The extension popup should show "connected". Then through AD:-# adom-desktop nbrowser_status -> {ok:true, ext:"adom-browser-extension", browser:"chrome"|"edge"}+# 4. The extension popup should show "connected". Then through ab:+# adom-bridge-cli nbrowser_status -> {ok:true, ext:"nbe", browser:"chrome"|"edge"} ``` ## Recording - full-framerate, in the background (the doctrine)@@ -401,7 +401,7 @@ node bridge/server.js > **full-resolution, high-framerate (20-60 fps)** capture. That single requirement drives every choice > here. **The enemy is *paint-throttling*, not the codec:** Chrome correctly stops painting occluded / > background / static windows (battery), and capture is event-driven, so a throttled page silently drops to-> ~5 fps ("frozen" / sped-up video). Every technique below keeps frames flowing. *(Pup's `browser_record_start`+> ~5 fps ("frozen" / sped-up video). Every technique below keeps frames flowing. *(Pup's `pup_record_start` > CDP screencast is a perfectly good 20-30 fps engine when the page paints - the full cross-surface reference, > incl. Hydrogen and the foreground-rebump trick, is in the demo-recording skill's `recording-mechanics.md`.)* @@ -444,21 +444,21 @@ battery) · use CDP screencast for demo video (low framerate, ugly). ### The two engines The browser can be recorded with **either** of two engines. Pick with `method`; on Windows it defaults to-`wgc` (via adom-desktop), otherwise `getdisplaymedia`.+`wgc` (via Bridge), otherwise `getdisplaymedia`. -| | **`getdisplaymedia`** (a.k.a. desktopCapture) | **`wgc`** (Windows Graphics Capture, via adom-desktop 1.8.148+) |+| | **`getdisplaymedia`** (a.k.a. desktopCapture) | **`wgc`** (Windows Graphics Capture, via Bridge) | |---|---|---|-| **Platforms** | ✅ macOS, Ubuntu, Windows (cross-platform) | ⛔ Windows only (needs adom-desktop 1.8.148+) |+| **Platforms** | ✅ macOS, Ubuntu, Windows (cross-platform) | ⛔ Windows only (needs Bridge) | | **Engine** | Same WebRTC pipeline Google Meet uses | OS-level Windows Graphics Capture → mp4 | | **FPS** | High (30-60) | High (up to 60) | | **Picker** | ❌ Chrome forces a one-time "choose what to share" picker per recording - no silent variant exists in any browser | ✅ None - agent picks the window by handle | | **Background** | ✅ keeps capturing the chosen surface even when occluded… | ✅ true background | | **Display wake-lock** | ⚠️ holds the display awake while sharing (can't blank the screen) | ✅ none | | **Output** | WebM (base64 returned) | mp4 (file path) |-| **Where it runs** | the extension (offscreen `MediaRecorder`) | the **bridge → adom-desktop** (`desktop_record_window_*`, standalone - never HD) |+| **Where it runs** | the extension (offscreen `MediaRecorder`) | the **bridge → Bridge** (`desktop_record_window_*`, standalone - never HD) | -**Rule of thumb:** on Windows (adom-desktop 1.8.148+), use **`wgc`** - it's picker-free and truly background,-ideal for "record while the user does something else." Everywhere else (Mac/Ubuntu, or older AD), use+**Rule of thumb:** on Windows (Bridge), use **`wgc`** - it's picker-free and truly background,+ideal for "record while the user does something else." Everywhere else (Mac/Ubuntu, or older ab), use **`getdisplaymedia`** and accept the one-time share picker. The verbs expose both so the caller/AI can choose and we can compare them head-to-head.
TASKBAR-FEATURE.md+5−5@@ -15,7 +15,7 @@ Only these work cross-window (ITaskbarList3 + FlashWindowEx take any hwnd): with a custom icon; `"none"` clears. (Thumbnail-toolbar buttons, jump lists, iconic/live-preview only work on the owner's OWN window, so-they'd be AD's button — "AI cockpit", phase 2. A DWM live thumbnail of the browser inside AD is the+they'd be ab's button — "AI cockpit", phase 2. A DWM live thumbnail of the browser inside ab is the juicy phase-2 item.) ## Flash modes (set by USER or AI)@@ -25,12 +25,12 @@ juicy phase-2 item.) users who want to watch everything. - `off` — no auto-flash; explicit `nbrowser_flash` still works. -## Bridge verb surface (bridge owns the logic; AD owns the OS primitive)+## Bridge verb surface (bridge owns the logic; ab owns the OS primitive) - `nbrowser_flash {sessionId?}` / `nbrowser_flash_stop {sessionId?}` - `nbrowser_flash_mode {mode:"quiet"|"chatty"|"off"}` - `nbrowser_taskbar {sessionId?, progress?:{state,value?}, overlay?:{badge,tooltip?}}` - **hwnd captured at control-start**: on `nbrowser_open_window` the bridge resolves the OS hwnd (via- AD GetForegroundWindow / desktop_list_windows — the just-opened window is focused) and caches+ ab GetForegroundWindow / desktop_list_windows — the just-opened window is focused) and caches `sessionId → hwnd`. All taskbar ops target the cached hwnd → never the wrong window. - On control-start the bridge sets overlay=Adom by default. @@ -42,14 +42,14 @@ juicy phase-2 item.) - `nbrowser_flash_mode` echoes the new mode + that the user can change it anytime. - Teach WHY/WHEN (courtesy to the user, tied to their preference), not just HOW. -## Dependency: AD `desktop_taskbar` verb (prompt sent to AD thread)+## Dependency: ab `desktop_taskbar` verb (prompt sent to ab thread) `desktop_taskbar { hwnd, flash?, progress?:{state,value?}, overlay?:{badge:"adom"(default)|"<named>"|"none", icon?:"<path|base64>", tooltip?} } -> {ok}` - Lightweight persistent COM (ITaskbarList3 init once), no per-call process spawn. - Ships the Adom overlay icon; accepts custom icons for 3rd-party bridges. - Flash is the only channel doable via run_script fallback before this lands; progress/overlay need it. ## Build order-1. AD confirms `desktop_taskbar` shape (+ a foreground-hwnd helper if needed).+1. ab confirms `desktop_taskbar` shape (+ a foreground-hwnd helper if needed). 2. Bridge: hwnd capture on open_window + the 4 verbs + modes + auto-flash hook + Adom-overlay default + the rich hints. Extension: report active-window title if needed for hwnd resolution of adopted windows. 3. Test: open window → correct hwnd flashes; progress green/yellow/red; Adom badge shows while driving.
bridge/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.1.55+0.2.0
bridge/bridge.json+37−24@@ -2,9 +2,9 @@ "manifest_version": 1, "name": "native-browser", "displayName": "Adom Native Browser",- "version": "0.1.55",- "expectedExtensionVersion": "0.1.3",- "description": "Drives the user's REAL signed-in Chrome/Edge. MOST verbs require the Adom extension installed in that browser profile (CDP driving: navigate/click/read/screenshot). TWO verbs need NO extension at all and work on any machine: nbrowser_open_os_window (open a URL in the user's real profile, background+flashing - the OAuth/consent path) and nbrowser_os_windows (enumerate browser windows). Also extension-free: nbrowser_profiles, nbrowser_wake_profile, nbrowser_force_close. The bridge binds an ephemeral loopback port and the native-messaging host pumps stdio <-> bridge.",+ "version": "0.2.0",+ "expectedExtensionVersion": "0.2.0",+ "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/adom-native-browser", "author": "Adom Inc.", "license": "MIT",@@ -23,12 +23,15 @@ "verbs": [ "nbrowser_status", "nbrowser_readiness",- "nbrowser_chrome_status", "nbrowser_install_chrome",+ "nbrowser_chrome_status", "nbrowser_ping", "nbrowser_activity", "nbrowser_open_window", "nbrowser_close_window",+ "nbrowser_owned",+ "nbrowser_cleanup",+ "nbrowser_force_close", "nbrowser_switch_window", "nbrowser_list_windows", "nbrowser_window_state",@@ -37,14 +40,16 @@ "nbrowser_credentials", "nbrowser_profiles", "nbrowser_use_profile",+ "nbrowser_pin_session",+ "nbrowser_open_os_window",+ "nbrowser_os_windows",+ "nbrowser_wake_profile",+ "nbrowser_resolve_profiles",+ "nbrowser_whoami", "nbrowser_profile_block", "nbrowser_profile_unblock", "nbrowser_rescan", "nbrowser_resync_sessions",- "nbrowser_owned",- "nbrowser_cleanup",- "nbrowser_force_close",- "nbrowser_handle_dialog", "nbrowser_open_tab", "nbrowser_close_tab", "nbrowser_switch_tab",@@ -54,17 +59,24 @@ "nbrowser_back", "nbrowser_forward", "nbrowser_wait",- "nbrowser_keepalive", "nbrowser_wait_for",+ "nbrowser_keepalive", "nbrowser_screenshot", "nbrowser_screenshot_full_res", "nbrowser_eval", "nbrowser_evaluate", "nbrowser_input_dispatch", "nbrowser_click",+ "nbrowser_select",+ "nbrowser_set_file_input",+ "nbrowser_set_date",+ "nbrowser_totp",+ "nbrowser_register_totp",+ "nbrowser_add_extension_origin", "nbrowser_type", "nbrowser_press_key", "nbrowser_fetch_url",+ "nbrowser_download_wait", "nbrowser_errors", "nbrowser_close", "nbrowser_record_start",@@ -76,29 +88,30 @@ "nbrowser_flash_mode", "nbrowser_taskbar", "nbrowser_describe",- "nbrowser_set_date",- "nbrowser_set_file_input",- "nbrowser_download_wait",- "nbrowser_totp",- "nbrowser_register_totp",- "nbrowser_add_extension_origin",- "nbrowser_select",- "nbrowser_whoami",- "nbrowser_pin_session",- "nbrowser_resolve_profiles",- "nbrowser_wake_profile",- "nbrowser_open_os_window",- "nbrowser_os_windows"+ "nbrowser_handle_dialog",+ "nbrowser_dev_reload",+ "nbrowser_clear_cookies",+ "nbrowser_caption",+ "nbrowser_focus",+ "nbrowser_hover",+ "nbrowser_double_click",+ "nbrowser_right_click",+ "nbrowser_login_form",+ "nbrowser_autologin",+ "nbrowser_cursor",+ "nbrowser_events",+ "nbrowser_download" ], "dependencies": { "node": ">=18" }, "category": "Browser Automation", "tags": [+ "nb",+ "nbe", "native-browser", "chrome", "edge",- "extension", "nbrowser", "pup-parity" ],@@ -137,4 +150,4 @@ } ] }-}+}
bridge/package.json+2−2@@ -1,8 +1,8 @@ { "name": "adom-native-browser-bridge",- "version": "0.1.55",+ "version": "0.2.0", "private": true,- "description": "adom-desktop bridge for the Adom native-browser extension (Chrome + Edge)",+ "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", "bin": { "adom-native-browser-bridge": "server.js"
bridge/server.js+142−141@@ -3,10 +3,10 @@ /* * adom-native-browser :: bridge * --------------------------------- * AD-spawned Node server. Two listeners, BOTH ephemeral (per adom-desktop/skills/PORTS.md - a fixed+ * ab-spawned Node server. Two listeners, BOTH ephemeral (per adom-bridge/skills/PORTS.md - a fixed * or scan-range port dies silently on HNS-reserved Windows machines): *- * - HTTP server : AD talks /status + /command here (the standard bridge contract). AD allocates+ * - HTTP server : ab talks /status + /command here (the standard bridge contract). ab allocates * the port (bridge.json port:0) and passes it via `--port`; we fall back to an OS * ephemeral when run standalone. * - raw-TCP server (host loopback): the native-messaging host (Chrome-launched) dials this and@@ -31,7 +31,7 @@ const { execFile } = require('child_process'); const BRIDGE = 'native-browser'; let VERSION = '0.0.1'; try { VERSION = require('./package.json').version || VERSION; } catch {}-// bridge.json LAST so it wins - it's the manifest AD itself reads and reports, and it's what we bump+// bridge.json LAST so it wins - it's the manifest ab itself reads and reports, and it's what we bump // per release, so the code's self-reported VERSION can never drift from the release version. try { VERSION = require('./bridge.json').version || VERSION; } catch {} @@ -51,19 +51,19 @@ function cmpVer(a, b) { 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-// bridge, however, IS spawned by AD, so process.execPath is exactly the Node AD provisioned (system or+// inherit ab's managed-runtime PATH (ab only puts its Node on the PATH of processes IT spawns). THIS+// bridge, however, IS spawned by ab, so process.execPath is exactly the Node ab provisioned (system or // the portable no-UAC copy). We publish that absolute path here in plain text so native-host.bat can-// point at a guaranteed-present Node even on a machine with no system Node. Self-heals across AD runtime-// upgrades: every respawn rewrites it. See the adom-desktop-bridges SDK "runtime contract".+// point at a guaranteed-present Node even on a machine with no system Node. Self-heals across ab runtime+// upgrades: every respawn rewrites it. See the adom-bridge-sdk SDK "runtime contract". const NODEPATHFILE = path.join(os.homedir(), '.adom', 'bridges', BRIDGE, 'node-path.txt'); const DEBUGLOG = path.join(os.homedir(), '.adom', 'bridges', BRIDGE, 'bridge-debug.log'); // ---- persistence hardening --------------------------------------------------// AD spawns dynamic bridges fire-and-forget and REUSES a still-running one via its health probe-// (confirmed by the AD team: AD never kills dynamic bridges). So we must be a long-lived server and+// ab spawns dynamic bridges fire-and-forget and REUSES a still-running one via its health probe+// (confirmed by the ab team: ab never kills dynamic bridges). So we must be a long-lived server and // never exit on our own. The silent killers to guard:-// - EPIPE when AD drops our stdout/stderr pipe after spawn (a stray write would otherwise crash us),+// - EPIPE when ab drops our stdout/stderr pipe after spawn (a stray write would otherwise crash us), // - any uncaught exception / rejection (log it, don't die), // - the event loop somehow emptying (a no-op interval pins it open). function dlog(m) { try { fs.appendFileSync(DEBUGLOG, `${new Date().toISOString()} ${m}\n`); } catch {} }@@ -73,7 +73,7 @@ process.on('uncaughtException', (e) => dlog(`uncaughtException: ${(e && e.stack) process.on('unhandledRejection', (e) => dlog(`unhandledRejection: ${(e && e.stack) || e}`)); setInterval(() => {}, 60000); // keep-alive: the listeners already pin the loop, this is belt-and-suspenders -// ---- pending AD commands awaiting an extension reply -----------------------+// ---- pending ab commands awaiting an extension reply ----------------------- let seq = 0; const pending = new Map(); // id -> { resolve, timer } let lastVerb = null, lastActivityAt = 0;@@ -231,8 +231,8 @@ function registerConn(sock, profile, browser, version) { dlog(`hello: profile ${key} registered (${conns.size} total)`); } -// Bridge-self-reported chip status (led/summary/tooltip). AD is a pure renderer - ALL the reasoning-// about what green/yellow/red means lives HERE, since only this bridge knows its own domain. AD just+// Bridge-self-reported chip status (led/summary/tooltip). ab is a pure renderer - ALL the reasoning+// about what green/yellow/red means lives HERE, since only this bridge knows its own domain. ab just // shows the color + prints the tooltip on hover; it owns only the offline/gray state (a dead bridge // can't self-report). See /status. function statusReport() {@@ -252,7 +252,7 @@ function statusReport() { return { led: 'green', summary: live.length === 1 ? 'Extension connected' : `${live.length} profiles connected`,- tooltip: `Adom browser extension driving ${live.length} profile(s): ${names}.\n▶ = active default target (switch with nbrowser_use_profile).${last}${blocked}`,+ tooltip: `nbe driving ${live.length} profile(s): ${names}.\n▶ = active default target (switch with nbrowser_use_profile).${last}${blocked}`, }; } return {@@ -272,8 +272,8 @@ const HELP = { overview: 'Drive the user\'s REAL logged-in Chrome/Edge. Verbs mirror pup\'s browser_* as nbrowser_*.', pitfalls: [ 'The extension lives in ONE browser PROFILE and DIES if that profile has no open windows - if you get "not connected", ask the user to open a window in that profile (or open one yourself once reconnected). Never close your LAST window in that profile.',- 'Multiple Adom Desktops on the relay → pass `--target <name>` (e.g. AdomLapper) or you get ambiguous_target.',- 'Screenshots are large base64 and can TIME OUT over the relay - retry, or prefer AD desktop_screenshot_window for proof shots.',+ 'Multiple Bridges on the relay → pass `--target <name>` (e.g. AdomLapper) or you get ambiguous_target.',+ 'Screenshots are large base64 and can TIME OUT over the relay - retry, or prefer ab desktop_screenshot_window for proof shots.', 'nbrowser_eval runs in the PAGE world: chrome.* APIs are NOT available there (only DOM/window).', 'ALWAYS clean up windows you open (nbrowser_close_window) - abandoned windows annoy the user.', 'Poll nbrowser_events to learn if the USER closed your windows/tabs (window_closed {byUser:true}), or if a foregrounding attempt was auto-reverted (focus_violation {verb, autoReverted:true}) - a focus_violation means you tried to shove a window in front of the user; stop foregrounding.',@@ -283,9 +283,9 @@ const HELP = { '⛔ BACKGROUND BY DEFAULT - NEVER foreground a window unless the user explicitly asked to watch. nbrowser_open_window opens UNFOCUSED, open_tab opens active:false, switch_window does NOT raise - keep it that way. To capture a chrome:// / edge:// page (e.g. chrome://extensions for a guide), DO NOT launch the browser yourself (msedge/chrome `--new-window` via launch/launch_process RAISES the new window to the foreground). Instead: nbrowser_open_window {url:"about:blank"} (background) → desktop_navigate that window\'s hwnd to the chrome:// URL (background, no foreground) → desktop_screenshot_window (WGC captures a background/occluded window) → nbrowser_close_window. Same for recording (WGC) and screenshots (nbrowser_screenshot CDP) - both capture occluded windows, so foregrounding is never needed. Foregrounding is the USER\'s choice; do it only on explicit request (desktop_bring_to_front).', 'Before driving, nbrowser_status to confirm connected. After a dev_reload, expect a ~3-6s reconnect.', '⭐ BACKGROUND IS THE WHOLE POINT - NEVER foreground the driven window to screenshot OR record. The user must keep working while a demo records behind their other windows; yanking Chrome forward makes the feature useless. nbrowser_screenshot (CDP) captures a background/occluded window unconditionally. WGC recording captures a background/occluded window TOO - but only when the recorded Chrome was launched with the occlusion-disable flags (see Recording); without them an occluded window paints blank. Foregrounding is the USER\'s choice, made by them when they want to watch - it is never something the AI should do on its own.',- 'For demos: nbrowser_caption (in-page subtitle/alert) OR AD desktop_caption (fullscreen; use position:bottom - top covers the URL bar + Chrome warning).',+ 'For demos: nbrowser_caption (in-page subtitle/alert) OR ab desktop_caption (fullscreen; use position:bottom - top covers the URL bar + Chrome warning).', 'Mouse verbs: pass cursor:true to show the gliding Adom cursor, and intent:"Clicking the Pay button" so the cursor LABEL narrates the action (builds user trust).',- '⭐ Recording a browser window: the BEST tool is the CORE adom-desktop verb desktop_record_window_start (WGC) - or nbrowser_record_start {method:"wgc"}, which delegates to it. Background, up to 60fps, NO source picker, NO banner, captures OCCLUDED windows, never steals focus. This bridge\'s own getDisplayMedia/tabcapture recording is only a LAST-RESORT FALLBACK and is REFUSED unless you pass fallbackConfirmed:true, because a browser extension CANNOT silently screen-record (Chrome forces a manual share-source picker a human must click + a persistent banner, and won\'t reliably capture a background window). Prefer WGC every time; the extension is the fallback. ⚠️ For WGC background capture of an OCCLUDED Chrome window, Chrome must be launched with `--disable-backgrounding-occluded-windows --disable-renderer-backgrounding --disable-background-timer-throttling --disable-features=CalculateNativeWinOcclusion` (pup\'s Chrome ships these; a default user Chrome does NOT - without them Chrome stops painting the occluded window and WGC records a blank surface). Stop with nbrowser_record_stop {method:"wgc",recordingId}.',+ '⭐ Recording a browser window: the BEST tool is the CORE Bridge verb desktop_record_window_start (WGC) - or nbrowser_record_start {method:"wgc"}, which delegates to it. Background, up to 60fps, NO source picker, NO banner, captures OCCLUDED windows, never steals focus. This bridge\'s own getDisplayMedia/tabcapture recording is only a LAST-RESORT FALLBACK and is REFUSED unless you pass fallbackConfirmed:true, because a browser extension CANNOT silently screen-record (Chrome forces a manual share-source picker a human must click + a persistent banner, and won\'t reliably capture a background window). Prefer WGC every time; the extension is the fallback. ⚠️ For WGC background capture of an OCCLUDED Chrome window, Chrome must be launched with `--disable-backgrounding-occluded-windows --disable-renderer-backgrounding --disable-background-timer-throttling --disable-features=CalculateNativeWinOcclusion` (pup\'s Chrome ships these; a default user Chrome does NOT - without them Chrome stops painting the occluded window and WGC records a blank surface). Stop with nbrowser_record_stop {method:"wgc",recordingId}.', ], verbs: { 'open_window/open_tab/close_window/close_tab/switch_window/switch_tab/list_windows/list_tabs/rescan': 'Sessions = a real Chrome window (durable across reloads). tabId = real chrome tab id. close what you open.',@@ -300,16 +300,16 @@ const HELP = { }, }; -// ---- WGC recording: bridge -> AD (standalone, NO HD dependency) ------------------------------------// WGC per-window capture can't be done in a browser, so it lives in AD (which is always present with-// this bridge). We delegate to AD's direct API. AD writes the mp4 to a WINDOWS path (so pull_file-// grabs it directly - no WSL distro). NEVER call HD here: AD must run standalone without HD.-// AD 1.8.149 control-API discovery contract: env var first (AD injects it into every spawned bridge --// no file race, no stale port), then the host:port discovery file, then AD's fixed 47200..47209 range.-// ⚠️ The spawn-time env values FREEZE the port AD had when it spawned us. AD SELF-UPDATES rotate the+// ---- WGC recording: bridge -> ab (standalone, NO HD dependency) -----------------------------------+// WGC per-window capture can't be done in a browser, so it lives in ab (which is always present with+// this bridge). We delegate to ab's direct API. ab writes the mp4 to a WINDOWS path (so pull_file+// grabs it directly - no WSL distro). NEVER call HD here: ab must run standalone without HD.+// ab 1.8.149 control-API discovery contract: env var first (ab injects it into every spawned bridge -+// no file race, no stale port), then the host:port discovery file, then ab's fixed 47200..47209 range.+// ⚠️ The spawn-time env values FREEZE the port ab had when it spawned us. ab SELF-UPDATES rotate the // control port while we keep running (real incident, 2026-07-18: a Jul-16 bridge process hammered a dead // port for hours - hwnd capture, badge painting, and repair ALL failed SILENTLY). So: env/override is-// only the first try; on any connection failure adCommand re-discovers from the FILE (which AD keeps+// only the first try; on any connection failure adCommand re-discovers from the FILE (which ab keeps // current), retries once, caches the working base, and LOGS the outage + recovery. let adBaseOverride = null; // set when file-rediscovery finds a LIVE base that differs from the env one let adDownSince = 0;@@ -325,17 +325,17 @@ function discoverAdBase(fileOnly) { } catch {} return 'http://127.0.0.1:47200'; }-let adTokenRejected = false; // AD rotates the bridge token on self-update; a stale one 403s every call+let adTokenRejected = false; // ab rotates the bridge token on self-update; a stale one 403s every call // ── CALLER PROVENANCE (wiki #345/#351) ─────────────────────────────────────────────────────────────-// AD hands us WHO asked (X-Adom-Caller-*). We (a) log it, (b) FORWARD it on every AD callback we make-// while acting on that thread's behalf, adding X-Adom-Caller-Delegate so AD can show the user both+// ab hands us WHO asked (X-Adom-Caller-*). We (a) log it, (b) FORWARD it on every ab callback we make+// while acting on that thread's behalf, adding X-Adom-Caller-Delegate so ab can show the user both // facts: "chip-fetcher tab 3 (via native-browser)". Self-asserted: never used for authorization. const { AsyncLocalStorage } = require('async_hooks'); const callerCtx = new AsyncLocalStorage(); function withCaller(caller, fn) { return callerCtx.run(caller && (caller.thread || caller.container) ? caller : null, fn); } function currentCaller() { return callerCtx.getStore() || null; } function callerLabel() { const c = currentCaller(); return c && c.thread ? c.thread : null; }-// Headers for an AD call. Acting FOR a thread -> echo its identity + name ourselves as the delegate.+// Headers for a Bridge call. Acting FOR a thread -> echo its identity + name ourselves as the delegate. // Acting on our OWN behalf (idle timers, startup sweeps, reapers) -> no stale identity is forwarded; // callers of adCommand pass selfCaller:true and we send our own name instead. function callerHeaders(self) {@@ -352,79 +352,79 @@ function adRequest(base, payload, noToken, self) { return new Promise((resolve) => { let u; try { u = new URL('/command', base); } catch { u = new URL('http://127.0.0.1:47200/command'); } const data = Buffer.from(JSON.stringify(payload));- // X-Adom-Bridge-Token is ATTRIBUTION only (AD 1.9.84) - AD injects ADOM_BRIDGE_TOKEN into our spawn+ // X-Adom-Bridge-Token is ATTRIBUTION only (ab 1.9.84) - ab injects ADOM_BRIDGE_TOKEN into our spawn // env; sending it badges our outbound calls as "bridge" in the Activity Log. It is NOT an approval- // gate (we're trusted-by-install; AD binds 127.0.0.1 only). Omitting it is fine; a stale token 403s.+ // gate (we're trusted-by-install; ab binds 127.0.0.1 only). Omitting it is fine; a stale token 403s. const headers = { 'content-type': 'application/json', 'content-length': data.length, ...callerHeaders(self) }; if (process.env.ADOM_BRIDGE_TOKEN && !noToken && !adTokenRejected) headers['x-adom-bridge-token'] = process.env.ADOM_BRIDGE_TOKEN; const req = http.request({ host: u.hostname, port: u.port || 80, path: u.pathname, method: 'POST', headers, timeout: 20000 }, (res) => { let b = ''; res.on('data', (c) => (b += c)); res.on('end', () => { let j; try { j = JSON.parse(b); } catch { j = b; } resolve({ status: res.statusCode, body: j }); }); }); req.on('error', (e) => resolve({ status: 0, body: { error: String((e && e.message) || e) } }));- req.on('timeout', () => { req.destroy(); resolve({ status: 0, body: { error: 'AD direct-api timed out' } }); });+ req.on('timeout', () => { req.destroy(); resolve({ status: 0, body: { error: 'ab direct-api timed out' } }); }); req.write(data); req.end(); }); } function adCommand(app, command, args, target, self) {- // app is OPTIONAL on AD 1.9.84+ (inferred) - omit it for top-level verbs like notify_user, keep it+ // app is OPTIONAL on ab 1.9.84+ (inferred) - omit it for top-level verbs like notify_user, keep it // when a caller passes one (e.g. desktop_* recording delegation). const payload = { command, args: args || {} }; if (app) payload.app = app;- // CROSS-AD routing: `target` (a peer clientName from `targets`, or "all") routes this call to a PEER- // AD via the relay - so a bridge running on a VM reaches the user on their LAPTOP. Omitted = local AD.+ // CROSS-ab routing: `target` (a peer clientName from `targets`, or "all") routes this call to a PEER+ // ab via the relay - so a bridge running on a VM reaches the user on their LAPTOP. Omitted = local ab. if (target) payload.target = target; const first = discoverAdBase(); const ok2xx = (r) => r && r.status >= 200 && r.status < 300; return adRequest(first, payload, false, self).then(async (r) => {- // A STALE BRIDGE TOKEN 403s EVERY CALL (AD rotates it when it self-updates). The token is- // ATTRIBUTION-ONLY, so the correct recovery is to DROP it and retry - otherwise every AD-backed+ // A STALE BRIDGE TOKEN 403s EVERY CALL (ab rotates it when it self-updates). The token is+ // ATTRIBUTION-ONLY, so the correct recovery is to DROP it and retry - otherwise every ab-backed // feature (window correlation, taskbar badge, flash) dies SILENTLY. Real incident 2026-07-21: the- // fusion-bridge maintainer saw nbrowser_os_windows report 0 windows while AD saw 21.+ // fusion-bridge maintainer saw nbrowser_os_windows report 0 windows while ab saw 21. if (r.status === 401 || r.status === 403) { const retry = await adRequest(first, payload, true, self); if (ok2xx(retry)) {- if (!adTokenRejected) { adTokenRejected = true; console.error('[native-browser] AD rejected our bridge token (403) - it is attribution-only, so dropping it and continuing. AD likely self-updated and rotated it.'); logActivity({ verb: '_ad_link', ok: true, errorCode: 'token_dropped', internal: true }); }+ if (!adTokenRejected) { adTokenRejected = true; console.error('[native-browser] ab rejected our bridge token (403) - it is attribution-only, so dropping it and continuing. ab likely self-updated and rotated it.'); logActivity({ verb: '_ab_link', ok: true, errorCode: 'token_dropped', internal: true }); } adDownSince = 0; return retry; } r = retry; } if (ok2xx(r)) {- if (adDownSince) { console.error(`[native-browser] AD link RECOVERED after ${Math.round((Date.now() - adDownSince) / 1000)}s`); logActivity({ verb: '_ad_link', ok: true, internal: true }); adDownSince = 0; }+ if (adDownSince) { console.error(`[native-browser] ab link RECOVERED after ${Math.round((Date.now() - adDownSince) / 1000)}s`); logActivity({ verb: '_ab_link', ok: true, internal: true }); adDownSince = 0; } return r; } // Any other non-2xx is a REAL failure - never treat it as success (that is what silently produced // empty window lists). Log it so a missing badge/flash is diagnosable from nbrowser_activity. if (r.status !== 0) {- if (!adDownSince) { adDownSince = Date.now(); console.error(`[native-browser] AD returned HTTP ${r.status} for ${command} - AD-backed features (badge/flash/window correlation) are degraded`); logActivity({ verb: '_ad_link', ok: false, errorCode: `http_${r.status}`, internal: true }); }+ if (!adDownSince) { adDownSince = Date.now(); console.error(`[native-browser] ab returned HTTP ${r.status} for ${command} - ab-backed features (badge/flash/window correlation) are degraded`); logActivity({ verb: '_ab_link', ok: false, errorCode: `http_${r.status}`, internal: true }); } return r; }- // Connection-level failure -> AD may have restarted on a new port. Re-discover from the FILE and retry.+ // Connection-level failure -> ab may have restarted on a new port. Re-discover from the FILE and retry. const fromFile = discoverAdBase(true); const retry = fromFile && fromFile !== first ? adRequest(fromFile, payload, false, self) : Promise.resolve(r); return retry.then((r2) => { if (ok2xx(r2)) { adBaseOverride = fromFile;- console.error(`[native-browser] AD port rotated (${first} dead) - rediscovered ${fromFile} from file, link restored`);- logActivity({ verb: '_ad_link', ok: true, internal: true });+ console.error(`[native-browser] ab port rotated (${first} dead) - rediscovered ${fromFile} from file, link restored`);+ logActivity({ verb: '_ab_link', ok: true, internal: true }); adDownSince = 0; return r2; } if (!adDownSince) { adDownSince = Date.now();- console.error(`[native-browser] AD UNREACHABLE at ${first}${fromFile !== first ? ` and ${fromFile}` : ''} (${(r2.body && r2.body.error) || 'connect failed'}) - taskbar badge/flash + window correlation degraded until the link recovers`);- logActivity({ verb: '_ad_link', ok: false, errorCode: 'ad_unreachable', internal: true });+ console.error(`[native-browser] ab UNREACHABLE at ${first}${fromFile !== first ? ` and ${fromFile}` : ''} (${(r2.body && r2.body.error) || 'connect failed'}) - taskbar badge/flash + window correlation degraded until the link recovers`);+ logActivity({ verb: '_ab_link', ok: false, errorCode: 'ad_unreachable', internal: true }); } return r2; }); }); } // ⭐ The hint we hand back whenever an AI tries to record WITHOUT choosing WGC. The AI is not assumed-// to know the core adom-desktop window recorder exists, so we spell it out and explain - in detail -+// to know the core Bridge window recorder exists, so we spell it out and explain - in detail - // why the extension's own screen-capture is a heinous last resort. (Lesson from a real thread that // blundered into the getDisplayMedia picker before discovering desktop_record_window_start on its own.) const RECORD_WGC_HINT =- '⭐ BEST PATH - record the window with WGC, NOT the extension. Use the CORE adom-desktop verb ' ++ '⭐ BEST PATH - record the window with WGC, NOT the extension. Use the CORE Bridge verb ' + 'desktop_record_window_start {"titleContains":"<window title>"} (or {"hwnd":<n>}) - or call THIS verb ' + 'with method:"wgc", which delegates straight to it. WGC (Windows Graphics Capture) records ONE specific ' + 'window in the BACKGROUND at HIGH frame rate (up to 60fps), with NO source picker, NO "sharing" banner, ' +@@ -439,8 +439,8 @@ const RECORD_WGC_HINT = 'occluded window. I am the fallback, not the default. If WGC truly will not work, re-call with ' + 'method:"getdisplaymedia" AND fallbackConfirmed:true to acknowledge the picker + banner tradeoff.'; -// AD's native per-window recorder shipped in adom-desktop 1.8.148 (standalone Rust WGC + Media-// Foundation - no picker, no banner, background-capturable, real .mp4 under %TEMP%\adom-desktop-+// ab's native per-window recorder shipped in Bridge (standalone Rust WGC + Media+// Foundation - no picker, no banner, background-capturable, real .mp4 under %TEMP%\adom-bridge- // recordings\, pullable with pull_file). We delegate method:"wgc" straight to it. async function wgcRecord(verb, args) { const map = {@@ -450,10 +450,10 @@ async function wgcRecord(verb, args) { nbrowser_record_list: 'desktop_record_window_status', }; const adArgs = { ...args };- delete adArgs.method; // 'wgc' is the bridge-side selector, not an AD arg+ delete adArgs.method; // 'wgc' is the bridge-side selector, not a Bridge arg const r = await adCommand('desktop', map[verb], adArgs); let out = r.body;- // AD wraps results differently across surfaces - unwrap {output:"<json>"} and/or {data:{...}} so+ // ab wraps results differently across surfaces - unwrap {output:"<json>"} and/or {data:{...}} so // recordingId/path/fileExists are reachable no matter which shape the direct API returns. if (out && typeof out.output === 'string') { try { out = JSON.parse(out.output); } catch {} } if (out && out.data && typeof out.data === 'object') out = out.data;@@ -461,12 +461,12 @@ async function wgcRecord(verb, args) { if (unknown) { return { success: false,- error: 'AD is too old for native window recording',- _hint: 'desktop_record_window_* shipped in adom-desktop 1.8.148 - upgrade AD (adom-desktop --version must be >= 1.8.148; older builds mis-route to the puppeteer bridge). Meanwhile use method:"getdisplaymedia" or the "Record this tab" toolbar button.',+ error: 'ab is too old for native window recording',+ _hint: 'desktop_record_window_* shipped in Bridge - upgrade ab (adom-bridge-cli --version must be >= 1.8.148; older builds mis-route to the puppeteer bridge). Meanwhile use method:"getdisplaymedia" or the "Record this tab" toolbar button.', }; } if (r.status >= 200 && r.status < 300 && out && out.success !== false && !out.error) {- const stopHint = 'mp4 is on the laptop at out.path (under %TEMP%\\adom-desktop-recordings) - fetch it with: adom-desktop pull_file {"filePaths":["<path>"]}';+ const stopHint = 'mp4 is on the laptop at out.path (under %TEMP%\\adom-bridge-recordings) - fetch it with: adom-bridge-cli pull_file {"filePaths":["<path>"]}'; const startHint = 'recording started in the BACKGROUND - the user keeps working; do NOT foreground the window (foregrounding is the user\'s choice, never the AI\'s). ⚠️ If frames come out BLANK, the recorded Chrome was NOT launched with the occlusion-disable flags (--disable-backgrounding-occluded-windows --disable-renderer-backgrounding --disable-background-timer-throttling --disable-features=CalculateNativeWinOcclusion) - Chrome stops painting an occluded window without them. pup\'s Chrome already has these (so it background-records fine); a default user Chrome must be relaunched with them. Poll nbrowser_record_status {method:"wgc",recordingId}; stop with nbrowser_record_stop {method:"wgc",recordingId}.'; return { success: true, output: JSON.stringify({ ok: true, method: 'wgc',@@ -475,7 +475,7 @@ async function wgcRecord(verb, args) { _hint: verb === 'nbrowser_record_stop' ? stopHint : startHint, }) }; }- // a real AD-side failure (window not found, capture error, …) - surface it, don't pretend it's missing+ // a real ab-side failure (window not found, capture error, …) - surface it, don't pretend it's missing return { success: false, error: (out && out.error) || `WGC ${verb} failed`,@@ -484,17 +484,17 @@ async function wgcRecord(verb, args) { }; } -// ---- taskbar status (flash now; Adom-overlay + progress when AD ships desktop_taskbar) ----------+// ---- taskbar status (flash now; Adom-overlay + progress when ab ships desktop_taskbar) ---------- // We paint a "come look" cue on the REAL browser window's Windows taskbar button. The hwnd is captured // at control-start (the just-opened window is foreground) and cached, so flashes never hit the wrong-// window. Flash works today via run_script FlashWindowEx; overlay/progress need AD's desktop_taskbar+// window. Flash works today via run_script FlashWindowEx; overlay/progress need ab's desktop_taskbar // (ITaskbarList3 - can't be driven cleanly from PowerShell). See TASKBAR-FEATURE.md. let lastHwnd = 0; // most-recently driven window const sessionHwnd = new Map(); // sessionId -> hwnd (opened windows) const spawnHwnd = new Map(); // chrome windowId -> hwnd (adopted SPAWNED windows: compose/popups) const sessionProfile = new Map(); // sessionId -> profile KEY (pin: verbs on this session route here by default) const sessionThread = new Map(); // sessionId -> thread label (from registration) — for the activity log-const sessionCaller = new Map(); // sessionId -> AD-stamped caller thread (wiki #345: who ACTUALLY owns this session)+const sessionCaller = new Map(); // sessionId -> ab-stamped caller thread (wiki #345: who ACTUALLY owns this session) const sessionPurpose = new Map(); // sessionId -> purpose (from registration) // ── ACTIVITY LOG — a bounded, queryable "who drove what, when" trail so we can answer questions like@@ -556,14 +556,14 @@ const COMELOOK = new Set(['nbrowser_navigate', 'nbrowser_screenshot', 'nbrowser_ const ACTIVITY = new Set([...COMELOOK, 'nbrowser_reload', 'nbrowser_click', 'nbrowser_double_click', 'nbrowser_right_click', 'nbrowser_type', 'nbrowser_press_key', 'nbrowser_input_dispatch', 'nbrowser_hover', 'nbrowser_eval', 'nbrowser_evaluate', 'nbrowser_open_tab', 'nbrowser_back', 'nbrowser_forward', 'nbrowser_select', 'nbrowser_wait_for', 'nbrowser_switch_tab', 'nbrowser_switch_window', 'nbrowser_close_tab', 'nbrowser_set_date', 'nbrowser_download_wait']); const b64 = (s) => Buffer.from(s, 'utf8').toString('base64'); -// All taskbar status goes through AD's desktop_taskbar (1.8.152): flash + progress + the Adom overlay+// All taskbar status goes through ab's desktop_taskbar (1.8.152): flash + progress + the Adom overlay // badge, painted on the real browser window by hwnd. (Replaces the old run_script FlashWindowEx hack.) function taskbar(hwnd, opts) { if (!hwnd) return Promise.resolve(); return adCommand('desktop', 'desktop_taskbar', { hwnd, ...opts }).then((r) => { // LOG every overlay/flash (John, 2026-07-21): "log when you place the adom icon and when you flash // the taskbar so we can debug you better." A silently-failing badge is a broken TRUST signal, so it- // must be visible in nbrowser_activity, with the HTTP status when AD refuses.+ // must be visible in nbrowser_activity, with the HTTP status when ab refuses. const ops = []; const o = opts && opts.overlay; if (o) ops.push(o.icon ? 'overlay:composite' : `overlay:${o.badge || '?'}`);@@ -586,9 +586,9 @@ function armBadgeIdle(sid) { const prev = badgeIdle.get(sid); if (prev) clearTimeout(prev); badgeIdle.set(sid, setTimeout(() => { const h = sessionHwnd.get(sid); if (h) nativeOverlay(h, sessionProfile.get(sid)); badgeOn.delete(sid); badgeIdle.delete(sid); }, BADGE_IDLE_MS)); }-// Map a driven profile → its Chrome/Edge profile DIRECTORY, so AD (>=1.9.110) can composite the Adom mark+// Map a driven profile → its Chrome/Edge profile DIRECTORY, so ab (>=1.9.110) can composite the Adom mark // onto that profile's AVATAR instead of evicting it (the taskbar overlay has one slot = the avatar's slot).-// AD can't derive this itself: Chrome/Edge run ONE browser process for all profiles, so hwnd→PID→cmdline+// ab can't derive this itself: Chrome/Edge run ONE browser process for all profiles, so hwnd→PID→cmdline // has no --profile-directory. We read the browser's Local State (profile.info_cache: dir → {user_name}). const ADOM_EXT_ID_PREFIX = 'nncjgceobm'; // our pinned (keyed) extension id - stable across packed/unpacked const _avatarCache = new Map(); // profileKey -> {userDataDir, profileDir} | null@@ -633,8 +633,8 @@ function avatarForProfile(profileKey) { return result; } const BADGE_ENABLED = true;-// The BRIDGE is the browser expert, so WE build the FINAL taskbar overlay icon ourselves and hand AD a-// ready PNG via overlay.icon (AD stays generic - no browser/avatar/folder knowledge). The icon is the+// The BRIDGE is the browser expert, so WE build the FINAL taskbar overlay icon ourselves and hand ab a+// ready PNG via overlay.icon (ab stays generic - no browser/avatar/folder knowledge). The icon is the // profile's own avatar CIRCLE-cropped (the way Chrome/Edge draw it) with the Adom favicon sitting flush // in the lower-right corner. Compositing runs natively on Windows via PowerShell + System.Drawing (GDI+): // zero npm deps (the bridge has none), antialiased circle-crop + HighQualityBicubic resize, reads any PNG.@@ -686,8 +686,8 @@ const _iconCache = new Map(); // profileKey -> { mtime, badge: path|null, plain: const _sanitizeKey = (k) => String(k).replace(/[^a-z0-9]+/gi, '_').slice(0, 60); // Build (or return cached) the overlay PNG for a profile. withBadge=true → avatar + Adom favicon; false → // plain circle avatar (the native look). Cache keyed on the avatar file's mtime, so a changed profile pic-// regenerates. Resolves to a file PATH (AD reads it - same machine) or null (no avatar photo for this-// profile → caller falls back to AD's built-in plain badge / clears).+// regenerates. Resolves to a file PATH (ab reads it - same machine) or null (no avatar photo for this+// profile → caller falls back to ab's built-in plain badge / clears). function buildOverlayIcon(profileKey, withBadge, style) { return new Promise((resolve) => { try {@@ -818,7 +818,7 @@ function restoreSessions(liveHwnds) { } catch {} } // ── BRIDGE-ORIGINATED EXTENSION QUERY — lets the bridge ask a profile's extension something on its own-// behalf (same pending/frame plumbing the AD relay uses). Used to re-correlate a profile's windows when a+// behalf (same pending/frame plumbing the ab relay uses). Used to re-correlate a profile's windows when a // thread drives PROFILE-KEYED (no sessionId) and we have no hwnd for it. function extQuery(profileKey, verb, args, timeoutMs) { return new Promise((resolve) => {@@ -850,7 +850,7 @@ function touchProfileBadge(profileKey) { if ((_profCorrAt.get(profileKey) || 0) + 30000 > now) return; // throttle: at most one repair query / 30s / profile _profCorrAt.set(profileKey, now); extQuery(profileKey, 'nbrowser_list_windows', {}).then((r) => {- // pending resolves with the AD-facing wrapper {success, output:"<json>"} - unwrap to the raw payload+ // pending resolves with the ab-facing wrapper {success, output:"<json>"} - unwrap to the raw payload let payload = r; if (r && typeof r.output === 'string') { try { payload = JSON.parse(r.output); } catch { payload = null; } } if (payload) badgeSpawnsIn(payload, profileKey).catch(() => {});@@ -980,8 +980,8 @@ function flashHint() { // have no Chrome, we get them one. Mirrors pup's proven flow: download Google's OFFICIAL offline // installer, run a QUIET user-scoped install (.exe --silent --install, no admin on a normal PC → %LOCALAPPDATA%\ // Google\Chrome), and on a locked-down box (VM/managed) ESCALATE to a notify-FIRST UAC (fire a sticky-// "Approve now" toast via the AD direct-API, wait for the tap, THEN raise the prompt so the secure-// desktop doesn't hide the toast; re-arm if it expires; ping peer ADs if the user is on another+// "Approve now" toast via the ab direct-API, wait for the tap, THEN raise the prompt so the secure+// desktop doesn't hide the toast; re-arm if it expires; ping peer abs if the user is on another // machine). Non-blocking: nbrowser_install_chrome returns immediately; poll nbrowser_chrome_status. // ============================================================================================ const CHROME_STABLE_URL = 'https://dl.google.com/chrome/install/standalonesetup64.exe';@@ -1027,9 +1027,9 @@ function _downloadChrome(url, destPath, redirects = 0) { }); } -// notify helpers via the AD direct-API (app-less on AD 1.9.84). Best-effort: null on any failure.+// notify helpers via the ab direct-API (app-less on ab 1.9.84). Best-effort: null on any failure. // WHERE the toast lands: a bridge on a VM must reach the user on their LAPTOP, not fire on the VM's own-// (unwatched) desktop. So if the relay shows any PEER desktop, we notify `target:"all"` (the SDK cross-AD+// (unwatched) desktop. So if the relay shows any PEER desktop, we notify `target:"all"` (the SDK cross-ab // pattern) - the user sees it wherever they're attended. No peers (we're on the user's own machine) = // local toast. Cached ~30s so we don't hit `targets` on every poll. (Users keep an RDP session open to // their VMs, so a laptop toast that says "approve the UAC on <host>" is actionable immediately.)@@ -1052,7 +1052,7 @@ const _notify = async (args) => adCommand(null, 'notify_user', args, await _noti const _notifyResp = async (id) => adCommand(null, 'notify_response', { id }, await _notifyTarget()); async function _adCanNotify() { const r = await adCommand(null, 'notify_user', { id: '_probe_', title: '', body: '', level: 'info', _probe: true }).catch(() => null);- // if AD accepted (any 2xx-ish body) treat notify as available; unknown-verb → unavailable+ // if ab accepted (any 2xx-ish body) treat notify as available; unknown-verb → unavailable const body = r && r.body; if (!r || r.status === 0) return false; const err = body && (body.error || body.errorCode);@@ -1077,9 +1077,9 @@ function _raiseElevatedMsi(msiPath) { }); } -// Locked-down box path: quiet install didn't land → drive the user to the UAC. Notify-FIRST if AD can+// Locked-down box path: quiet install didn't land → drive the user to the UAC. Notify-FIRST if ab can // notify (fire toast, WAIT for tap, THEN raise UAC so the secure desktop doesn't hide it; re-arm on-// expiry). If AD can't notify, raise ONCE and surface awaiting_uac for the caller to relay (no spam).+// expiry). If ab can't notify, raise ONCE and surface awaiting_uac for the caller to relay (no spam). async function _elevatedChromeWithNotify(msiPath) { const id = 'nbrowser-chrome-install'; chromeState.phase = 'awaiting_uac'; chromeState.awaitingUacSince = Date.now();@@ -1179,7 +1179,7 @@ function installNativeChrome(opts = {}) { chromeState.phase = 'failed'; if (elevate === false) { chromeState.errorCode = 'chrome_not_detected'; chromeState.error = 'Ran the Chrome installer but chrome.exe did not appear (no elevation, elevate:false).'; } else if (chromeState.notifyChannel) { chromeState.errorCode = 'chrome_needs_approval'; chromeState.error = 'Chrome needs a Windows approval (UAC) that was not accepted in time. A sticky "Approve now" toast is on the desktop; tapping it re-opens the prompt.'; }- else { chromeState.errorCode = 'chrome_needs_approval_uncced'; chromeState.error = 'Chrome needs a Windows approval (UAC), but this AD cannot pop a notification. RELAY TO THE USER: go to the machine and click YES on the Google Chrome installer prompt, then re-call nbrowser_install_chrome.'; }+ else { chromeState.errorCode = 'chrome_needs_approval_uncced'; chromeState.error = 'Chrome needs a Windows approval (UAC), but this ab cannot pop a notification. RELAY TO THE USER: go to the machine and click YES on the Google Chrome installer prompt, then re-call nbrowser_install_chrome.'; } return { ok: false, errorCode: chromeState.errorCode, error: chromeState.error }; } chromeState.phase = 'ready'; chromeState.finishedAt = Date.now();@@ -1232,13 +1232,14 @@ function _hostManifestPaths() { const home = process.env.USERPROFILE || os.homedir(); return [ path.join(home, '.adom', 'native-browser', 'host-manifest.json'),- path.join(home, 'adom-browser-extension', 'host', 'host-manifest.json'),+ path.join(home, 'adom-native-browser', 'host', 'host-manifest.json'),+ path.join(home, 'adom-browser-extension', 'host', 'host-manifest.json'), // pre-rename clone dir ]; } // ============================================================================================ // VERB_META — ONE source of truth for per-verb hint/related/pitfalls. Feeds BOTH (a) nbrowser_describe-// (so AD's Verbs tab shows guidance + chaining + traps) AND (b) the rich `_hint` attached to EVERY+// (so ab's Verbs tab shows guidance + chaining + traps) AND (b) the rich `_hint` attached to EVERY // verb response (decorateHint). The whole point: a calling AI should never have to guess or read docs, // and should be STEERED away from the dangerous moves these powerful tools allow. ASCII only (no em-dashes). // ⛔ SAFETY THREAD: this bridge drives the user's REAL, signed-in browser. The cardinal rule, repeated on@@ -1247,10 +1248,10 @@ function _hostManifestPaths() { const OWN_ONLY = 'ONLY operate on a window/tab YOU opened via nbrowser_open_window. NEVER navigate/reload/close a window or tab the user already had open - the bridge drives their REAL browser, so you would destroy their live page, forms, sessions, or other AI threads.'; // The politeness mandate, repeated verbatim on every focus-relevant verb. The bridge ENFORCES it in code // (background-by-default + focus-steal auto-revert + focus_violation events); this hint tells the AI why.-const NO_FG = 'Open in the BACKGROUND — users automating the browser almost always have OTHER things in the foreground, and a raised window STEALS their keyboard focus (it has typed a user\'s own keystrokes into a username field and dismissed Chrome\'s password dropdown mid-task). Background is where automation SUCCEEDS. NEVER fall back to OS-level launches (Start-Process/launch/msedge/chrome --new-window) or bring_to_front/maximize to surface a window. To genuinely foreground (some auth pages / browser features need it) you must do real work: (1) FIRST warn the user with an AD notify (adom-desktop notify_user {message:"Adom is about to bring a browser window forward for <reason>"}), then (2) pass userRequestedForeground:true AND foregroundReason:"<why>". A bare userRequestedForeground with no foregroundReason STAYS background; any driven window that lands topmost is auto-reverted + logged (focus_violation / foreground_raised).';+const NO_FG = 'Open in the BACKGROUND — users automating the browser almost always have OTHER things in the foreground, and a raised window STEALS their keyboard focus (it has typed a user\'s own keystrokes into a username field and dismissed Chrome\'s password dropdown mid-task). Background is where automation SUCCEEDS. NEVER fall back to OS-level launches (Start-Process/launch/msedge/chrome --new-window) or bring_to_front/maximize to surface a window. To genuinely foreground (some auth pages / browser features need it) you must do real work: (1) FIRST warn the user with a Bridge notify (adom-bridge-cli notify_user {message:"Adom is about to bring a browser window forward for <reason>"}), then (2) pass userRequestedForeground:true AND foregroundReason:"<why>". A bare userRequestedForeground with no foregroundReason STAYS background; any driven window that lands topmost is auto-reverted + logged (focus_violation / foreground_raised).'; const VERB_META = { nbrowser_status: { hint: 'Connectivity check: connected profiles + active browser. Call this first if any verb errors "not connected".', related: ['nbrowser_profiles', 'nbrowser_ping', 'nbrowser_open_window'], pitfalls: ['a profile\'s extension SLEEPS when that profile has no open windows - it then shows disconnected; open a window to wake it'] },- nbrowser_readiness: { hint: 'READ-ONLY cold-start probe: is the native-browser extension connected + ready to drive? NEVER spawns work or installs anything. state:"ready" = drive now; state:"needs-extension" = offer to install it (Level-2, see installing-the-extension) or it is asleep (open a window in that profile). Also reports chromeInstalled: if false, the user has no native Chrome and you can offer nbrowser_install_chrome.', related: ['nbrowser_status', 'nbrowser_open_window', 'nbrowser_install_chrome'], pitfalls: ['"needs-extension" can mean not-installed OR installed-but-asleep - nbrowser_status\'s tooltip distinguishes', 'this verb never installs; to install, run the installing-the-extension flow', 'for a machine-wide "what do I have" answer prefer AD\'s bridge_readiness (aggregates all bridges, spawns none)'] },+ nbrowser_readiness: { hint: 'READ-ONLY cold-start probe: is the native-browser extension connected + ready to drive? NEVER spawns work or installs anything. state:"ready" = drive now; state:"needs-extension" = offer to install it (Level-2, see installing-the-extension) or it is asleep (open a window in that profile). Also reports chromeInstalled: if false, the user has no native Chrome and you can offer nbrowser_install_chrome.', related: ['nbrowser_status', 'nbrowser_open_window', 'nbrowser_install_chrome'], pitfalls: ['"needs-extension" can mean not-installed OR installed-but-asleep - nbrowser_status\'s tooltip distinguishes', 'this verb never installs; to install, run the installing-the-extension flow', 'for a machine-wide "what do I have" answer prefer ab\'s bridge_readiness (aggregates all bridges, spawns none)'] }, nbrowser_install_chrome: { hint: '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). NON-BLOCKING - returns immediately; poll nbrowser_chrome_status. On a locked-down box it escalates to a notify-first UAC (fires a sticky "Approve now" toast, waits for the tap, then raises the prompt). This is step 1 of getting the AI onto the user\'s REAL browser - after Chrome is in, help them sign into Google + load the extension.', related: ['nbrowser_chrome_status', 'nbrowser_readiness', 'installing-the-extension'], pitfalls: ['non-blocking: do NOT expect Chrome to exist on return - POLL nbrowser_chrome_status until phase:"ready"', 'awaiting_uac means a prompt is on the user\'s desktop - relay it; do not re-trigger install', 'Windows only for now', 'this only INSTALLS Chrome; the user still signs into Google themselves (their credential) - guide them via setting-up-native-chrome'] }, nbrowser_chrome_status: { hint: 'READ-ONLY: is native Google Chrome installed, and how is an in-flight install going? Returns {installed, executablePath, phase, installing, awaitingUac}. phase: idle|installing|awaiting_uac|ready|failed. Poll this after nbrowser_install_chrome. Never installs or spawns.', related: ['nbrowser_install_chrome', 'nbrowser_readiness'], pitfalls: ['awaiting_uac + notifyChannel:true = a toast is on the desktop, keep polling; notifyChannel:false = relay the UAC to the user yourself', 'installed:true means Chrome.exe is present - next help the user sign into Google + load the extension'] }, nbrowser_ping: { hint: 'Round-trip liveness through to the extension.', related: ['nbrowser_status'], pitfalls: ['a pong proves the extension is reachable, NOT that any profile has a drivable window — use nbrowser_status/readiness for that'] },@@ -1259,7 +1260,7 @@ const VERB_META = { nbrowser_close_window: { hint: 'Close a window you opened. Pass includeSpawned:true to also close everything that window\'s pages SPAWNED (composes/popups). Clean up your own sessions when done.', related: ['nbrowser_owned', 'nbrowser_cleanup', 'nbrowser_list_windows'], pitfalls: ['⛔ only close a sessionId YOU opened - closing the user\'s window can wipe their live work and other AI threads', 'if the session spawned windows (a page opened a compose/popup), they stay open unless you pass includeSpawned:true or call nbrowser_cleanup'] }, nbrowser_owned: { hint: 'List everything a session owns: opened windows + windows/tabs a page it drove SPAWNED (compose, target=_blank, OAuth popup). Calling it ACKNOWLEDGES the spawns (clears an unacknowledged_spawns block). Use it before ending a task to see what to clean up.', related: ['nbrowser_cleanup', 'nbrowser_close_window', 'nbrowser_list_windows'], next: 'nbrowser_cleanup', pitfalls: ['spawned windows belong to no session until adopted — this is how you find leaked ones (e.g. a Gmail Contacts compose)', 'after seeing spawns, KEEP the ones you need or nbrowser_cleanup the litter'] }, nbrowser_cleanup: { hint: 'Close ONLY what this session SPAWNED (leaked composes/popups) — never the user\'s windows, never your primary opened window. The fix for spawned-window leaks.', related: ['nbrowser_owned', 'nbrowser_close_window'], pitfalls: ['needs the sessionId whose spawns to close', 'your primary opened window is KEPT — close it separately with nbrowser_close_window {sessionId, includeSpawned:true}'] },- nbrowser_force_close: { hint: 'LAST-RESORT override to close a TRUE ORPHAN window (one ABE cannot attribute to any live session) or your own - INCLUDING litter another tool left behind (browser_open/pup windows from an HD test, an abandoned launch). TWO forms, SAME two-step gate: {windowId} when the extension can see the window; {hwnd} (OS-level, from desktop_list_windows) when it cannot - e.g. an Edge window in a profile with no extension. Call once → overrideTicket + exactly what will be destroyed; call again with {…, overrideTicket, attestOrphan:true, reason} to actually close. NEVER closes another live thread\'s window.', related: ['nbrowser_owned', 'nbrowser_cleanup', 'nbrowser_list_windows'], pitfalls: ['a single call NEVER closes anything — that is the point; you must attest on a second call with a real reason', 'refused (owned_by_other) if the window belongs to another live session — cross-thread ownership is absolute', 'prefer nbrowser_cleanup for tracked spawns; force_close is only for true orphans', 'the {hwnd} form only targets REAL browser windows (Chrome_WidgetWin) and sends a graceful WM_CLOSE - unsaved pages may prompt instead of closing'] },+ nbrowser_force_close: { hint: 'LAST-RESORT override to close a TRUE ORPHAN window (one nbe cannot attribute to any live session) or your own - INCLUDING litter another tool left behind (pup_open/pup windows from an HD test, an abandoned launch). TWO forms, SAME two-step gate: {windowId} when the extension can see the window; {hwnd} (OS-level, from desktop_list_windows) when it cannot - e.g. an Edge window in a profile with no extension. Call once → overrideTicket + exactly what will be destroyed; call again with {…, overrideTicket, attestOrphan:true, reason} to actually close. NEVER closes another live thread\'s window.', related: ['nbrowser_owned', 'nbrowser_cleanup', 'nbrowser_list_windows'], pitfalls: ['a single call NEVER closes anything — that is the point; you must attest on a second call with a real reason', 'refused (owned_by_other) if the window belongs to another live session — cross-thread ownership is absolute', 'prefer nbrowser_cleanup for tracked spawns; force_close is only for true orphans', 'the {hwnd} form only targets REAL browser windows (Chrome_WidgetWin) and sends a graceful WM_CLOSE - unsaved pages may prompt instead of closing'] }, nbrowser_switch_window: { hint: 'Target a window for later verbs. Does NOT raise it. ' + NO_FG, related: ['nbrowser_list_windows'], pitfalls: ['does not foreground (by design); only userRequestedForeground:true raises it, and only when the user explicitly asked to watch live'] }, nbrowser_list_windows: { hint: 'List live windows/sessions with bounds+state + an owner label: agent-opened | agent-spawned | user. Pass mine:true for only windows an agent opened/spawned.', related: ['nbrowser_owned', 'nbrowser_switch_window', 'nbrowser_rescan'], pitfalls: ['owner:"agent-spawned" = a page you drove opened it indirectly (compose/popup) — clean it up with nbrowser_cleanup', 'owner:"user" windows are the user\'s own — NEVER drive/close them'] }, nbrowser_window_state: { hint: 'Maximize/fullscreen/normal/minimize or move+resize. ' + NO_FG, related: ['nbrowser_fullscreen'], pitfalls: ['maximize/fullscreen are USER-only foregrounding actions: they enlarge the window but it STAYS in the background (auto-reverted) unless userRequestedForeground:true - so recordings still work but you can never shove it in front of the user', 'resizing a window the USER is in disrupts them; act on your own session windows'] },@@ -1269,8 +1270,8 @@ const VERB_META = { nbrowser_profiles: { hint: 'List every BROWSER x PROFILE you can drive + which is active. Each row carries browser (chrome|edge), email, displayName (the human label the user sees on the browser profile button, e.g. "John Personal" vs "adom.inc"), and profileDir. Pick work-vs-personal from the email DOMAIN + displayName, never from which window happens to be focused. Drive several in parallel with profile:"<key>".', related: ['nbrowser_use_profile', 'nbrowser_profile_block', 'nbrowser_resolve_profiles'], pitfalls: ['active is only the default when no profile is passed', 'an entry with unresolved:true has no email yet - nbrowser_resolve_profiles first, or you may drive the wrong account', 'the same email can live in Chrome AND Edge - match browser too', 'asleep:true = the profile exists (Local State) but has no open window so its extension is not running - it cannot be driven until the user opens a window in it; never claim a profile "does not exist" when it is merely asleep'] }, nbrowser_use_profile: { hint: 'Set the default profile that verbs target.', related: ['nbrowser_profiles', 'nbrowser_pin_session'], pitfalls: ['you can still override per-call with profile:"<email>"'] }, nbrowser_pin_session: { hint: 'Pin a sessionId to a profile so EVERY verb carrying that sessionId auto-routes to it (no cross-profile mistakes when driving several accounts). open_window pins automatically to the profile it opened in; use this to re-pin explicitly.', related: ['nbrowser_profiles', 'nbrowser_resolve_profiles', 'nbrowser_open_window'], pitfalls: ['pins are per-session; a verb with an explicit profile: still overrides the pin', 'if the pinned profile goes offline, verbs fall back to the active profile'] },- nbrowser_open_os_window: { hint: 'EXTENSION-FREE: open a URL in the user\'s REAL browser profile (Chrome or Edge) when ABE is not installed there - the graceful-degradation path for flows where the USER does the clicking, above all OAuth consent (adom-google, Autodesk APS: they are already signed in; they just click Approve). BACKGROUND by default: brief blip, then minimized + ORANGE flashing taskbar button = "come click when ready". Registration (sessionId+thread+purpose) required like every window.', related: ['nbrowser_profiles', 'nbrowser_os_windows', 'nbrowser_force_close', 'nbrowser_wake_profile'], pitfalls: ['you CANNOT read, drive, or screenshot this window - no extension in that profile; poll the outcome out-of-band (OAuth token poll) or ask the user', 'if the extension IS installed in the profile, prefer wake_profile + open_window (full control, guards, cleanup)', 'foreground:true needs foregroundReason (+ notify_user first) - background+flash is almost always right', 'the background MINIMIZE never touches the workspace/editor window or another thread\'s live session (they are excluded); a plain new window just adds to the running browser, it does not restart it', 'opens MINIMIZED + flashing by default; to raise it later use desktop_bring_to_front {hwnd} (returned in the result) - only when the user asks to see it, never for your own convenience', 'close it afterwards with nbrowser_force_close {hwnd} - never leave auth litter', 'every response nudges the ABE install - relay the upsell to the user at a natural moment, never mid-auth'] },- nbrowser_os_windows: { hint: 'OS-level browser-window inventory measured ON the laptop by the bridge: every Chrome/Edge/pup window (hwnd, title, rect, z) annotated with the ABE session that owns it. THE reliable probe when your own desktop_list_windows returns 0/null (wrong --target, AD hiccup) or when a profile has no extension (Edge) so list_windows cannot see it.', related: ['nbrowser_force_close', 'nbrowser_list_windows', 'nbrowser_profiles'], pitfalls: ['a window with abeSession set belongs to a LIVE thread - never force-close it', 'ok:false = the bridge\'s AD link is down, meaning "unknown", NOT "no windows"'] },+ nbrowser_open_os_window: { hint: 'EXTENSION-FREE: open a URL in the user\'s REAL browser profile (Chrome or Edge) when nbe is not installed there - the graceful-degradation path for flows where the USER does the clicking, above all OAuth consent (adom-google, Autodesk APS: they are already signed in; they just click Approve). BACKGROUND by default: brief blip, then minimized + ORANGE flashing taskbar button = "come click when ready". Registration (sessionId+thread+purpose) required like every window.', related: ['nbrowser_profiles', 'nbrowser_os_windows', 'nbrowser_force_close', 'nbrowser_wake_profile'], pitfalls: ['you CANNOT read, drive, or screenshot this window - no extension in that profile; poll the outcome out-of-band (OAuth token poll) or ask the user', 'if the extension IS installed in the profile, prefer wake_profile + open_window (full control, guards, cleanup)', 'foreground:true needs foregroundReason (+ notify_user first) - background+flash is almost always right', 'the background MINIMIZE never touches the workspace/editor window or another thread\'s live session (they are excluded); a plain new window just adds to the running browser, it does not restart it', 'opens MINIMIZED + flashing by default; to raise it later use desktop_bring_to_front {hwnd} (returned in the result) - only when the user asks to see it, never for your own convenience', 'close it afterwards with nbrowser_force_close {hwnd} - never leave auth litter', 'every response nudges the nbe install - relay the upsell to the user at a natural moment, never mid-auth'] },+ nbrowser_os_windows: { hint: 'OS-level browser-window inventory measured ON the laptop by the bridge: every Chrome/Edge/pup window (hwnd, title, rect, z) annotated with the nbe session that owns it. THE reliable probe when your own desktop_list_windows returns 0/null (wrong --target, ab hiccup) or when a profile has no extension (Edge) so list_windows cannot see it.', related: ['nbrowser_force_close', 'nbrowser_list_windows', 'nbrowser_profiles'], pitfalls: ['a window with nbeSession set belongs to a LIVE thread - never force-close it', 'ok:false = the bridge\'s ab link is down, meaning "unknown", NOT "no windows"'] }, nbrowser_wake_profile: { hint: 'Wake an ASLEEP profile (exists in nbrowser_profiles with asleep:true but not drivable) WITHOUT opening any window: launches the browser with --no-startup-window so the profile + its extension load in background mode - nothing foregrounds, no focus is stolen. When it returns woke:true, drive normally.', related: ['nbrowser_profiles', 'nbrowser_open_window', 'nbrowser_resolve_profiles'], pitfalls: ['NEVER launch a foreground browser window to wake a profile - this verb exists so waking honors background-by-default', 'woke:false usually means the Adom extension is not installed in THAT profile (extensions are per-profile)'] }, nbrowser_resolve_profiles: { hint: 'Force every connected extension to re-resolve its Google email and re-announce, so pending-N / empty-email keys eagerly become chrome:<email> / edge:<email> - call it up front so you never drive the wrong (unresolved) profile.', related: ['nbrowser_profiles', 'nbrowser_pin_session', 'nbrowser_whoami'], pitfalls: ['takes ~2.5s (waits for each re-announce)', 'a profile not signed into Google stays email-less - target it by id-key or browser:'] }, nbrowser_whoami: { hint: 'Ask ONE profile to (re)resolve + report its Google email (and re-announce it to the bridge). For all profiles at once use nbrowser_resolve_profiles.', related: ['nbrowser_resolve_profiles', 'nbrowser_profiles'], pitfalls: ['signedIn:false = this browser profile has no Google account, so no email'] },@@ -1289,8 +1290,8 @@ const VERB_META = { nbrowser_wait: { hint: 'Sleep server-side (let a page settle) before the next verb.', related: ['nbrowser_wait_for', 'nbrowser_screenshot'], pitfalls: ['prefer nbrowser_wait_for for a DETERMINISTIC wait (selector/url/text) instead of a fixed sleep'] }, nbrowser_wait_for: { hint: 'DETERMINISTIC wait after a click/nav: block until a CSS selector exists / the URL contains a substring / the visible text contains a substring, or timeoutMs. Use this instead of nbrowser_wait + fixed sleeps + coordinate races. Returns {matched, by, url, waitedMs}.', related: ['nbrowser_click', 'nbrowser_navigate', 'nbrowser_eval'], pitfalls: ['pass at least one of selector / urlIncludes / textIncludes', 'matched:false + timedOut:true means the condition never appeared - screenshot to see the real page', 'selector matches the top document only (not inside cross-origin iframes)'] }, nbrowser_keepalive: { hint: 'Keep a session ACTIVE to avoid vendor idle sign-outs (Gusto/Intuit ~10-15 min): replays a benign mousemove/focus on an interval (NO navigation, no side-effects). Per-session; auto-stops when the window closes. Call with {stop:true} to cancel. Set at the START of a long unattended automation.', related: ['nbrowser_open_window', 'nbrowser_login_state'], pitfalls: ['client-side activity only - a vendor that expires the SERVER session regardless may still sign out; combine with periodic real actions if so', 'default interval 4min (30s..15min); pick below the vendor idle timeout'] },- nbrowser_screenshot: { hint: 'CDP screenshot of the PAGE - captures a BACKGROUND/occluded window WITHOUT foregrounding it. Your DEFAULT way to "see" a page. It captures page CONTENT only - NOT the browser chrome (URL/tabs) and NOT native popups/dialogs (Chrome password dropdown, print dialog, alert/confirm) which live OUTSIDE the page.', related: ['nbrowser_eval', 'nbrowser_click', 'nbrowser_handle_dialog'], pitfalls: ['resized <=1568 for the model; pass toFile:true to save full-res to a laptop file (pull_file it)', 'to see the WINDOW FRAME or a native popup/dropdown/dialog, use AD desktop_screenshot_window {hwnd | titleContains} (WGC): it captures the window PLUS its OWNED CHILD windows (returns a parent/child hwnd array, so you see popups like the password dropdown), and it is background-safe (no foregrounding)', '⛔ NEVER take a fullscreen / whole-monitor screenshot to inspect a browser window - it grabs the user\'s other work, is imprecise, and often implies foregrounding. Page → nbrowser_screenshot; window/chrome/popup → desktop_screenshot_window'] },- nbrowser_screenshot_full_res: { hint: 'Full-resolution, full-PAGE screenshot via CDP (the entire scrollable page, not just the viewport). Still PAGE content only.', related: ['nbrowser_screenshot'], pitfalls: ['this is full-PAGE, NOT "fullscreen monitor" - it never captures the desktop or the user\'s other windows', 'large base64 can be slow over the relay - prefer toFile then pull_file', 'for the window frame or a native dialog/popup, use AD desktop_screenshot_window {hwnd | titleContains} (parent/child hwnds), never a monitor capture'] },+ nbrowser_screenshot: { hint: 'CDP screenshot of the PAGE - captures a BACKGROUND/occluded window WITHOUT foregrounding it. Your DEFAULT way to "see" a page. It captures page CONTENT only - NOT the browser chrome (URL/tabs) and NOT native popups/dialogs (Chrome password dropdown, print dialog, alert/confirm) which live OUTSIDE the page.', related: ['nbrowser_eval', 'nbrowser_click', 'nbrowser_handle_dialog'], pitfalls: ['resized <=1568 for the model; pass toFile:true to save full-res to a laptop file (pull_file it)', 'to see the WINDOW FRAME or a native popup/dropdown/dialog, use ab desktop_screenshot_window {hwnd | titleContains} (WGC): it captures the window PLUS its OWNED CHILD windows (returns a parent/child hwnd array, so you see popups like the password dropdown), and it is background-safe (no foregrounding)', '⛔ NEVER take a fullscreen / whole-monitor screenshot to inspect a browser window - it grabs the user\'s other work, is imprecise, and often implies foregrounding. Page → nbrowser_screenshot; window/chrome/popup → desktop_screenshot_window'] },+ nbrowser_screenshot_full_res: { hint: 'Full-resolution, full-PAGE screenshot via CDP (the entire scrollable page, not just the viewport). Still PAGE content only.', related: ['nbrowser_screenshot'], pitfalls: ['this is full-PAGE, NOT "fullscreen monitor" - it never captures the desktop or the user\'s other windows', 'large base64 can be slow over the relay - prefer toFile then pull_file', 'for the window frame or a native dialog/popup, use ab desktop_screenshot_window {hwnd | titleContains} (parent/child hwnds), never a monitor capture'] }, nbrowser_eval: { hint: 'Run JS in the page and get the result. Powerful - use read-only expressions to inspect state.', related: ['nbrowser_screenshot'], pitfalls: ['runs arbitrary JS on the user\'s LIVE session - never run destructive scripts'] }, nbrowser_evaluate: { hint: 'Alias of nbrowser_eval.', related: ['nbrowser_eval'], pitfalls: ['runs arbitrary JS on the user\'s LIVE session - never run destructive scripts'] }, nbrowser_input_dispatch: { hint: 'Low-level isTrusted CDP input (click/move/type/key).', related: ['nbrowser_click', 'nbrowser_type'], pitfalls: ['prefer nbrowser_click (smart selector) over raw xy'] },@@ -1302,7 +1303,7 @@ const VERB_META = { nbrowser_register_totp: { hint: 'One-time: register a base32 TOTP secret for a host (the "manual entry / setup key" the vendor shows by the 2FA QR). Stored 0600 on the laptop, NEVER returned. Then nbrowser_totp {host} types codes.', related: ['nbrowser_totp'], pitfalls: ['base32 only (A-Z, 2-7)', 'stored on the laptop; the agent never sees it back', 'one seed per host - re-registering replaces it'] }, nbrowser_add_extension_origin: { hint: 'Add an extension ID to the native-messaging host manifest\'s allowed_origins so a browser whose loaded-unpacked ID differs (esp. Edge) can reach the host + appear in nbrowser_profiles. Pass {id} from edge://extensions. RELOAD the extension afterward so the browser re-reads it.', related: ['nbrowser_profiles', 'nbrowser_rescan'], pitfalls: ['MUST reload the extension after (edge://extensions -> Reload) for the browser to re-read allowed_origins', 'our manifest pins a key so IDs SHOULD match across browsers - if Edge diverged, add its id here'] }, nbrowser_type: { hint: 'Type into the focused field (isTrusted).', related: ['nbrowser_click', 'nbrowser_press_key'], pitfalls: ['click the field FIRST so it has focus'] },- nbrowser_press_key: { hint: 'Press a key (Enter/Tab/etc.) as an in-page CDP key event.', related: ['nbrowser_type'], pitfalls: ['the target field must be FOCUSED first (click it)', 'browser-chrome chords (Ctrl+T, F11, Ctrl+L) do NOT work here - the browser intercepts them before the page; use AD desktop_press_key or the dedicated verb (e.g. nbrowser_fullscreen for F11)'] },+ nbrowser_press_key: { hint: 'Press a key (Enter/Tab/etc.) as an in-page CDP key event.', related: ['nbrowser_type'], pitfalls: ['the target field must be FOCUSED first (click it)', 'browser-chrome chords (Ctrl+T, F11, Ctrl+L) do NOT work here - the browser intercepts them before the page; use ab desktop_press_key or the dedicated verb (e.g. nbrowser_fullscreen for F11)'] }, nbrowser_fetch_url: { hint: 'Authed fetch using the user\'s REAL cookies - perfect for logged-in JSON/APIs that pup cannot reach.', related: ['nbrowser_navigate'], pitfalls: ['results come from the user\'s live session - treat as sensitive'] }, nbrowser_download_wait: { hint: 'Wait for a browser DOWNLOAD to complete and return the landed filename(s) via chrome.downloads events (not timestamp-polling the Downloads folder). Optional saveToSubfolder routes subsequent downloads into a subfolder of Downloads. Call it right after you trigger an export.', related: ['nbrowser_click', 'nbrowser_fetch_url'], pitfalls: ['timedOut:true = nothing landed in the window - verify the export click fired, then call again', 'saveToSubfolder takes effect for downloads started AFTER this call', 'the buffer is in-memory; a service-worker restart clears history (use a fresh sinceMs)'] }, nbrowser_errors: { hint: 'Buffered page/console errors for a tab.', related: ['nbrowser_eval'], pitfalls: ['the buffer CLEARS on read by default (pass clear:false to keep it)', 'errors are only captured once the tab has been driven (debugger attached) - an empty list is not proof of no errors on a never-driven tab'] },@@ -1310,17 +1311,17 @@ const VERB_META = { nbrowser_record_start: { hint: RECORD_WGC_HINT, related: ['nbrowser_record_stop', 'nbrowser_record_status'], next: 'nbrowser_record_status', pitfalls: ['⭐ prefer method:"wgc" (delegates to core desktop_record_window_start)', 'getdisplaymedia/tabcapture are REFUSED without fallbackConfirmed:true - a browser extension cannot silently screen-record'] }, nbrowser_record_stop: { hint: 'Stop a recording and finalize the mp4 (pull it with pull_file).', related: ['nbrowser_record_status'], pitfalls: ['needs the recordingId from record_start, not a sessionId'] }, nbrowser_record_status: { hint: 'Poll a recording.', related: ['nbrowser_record_start'], pitfalls: ['pass the SAME method (wgc|getdisplaymedia|tabcapture) + recordingId you started with', 'blank frames on a WGC recording = the recorded Chrome lacks the occlusion-disable launch flags'] },- nbrowser_record_list: { hint: 'List recent recordings.', related: ['nbrowser_record_status'], pitfalls: ['paths are on the LAPTOP - pull them with adom-desktop pull_file, do not expect them in the container'] },+ nbrowser_record_list: { hint: 'List recent recordings.', related: ['nbrowser_record_status'], pitfalls: ['paths are on the LAPTOP - pull them with adom-bridge-cli pull_file, do not expect them in the container'] }, nbrowser_flash: { hint: 'Flash the driven window\'s taskbar button - the "come look" cue (e.g. at a captcha/2FA/login human-gate).', related: ['nbrowser_flash_stop', 'nbrowser_flash_mode'], pitfalls: ['auto-clears on focus; nbrowser_flash_stop to clear early'] }, nbrowser_flash_stop: { hint: 'Stop the taskbar flash.', related: ['nbrowser_flash'], pitfalls: ['the flash already auto-clears when the user focuses the window - only stop it early if you no longer need their attention'] }, nbrowser_flash_mode: { hint: 'Set auto-flash verbosity: quiet (come-look moments: page load, screenshot, error/refusal, human-gate, spawned window) | chatty (every action) | off (no auto-flash at all). Flash manually with nbrowser_flash at task-done.', related: ['nbrowser_flash'], pitfalls: ['off is the user opt-out - if the user says the orange flashing is annoying, set flash_mode:"off" and it stops entirely', 'quiet is the default; the flash is a "come look" cue, so keep it for human-gates even if you dial the rest down'] }, 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 AD\'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_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: 'Press OK/Cancel (or answer a prompt) on a native JS dialog (alert/confirm/prompt/beforeunload) BLOCKING the page. accept:true=OK/Yes/Leave · accept:false=Cancel/Stay · text:"…" answers a prompt.', related: ['nbrowser_screenshot', 'nbrowser_click'], next: 'nbrowser_screenshot', pitfalls: ['these dialogs are NOT in a screenshot and have NO hwnd (Chrome renders them) - you learn of them via a modal_open error or a screenshot _modal note', 'the page is FROZEN until you handle it: clicks/typing/eval return modal_open until you do', 'if the AI "keeps failing to fill a form", suspect an open dialog - handle it, then re-screenshot'] }, 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'] }, nbrowser_caption: { hint: 'Paint an on-page caption (subtitle or red alert banner) for demos/narration.', related: ['nbrowser_cursor'], pitfalls: ['only on a window you opened', 'pass action:"hide" or empty text to dismiss'] },- nbrowser_focus: { hint: 'Select a tab within its window + flash its taskbar button. Does NOT raise the window unless you justify a foreground (userRequestedForeground:true + foregroundReason).', related: ['nbrowser_flash', 'nbrowser_screenshot'], pitfalls: ['does NOT foreground by design - the user\'s focus is never stolen; capture in place with nbrowser_screenshot', 'to actually raise it you must justify + warn the user (AD notify) first'] },+ nbrowser_focus: { hint: 'Select a tab within its window + flash its taskbar button. Does NOT raise the window unless you justify a foreground (userRequestedForeground:true + foregroundReason).', related: ['nbrowser_flash', 'nbrowser_screenshot'], pitfalls: ['does NOT foreground by design - the user\'s focus is never stolen; capture in place with nbrowser_screenshot', 'to actually raise it you must justify + warn the user (ab notify) first'] }, nbrowser_hover: { hint: 'Hover an element (by selector/text) to reveal menus/tooltips (isTrusted).', related: ['nbrowser_click'], pitfalls: ['some menus need a dwell - hover then screenshot before clicking the revealed item'] }, nbrowser_double_click: { hint: 'Double-click an element (isTrusted).', related: ['nbrowser_click'], pitfalls: ['click-by-text matching >1 element is refused (ambiguous_target) - pass a selector'] }, nbrowser_right_click: { hint: 'Right-click an element to open a context menu (isTrusted).', related: ['nbrowser_click'], pitfalls: ['the native context menu is OS chrome, not page DOM - you often cannot click its items via CDP; prefer the page\'s own UI'] },@@ -1357,7 +1358,7 @@ function dispatch(verb, args) { if (args && args.sessionId) rows = rows.filter((r) => r.sessionId === args.sessionId); if (args && args.profile) rows = rows.filter((r) => (r.profile || '').includes(String(args.profile))); if (args && args.thread) rows = rows.filter((r) => r.thread === args.thread);- // NB: args.caller is RESERVED - AD/the CLI inject the identity there on every call. Filter by+ // NB: args.caller is RESERVED - ab/the CLI inject the identity there on every call. Filter by // `callerThread` instead, and only when it is a plain string. if (args && typeof args.callerThread === 'string' && args.callerThread) rows = rows.filter((r) => String(r.caller || '').includes(args.callerThread)); if (args && args.verb) rows = rows.filter((r) => r.verb === args.verb || r.verb === 'nbrowser_' + args.verb);@@ -1369,11 +1370,11 @@ function dispatch(verb, args) { const entries = rows.slice(-limit).map((r) => ({ agoMs: now - r.ts, verb: r.verb, sessionId: r.sessionId, thread: r.thread, profile: r.profile, ok: r.ok, errorCode: r.errorCode || undefined, error: r.error || undefined, hwnd: r.hwnd || undefined, ops: r.ops || undefined, internal: r.internal || undefined, caller: r.caller || undefined })); const bySession = {}; for (const r of rows) { if (!r.sessionId) continue; const s = bySession[r.sessionId] || (bySession[r.sessionId] = { sessionId: r.sessionId, thread: sessionThread.get(r.sessionId) || r.thread, ownerCaller: sessionCaller.get(r.sessionId) || null, purpose: sessionPurpose.get(r.sessionId) || null, profile: r.profile, count: 0, lastAgoMs: Infinity }); s.count++; s.lastAgoMs = Math.min(s.lastAgoMs, now - r.ts); }- return resolve({ success: true, output: JSON.stringify({ ok: true, count: entries.length, totalLogged: ACTIVITY_LOG.length, windowMs: ACT_LOG_TTL_MS, activeSessions: Object.values(bySession).sort((a, b) => a.lastAgoMs - b.lastAgoMs), entries, _hint: 'Bounded activity trail (max 600 entries / ~1h, in-memory, resets on bridge restart). Filter: {sessionId, thread, profile, verb, errorsOnly:true, sinceMs, limit} - verb:"_taskbar" shows every overlay-badge/flash paint (hwnd + ops + ok), verb:"_ad_link" shows AD-link outages/recoveries. activeSessions = who drove what + how long ago (lastAgoMs); a large lastAgoMs means that session is idle (its badge should have faded).' }) });+ return resolve({ success: true, output: JSON.stringify({ ok: true, count: entries.length, totalLogged: ACTIVITY_LOG.length, windowMs: ACT_LOG_TTL_MS, activeSessions: Object.values(bySession).sort((a, b) => a.lastAgoMs - b.lastAgoMs), entries, _hint: 'Bounded activity trail (max 600 entries / ~1h, in-memory, resets on bridge restart). Filter: {sessionId, thread, profile, verb, errorsOnly:true, sinceMs, limit} - verb:"_taskbar" shows every overlay-badge/flash paint (hwnd + ops + ok), verb:"_ab_link" shows ab-link outages/recoveries. activeSessions = who drove what + how long ago (lastAgoMs); a large lastAgoMs means that session is idle (its badge should have faded).' }) }); } // nbrowser_readiness - READ-ONLY cold-start probe (SDK Cold-start tiers). Never spawns work, never // installs. This bridge has no heavy runtime (tiny Node), so "readiness" == the Level-2 extension- // being connected. AD's own bridge_readiness aggregates across bridges; this reports THIS one.+ // being connected. ab's own bridge_readiness aggregates across bridges; this reports THIS one. if (verb === 'nbrowser_readiness') { const live = liveConns(); const ready = live.length > 0;@@ -1416,7 +1417,7 @@ function dispatch(verb, args) { } const hostHint = hostExit ? ` The native host last exited because: "${hostExit.reason}" (host v${hostExit.hostVersion || '?'}, ${hostExit.at}).` : ''; const skewHint = staleOnDisk.length- ? ` ⛔ VERSION SKEW (likely the cause of "Native host has exited"): ${staleOnDisk.map((x) => `${x.profile} has extension ${x.installed}`).join(', ')}, but this bridge (${VERSION}) expects extension ${EXPECTED_EXT}. An extension/host that old cannot complete the native-messaging handshake, so the host exits with no message and the SW never connects. FIX: update the extension in that profile to ${EXPECTED_EXT} (re-sync the extension/ + host/ folders per installing-the-extension), then nbrowser_dev_reload {"profile":"<profile>"}. Note: the extension line (${EXPECTED_EXT}) and the skills-package line (a 0.4.x number on the wiki page) are DIFFERENT version streams - match the extension against expectedExtensionVersion, not the pkg version.`+ ? ` ⛔ VERSION SKEW (likely the cause of "Native host has exited"): ${staleOnDisk.map((x) => `${x.profile} has extension ${x.installed}`).join(', ')}, but this bridge (${VERSION}) expects extension ${EXPECTED_EXT}. An extension/host that old cannot complete the native-messaging handshake, so the host exits with no message and the SW never connects. FIX: update the extension in that profile to ${EXPECTED_EXT} (re-sync the extension/ + host/ folders per installing-the-extension), then nbrowser_dev_reload {"profile":"<profile>"}. Note: the extension line (${EXPECTED_EXT}) and the skills-package line (a 0.5.x number on the wiki page) are DIFFERENT version streams - match the extension against expectedExtensionVersion, not the pkg version.` : ''; return resolve({ success: true, output: JSON.stringify({ ready,@@ -1436,8 +1437,8 @@ function dispatch(verb, args) { _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.` + skewHint : 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.` + skewHint + hostHint,+ ? `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. ${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.` + skewHint + hostHint, }) }); } // nbrowser_install_chrome - install native Google Chrome for the user (Windows). Non-blocking:@@ -1455,14 +1456,14 @@ function dispatch(verb, args) { const hint = st.installed ? 'Native Google Chrome is installed. Next: help the user sign into their Google account (open chrome://settings or accounts.google.com and turn ON sync so passwords/bookmarks/autofill carry over), then load the Adom extension (installing-the-extension). Full flow: setting-up-native-chrome.' : st.awaitingUac- ? (st.notifyChannel ? 'Waiting on the user to approve the Windows security prompt. A sticky "Approve now" toast is on their desktop; tapping it re-opens the UAC. Keep polling; do NOT re-trigger install.' : 'A UAC was raised once but this AD cannot pop a notification. RELAY TO THE USER: go to the machine and click YES on the Google Chrome installer prompt, then re-poll.')+ ? (st.notifyChannel ? 'Waiting on the user to approve the Windows security prompt. A sticky "Approve now" toast is on their desktop; tapping it re-opens the UAC. Keep polling; do NOT re-trigger install.' : 'A UAC was raised once but this ab cannot pop a notification. RELAY TO THE USER: go to the machine and click YES on the Google Chrome installer prompt, then re-poll.') : st.installing ? 'Chrome install in progress (downloading / silent install). Poll again in a few seconds.' : st.phase === 'failed' ? `Chrome install failed (${st.errorCode}): ${st.error}. Fixes depend on the code - e.g. free disk, check network, or ask the user to install Chrome manually.` : 'No native Chrome yet and no install running. Call nbrowser_install_chrome to install it.'; return resolve({ success: true, output: JSON.stringify({ ...st, _hint: hint }) }); }- // nbrowser_describe - per-verb schemas for AD's Verbs tab + inline runner (AD v1.9.23+).- // AD POSTs {command:"nbrowser_describe",args:{}} and renders each verb expandable with these I/O+ // nbrowser_describe - per-verb schemas for ab's Verbs tab + inline runner (ab v1.9.23+).+ // ab POSTs {command:"nbrowser_describe",args:{}} and renders each verb expandable with these I/O // docs, prefilling the runner with `example`. One entry per declared verb (+ describe itself). if (verb === 'nbrowser_describe') { const S = 'optional sessionId of a window opened via nbrowser_open_window';@@ -1478,11 +1479,11 @@ function dispatch(verb, args) { 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),- d('nbrowser_open_window', 'Open a NEW window in the BACKGROUND (unfocused) and return a sessionId to drive it. REGISTRATION REQUIRED: sessionId + thread + purpose, or refused (session_unregistered). Never steals focus; a topmost driven window is auto-reverted (focus_violation).', { sessionId: 'required str (your short task id)', thread: 'required str (which AI thread you are)', purpose: 'required str (human-readable, e.g. "finishing John\'s IRS signup")', url: 'required str', profile: P, userRequestedForeground: 'optional bool - raise to foreground ONLY when the user asked or a browser/auth feature requires it (default false)', foregroundReason: 'REQUIRED with userRequestedForeground - human-readable why; without it the window STAYS background. Warn the user via AD notify_user FIRST.' }, { ok: 'bool', sessionId: 'str', tabId: 'str', windowId: 'int', hwnd: 'int (OS window handle - use with desktop_screenshot_window to capture the window+chrome+child popups)', url: 'str', _warning: 'str?', keptBackground: 'bool?' }, 30, { example: { sessionId: 'irs', thread: 'thread-7', purpose: 'finishing John IRS signup', url: 'https://wiki.adom.inc' } }),+ d('nbrowser_open_window', 'Open a NEW window in the BACKGROUND (unfocused) and return a sessionId to drive it. REGISTRATION REQUIRED: sessionId + thread + purpose, or refused (session_unregistered). Never steals focus; a topmost driven window is auto-reverted (focus_violation).', { sessionId: 'required str (your short task id)', thread: 'required str (which AI thread you are)', purpose: 'required str (human-readable, e.g. "finishing John\'s IRS signup")', url: 'required str', profile: P, userRequestedForeground: 'optional bool - raise to foreground ONLY when the user asked or a browser/auth feature requires it (default false)', foregroundReason: 'REQUIRED with userRequestedForeground - human-readable why; without it the window STAYS background. Warn the user via ab notify_user FIRST.' }, { ok: 'bool', sessionId: 'str', tabId: 'str', windowId: 'int', hwnd: 'int (OS window handle - use with desktop_screenshot_window to capture the window+chrome+child popups)', url: 'str', _warning: 'str?', keptBackground: 'bool?' }, 30, { example: { sessionId: 'irs', thread: 'thread-7', purpose: 'finishing John IRS signup', url: 'https://wiki.adom.inc' } }), d('nbrowser_close_window', 'Close a window the agent opened. includeSpawned:true also closes everything that session SPAWNED (composes/popups).', { sessionId: 'required str', includeSpawned: 'optional bool (also close this session\'s spawned windows)', profile: P }, { ok: 'bool', closed: 'bool', sessionId: 'str', alsoClosedSpawned: '[int]' }, 20, { example: { sessionId: 'work', includeSpawned: true } }), d('nbrowser_owned', 'List everything a session owns: opened windows + adopted SPAWNED windows/tabs (compose/popup/target=_blank a driven page opened). Acknowledges spawns.', { sessionId: 'optional str (all sessions if omitted)', profile: P }, { ok: 'bool', opened: '[{sessionId,windowId,url,title,purpose,ageMs}]', spawned: '[{kind,windowId,tabId,url,ageMs,spawnedBy,opener}]' }, 20, { example: { sessionId: 'irs' } }), d('nbrowser_cleanup', 'Close ONLY what a session SPAWNED (leaked composes/popups). Never the user\'s windows, never your primary opened window.', { sessionId: 'required str', profile: P }, { ok: 'bool', closed: '[{windowId,url}]', count: 'int', keptOpenedWindow: 'int?' }, 20, { example: { sessionId: 'irs' } }),- d('nbrowser_force_close', 'LAST-RESORT: close a TRUE ORPHAN window (unattributable to any live session) or your own - incl. browser_open/HD-test litter. TWO-STEP: call 1 {windowId} (extension-visible) OR {hwnd} (OS-level, extension-unreachable e.g. Edge sans extension) returns overrideTicket + willDestroy; call 2 adds {overrideTicket,attestOrphan:true,reason} and closes (hwnd form = graceful WM_CLOSE). NEVER closes another live thread\'s window.', { windowId: 'int (extension-visible form)', hwnd: 'int (OS-level form, from desktop_list_windows)', sessionId: 'optional str (yours, to close your own)', overrideTicket: 'str (from call 1)', attestOrphan: 'bool (call 2: must be true)', reason: 'str (call 2: why)', profile: P }, { ok: 'bool', needsAttestation: 'bool?', overrideTicket: 'str?', willDestroy: 'obj?', closed: 'bool?' }, 20, { example: { windowId: 12345 } }),+ d('nbrowser_force_close', 'LAST-RESORT: close a TRUE ORPHAN window (unattributable to any live session) or your own - incl. pup_open/HD-test litter. TWO-STEP: call 1 {windowId} (extension-visible) OR {hwnd} (OS-level, extension-unreachable e.g. Edge sans extension) returns overrideTicket + willDestroy; call 2 adds {overrideTicket,attestOrphan:true,reason} and closes (hwnd form = graceful WM_CLOSE). NEVER closes another live thread\'s window.', { windowId: 'int (extension-visible form)', hwnd: 'int (OS-level form, from desktop_list_windows)', sessionId: 'optional str (yours, to close your own)', overrideTicket: 'str (from call 1)', attestOrphan: 'bool (call 2: must be true)', reason: 'str (call 2: why)', profile: P }, { ok: 'bool', needsAttestation: 'bool?', overrideTicket: 'str?', willDestroy: 'obj?', closed: 'bool?' }, 20, { example: { windowId: 12345 } }), d('nbrowser_switch_window', 'Target a window for subsequent verbs; does NOT raise it unless the user asked to watch live.', { sessionId: 'required str', userRequestedForeground: 'optional bool (default false = do not raise; only true, and only on explicit user request, raises it)' }, { ok: 'bool', focused: 'bool' }, 15, { example: { sessionId: 'work' } }), d('nbrowser_list_windows', 'List the live windows/sessions, each with an owner label (agent-opened|agent-spawned|user). mine:true = only agent windows.', { profile: P, mine: 'optional bool' }, { ok: 'bool', sessions: '[{id,windowId,owner,spawnedBy,purpose,tabCount,active,state,bounds,url}]', count: 'int' }, 20, { example: { mine: true } }), d('nbrowser_window_state', 'Set window state (fullscreen/maximized/normal/minimized) or move/resize via left/top/width/height. maximize/fullscreen enlarge the window but keep it in the BACKGROUND (auto-reverted) unless userRequestedForeground:true.', { sessionId: 'required str', state: 'optional fullscreen|maximized|normal|minimized', left: 'optional int', top: 'optional int', width: 'optional int', height: 'optional int', userRequestedForeground: 'optional bool - allow maximize/fullscreen to raise the window, ONLY when the user asked to watch live (default false)' }, { ok: 'bool', windowId: 'int', state: 'str', _warning: 'str?' }, 20, { example: { sessionId: 'work', state: 'maximized' } }),@@ -1492,8 +1493,8 @@ function dispatch(verb, args) { d('nbrowser_profiles', 'List connected BROWSER×PROFILE targets + which is active.', {}, { ok: 'bool', profiles: '[{profile,email,browser,active,blocked,live}]', active: 'str' }, 15, { example: {} }), d('nbrowser_use_profile', 'Set the default profile action verbs target.', { profile: 'required str (key or email)' }, { ok: 'bool', active: 'str' }, 15, { example: { profile: 'chrome:[email protected]' } }), d('nbrowser_pin_session', 'Pin a sessionId to a profile so every verb on that session auto-routes there (prevents cross-profile mistakes). open_window pins automatically.', { sessionId: 'required str', profile: 'required str (key or email)' }, { ok: 'bool', sessionId: 'str', pinnedTo: 'str' }, 15, { example: { sessionId: 'schwab', profile: 'chrome:[email protected]' } }),- d('nbrowser_open_os_window', 'EXTENSION-FREE open of a URL in the user\'s real browser profile (OAuth consent etc). Background by default (minimize + orange taskbar flash); foreground needs foregroundReason. Returns hwnd (close later via force_close). Upsells the ABE install in every response.', { sessionId: 'required str', thread: 'required str', purpose: 'required str', profile: 'required str (key or email)', url: 'required http(s) str', browser: 'optional chrome|edge', foreground: 'optional bool', foregroundReason: 'required if foreground' }, { ok: 'bool', hwnd: 'int? (foreground later via desktop_bring_to_front {hwnd})', minimized: 'bool', flashed: 'bool', extensionInstalled: 'bool' }, 20, { example: { sessionId: 'gauth', thread: 'adom-google-setup', purpose: 'open Google OAuth consent for adom-google', profile: 'chrome:[email protected]', url: 'https://accounts.google.com/o/oauth2/auth?...' } }),- d('nbrowser_os_windows', 'OS-level browser-window inventory from the laptop (bridge-side): all Chrome/Edge/pup windows with hwnd/title/rect/z + which ABE session owns each. Reliable even when the caller\'s desktop_list_windows returns 0 (wrong target / AD hiccup) or a profile has no extension.', {}, { ok: 'bool (false = AD link down, data unknown)', count: 'int', windows: '[{hwnd,title,rect,z,kind,abeSession}]' }, 15, { example: {} }),+ d('nbrowser_open_os_window', 'EXTENSION-FREE open of a URL in the user\'s real browser profile (OAuth consent etc). Background by default (minimize + orange taskbar flash); foreground needs foregroundReason. Returns hwnd (close later via force_close). Upsells the nbe install in every response.', { sessionId: 'required str', thread: 'required str', purpose: 'required str', profile: 'required str (key or email)', url: 'required http(s) str', browser: 'optional chrome|edge', foreground: 'optional bool', foregroundReason: 'required if foreground' }, { ok: 'bool', hwnd: 'int? (foreground later via desktop_bring_to_front {hwnd})', minimized: 'bool', flashed: 'bool', extensionInstalled: 'bool' }, 20, { example: { sessionId: 'gauth', thread: 'adom-google-setup', purpose: 'open Google OAuth consent for adom-google', profile: 'chrome:[email protected]', url: 'https://accounts.google.com/o/oauth2/auth?...' } }),+ d('nbrowser_os_windows', 'OS-level browser-window inventory from the laptop (bridge-side): all Chrome/Edge/pup windows with hwnd/title/rect/z + which nbe session owns each. Reliable even when the caller\'s desktop_list_windows returns 0 (wrong target / ab hiccup) or a profile has no extension.', {}, { ok: 'bool (false = ab link down, data unknown)', count: 'int', windows: '[{hwnd,title,rect,z,kind,nbeSession}]' }, 15, { example: {} }), d('nbrowser_wake_profile', 'Wake an asleep browser profile with NO window (launches --no-startup-window; nothing foregrounds). Profile becomes live/drivable when its extension connects.', { profile: 'required str (key or email)', browser: 'optional chrome|edge' }, { ok: 'bool', woke: 'bool', alreadyLive: 'bool?', profile: 'str' }, 15, { example: { profile: 'chrome:[email protected]' } }), d('nbrowser_resolve_profiles', 'Force every connected extension to re-resolve its Google email + re-announce, so pending-N/empty-email keys become chrome:<email>/edge:<email>.', {}, { ok: 'bool', profiles: '[{profile,email,browser}]' }, 15, { example: {} }), d('nbrowser_whoami', 'Ask one profile to (re)resolve + report its Google email and re-announce it.', { profile: P }, { ok: 'bool', email: 'str', signedIn: 'bool', browser: 'str' }, 15, { example: {} }),@@ -1531,7 +1532,7 @@ function dispatch(verb, args) { d('nbrowser_download_wait', 'Wait for a download to complete; returns the landed filename(s). Optional saveToSubfolder routes downloads into a Downloads subfolder.', { sessionId: S, sinceMs: 'optional int (look back window, default 60000)', timeoutMs: 'optional int (default 60000, max 300000)', saveToSubfolder: 'optional str' }, { ok: 'bool', files: '[str]', count: 'int', timedOut: 'bool' }, 120, { longRunning: true, example: { sessionId: 'work', timeoutMs: 60000 } }), d('nbrowser_errors', 'Return buffered page/console errors for the tab.', { sessionId: S, tabId: 'optional str' }, { ok: 'bool', errors: '[...]' }, 20, { example: { sessionId: 'work' } }), d('nbrowser_close', 'Detach the debugger / close managed windows. No sessionId = all.', { sessionId: 'optional str' }, { ok: 'bool' }, 20, { example: {} }),- d('nbrowser_record_start', '⭐ Record a browser window. STRONGLY prefer method:"wgc" (delegates to the core adom-desktop verb desktop_record_window_start: background, up to 60fps, NO picker, NO banner, captures occluded windows, no focus theft). method:"getdisplaymedia"/"tabcapture" is a LAST-RESORT fallback (Chrome forces a manual share picker + banner) and is REFUSED unless you also pass fallbackConfirmed:true. Calling with no method returns the WGC guidance hint.', { method: 'required "wgc" (recommended) | "getdisplaymedia" | "tabcapture"', hwnd: 'int? (or titleContains)', titleContains: 'str?', fps: 'optional int (default 30)', codec: 'optional "h264"|"hevc"', fallbackConfirmed: 'required true ONLY for getdisplaymedia/tabcapture, to acknowledge the picker/banner tradeoff' }, { ok: 'bool', recordingId: 'str', path: 'str' }, 120, { statusVerb: 'nbrowser_record_status', longRunning: true, example: { method: 'wgc', titleContains: 'chip-fetcher' } }),+ d('nbrowser_record_start', '⭐ Record a browser window. STRONGLY prefer method:"wgc" (delegates to the core Bridge verb desktop_record_window_start: background, up to 60fps, NO picker, NO banner, captures occluded windows, no focus theft). method:"getdisplaymedia"/"tabcapture" is a LAST-RESORT fallback (Chrome forces a manual share picker + banner) and is REFUSED unless you also pass fallbackConfirmed:true. Calling with no method returns the WGC guidance hint.', { method: 'required "wgc" (recommended) | "getdisplaymedia" | "tabcapture"', hwnd: 'int? (or titleContains)', titleContains: 'str?', fps: 'optional int (default 30)', codec: 'optional "h264"|"hevc"', fallbackConfirmed: 'required true ONLY for getdisplaymedia/tabcapture, to acknowledge the picker/banner tradeoff' }, { ok: 'bool', recordingId: 'str', path: 'str' }, 120, { statusVerb: 'nbrowser_record_status', longRunning: true, example: { method: 'wgc', titleContains: 'chip-fetcher' } }), d('nbrowser_record_stop', 'Stop a recording and finalize the mp4.', { method: 'required "wgc"', recordingId: 'required str' }, { ok: 'bool', path: 'str', frames: 'int' }, 120, { example: { method: 'wgc', recordingId: 'rec-...' } }), d('nbrowser_record_status', 'Poll a recording.', { method: 'required "wgc"', recordingId: 'optional str' }, { ok: 'bool', status: 'str' }, 20, { example: { method: 'wgc' } }), d('nbrowser_record_list', 'List recent recordings.', {}, { ok: 'bool', recordings: '[...]' }, 20, { example: {} }),@@ -1539,7 +1540,7 @@ function dispatch(verb, args) { d('nbrowser_flash_stop', 'Stop the taskbar flash.', { sessionId: 'required str' }, { ok: 'bool' }, 15, { example: { sessionId: 'work' } }), d('nbrowser_flash_mode', 'Set auto-flash verbosity.', { mode: 'required "quiet"|"chatty"|"off"' }, { ok: 'bool', mode: 'str' }, 15, { example: { mode: 'quiet' } }), d('nbrowser_taskbar', 'Paint progress/overlay/flash on the window taskbar button.', { sessionId: 'required str', progress: 'optional {state,value?}', overlay: 'optional {badge}', flash: 'optional "until_focused"|"stop"' }, { ok: 'bool', applied: '[str]' }, 15, { example: { sessionId: 'work', progress: { state: 'indeterminate' } } }),- d('nbrowser_describe', 'This verb: returns per-verb schemas for the AD Verbs tab.', {}, { success: 'bool', verbs: '[{name,summary,input,output,timeoutSeconds,statusVerb,longRunning,example}]' }, 15, { example: {} }),+ 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: {} }), ]; return resolve({ success: true, verbs }); }@@ -1615,9 +1616,9 @@ function dispatch(verb, args) { return; } // OS-LEVEL force_close: {hwnd} instead of {windowId} - for browser windows in a profile whose- // extension is NOT connected (e.g. Edge without the extension, or browser_open/pup litter from an+ // extension is NOT connected (e.g. Edge without the extension, or pup_open/pup litter from an // HD test). SAME dramatic two-step gate as the extension path: ticket + willDestroy + attestation,- // and a window belonging to a live ABE session is refused absolutely. Restricted to REAL browser+ // and a window belonging to a live nbe session is refused absolutely. Restricted to REAL browser // windows (Chrome_WidgetWin class) so this can never close an arbitrary app. Uses a graceful // WM_CLOSE - a page with unsaved state may prompt instead of dying. if (verb === 'nbrowser_force_close' && args && args.hwnd && !args.windowId) {@@ -1631,11 +1632,11 @@ function dispatch(verb, args) { } catch {} const w = ws.find((x) => x.hwnd === hwnd); if (!w) return resolve({ success: false, errorCode: 'bad_window', error: `hwnd ${hwnd} is not a live BROWSER window`, _hint: 'OS-level force_close only targets real Chrome/Edge windows (Chrome_WidgetWin class). Find candidates with desktop_list_windows; for windows the extension CAN see, use force_close {windowId} instead.' });- for (const [sid, h] of sessionHwnd) if (h === hwnd) return resolve({ success: false, errorCode: 'owned_by_other', error: `⛔ force_close REFUSED: hwnd ${hwnd} belongs to live ABE session '${sid}'. Cross-thread ownership is ABSOLUTE.`, _hint: 'Only that thread may close its window (nbrowser_close_window). If it is truly abandoned its badge idle-expires and its thread cleans up.' });+ for (const [sid, h] of sessionHwnd) if (h === hwnd) return resolve({ success: false, errorCode: 'owned_by_other', error: `⛔ force_close REFUSED: hwnd ${hwnd} belongs to live nbe session '${sid}'. Cross-thread ownership is ABSOLUTE.`, _hint: 'Only that thread may close its window (nbrowser_close_window). If it is truly abandoned its badge idle-expires and its thread cleans up.' }); if (!args.overrideTicket) { const ticket = 'os-' + Math.random().toString(36).slice(2, 10); _osCloseTickets.set(ticket, { hwnd, expires: Date.now() + 60000 });- return resolve({ success: false, errorCode: 'attestation_required', output: JSON.stringify({ ok: false, needsAttestation: true, overrideTicket: ticket, willDestroy: { hwnd, title: w.title, className: w.className, osLevel: true }, _hint: `⚠️ OS-LEVEL FORCE-CLOSE is a destructive last resort. This will close the REAL browser window "${String(w.title || '').slice(0, 70)}" (hwnd ${hwnd}) and any unsaved content in it. To proceed call again with EXACTLY: nbrowser_force_close {hwnd:${hwnd}, overrideTicket:"${ticket}", attestOrphan:true, reason:"<why this window must go>"}. Ticket expires in 60s, closes only this one window, and a window owned by a live ABE session can NEVER be closed this way.` }) });+ return resolve({ success: false, errorCode: 'attestation_required', output: JSON.stringify({ ok: false, needsAttestation: true, overrideTicket: ticket, willDestroy: { hwnd, title: w.title, className: w.className, osLevel: true }, _hint: `⚠️ OS-LEVEL FORCE-CLOSE is a destructive last resort. This will close the REAL browser window "${String(w.title || '').slice(0, 70)}" (hwnd ${hwnd}) and any unsaved content in it. To proceed call again with EXACTLY: nbrowser_force_close {hwnd:${hwnd}, overrideTicket:"${ticket}", attestOrphan:true, reason:"<why this window must go>"}. Ticket expires in 60s, closes only this one window, and a window owned by a live nbe session can NEVER be closed this way.` }) }); } const t = _osCloseTickets.get(args.overrideTicket); if (!t || t.hwnd !== hwnd) return resolve({ success: false, errorCode: 'bad_ticket', error: 'overrideTicket does not match this hwnd', _hint: 'One ticket = one window, one use. Re-request with just {hwnd}.' });@@ -1653,21 +1654,21 @@ function dispatch(verb, args) { const rb = rr && rr.body; const sent = !!(rr && rr.status && rr.status < 300 && rb && (rb.success === true || rb.ok === true) && (rb.exitCode === undefined || rb.exitCode === 0)); logActivity({ verb: 'nbrowser_force_close', sessionId: (args && args.sessionId) || null, thread: (args && args.thread) || null, profile: 'os-hwnd', ok: sent, errorCode: sent ? null : 'wm_close_failed' });- if (!sent) return resolve({ success: false, errorCode: 'wm_close_failed', error: `attestation accepted but the WM_CLOSE could not be delivered (${(rb && (rb.error || rb.stderr)) || 'AD run_script failed'})`, _hint: 'The window was NOT closed. Retry force_close from call 1 ({hwnd} for a fresh ticket); if AD is unreachable, taskbar/flash are degraded too - check nbrowser_activity for _ad_link entries.' });+ if (!sent) return resolve({ success: false, errorCode: 'wm_close_failed', error: `attestation accepted but the WM_CLOSE could not be delivered (${(rb && (rb.error || rb.stderr)) || 'ab run_script failed'})`, _hint: 'The window was NOT closed. Retry force_close from call 1 ({hwnd} for a fresh ticket); if ab is unreachable, taskbar/flash are degraded too - check nbrowser_activity for _ab_link entries.' }); return resolve({ success: true, output: JSON.stringify({ ok: true, closed: true, hwnd, osLevel: true, reason: args.reason, _hint: 'Sent a graceful WM_CLOSE. A window with unsaved state may show a confirm dialog instead of closing - verify with desktop_list_windows.' }) }); })(); return; } // AUTHORITATIVE OS-level browser-window inventory, measured ON the laptop BY the bridge - so an AI- // whose own desktop_list_windows returns 0 (wrong --target with two ADs connected, transient AD+ // whose own desktop_list_windows returns 0 (wrong --target with two abs connected, transient ab // hiccup, DPI-blind PowerShell EnumWindows) still has one reliable probe. Annotates each hwnd with- // the live ABE session that owns it (never force-close those) and flags pup/Chrome-for-Testing.+ // the live nbe session that owns it (never force-close those) and flags pup/Chrome-for-Testing. // EXTENSION-FREE OS OPEN — the graceful-degradation wedge (John, 2026-07-19). Getting a user to- // install ABE is hard; but MANY high-value tasks only need their REAL browser OPENED at a URL in the+ // install nbe is hard; but MANY high-value tasks only need their REAL browser OPENED at a URL in the // right profile - OAuth consent (adom-google, Autodesk APS) where they're already signed in and just // click Approve. So: open the native browser extension-free, BACKGROUND by default (brief blip, then // minimized + orange taskbar flash = "come click when you're ready"), registered like every window,- // and every response UPSELLS the ABE install for the full feature set.+ // and every response UPSELLS the nbe install for the full feature set. if (verb === 'nbrowser_open_os_window') { const a = args || {}; if (!a.sessionId || !a.thread || !a.purpose) {@@ -1786,7 +1787,7 @@ function dispatch(verb, args) { sessionThread.set(a.sessionId, a.thread); sessionPurpose.set(a.sessionId, a.purpose); sessionProfile.set(a.sessionId, kp.key); if (hwnd) sessionHwnd.set(a.sessionId, hwnd); osOpened.add(a.sessionId); persistSessions(); const upsell = extSt && extSt.installed ? `NOTE: the Adom extension IS installed in this profile${extSt.enabled ? '' : ' (but DISABLED)'} - you could have FULL control instead: nbrowser_wake_profile then nbrowser_open_window gives you navigation, reading, clicking, screenshots, and ownership guards. Prefer that path.`- : `UPSELL (tell the user when the moment is right): this window is OPEN but I cannot see or drive it - the Adom extension is not installed in this profile. With ABE installed (see installing-the-extension - it can be installed FOR the user in ~a minute) the assistant could fill the form, read the result, screenshot progress, and clean up after itself. Until then the USER must do the clicking.`;+ : `UPSELL (tell the user when the moment is right): this window is OPEN but I cannot see or drive it - the Adom extension is not installed in this profile. With nbe installed (see installing-the-extension - it can be installed FOR the user in ~a minute) the assistant could fill the form, read the result, screenshot progress, and clean up after itself. Until then the USER must do the clicking.`; // STABLE SUCCESS CONTRACT (fusion-bridge report, 2026-07-21): a fired launch IS success for a // consent flow - "the page is now in front of the user". hwnd resolution is a SEPARATE fact. // Callers must never have to regex _hint. ok/launched = did the browser launch; hwndResolved =@@ -1809,8 +1810,8 @@ function dispatch(verb, args) { if (r.status === 0) adOk = false; } catch { adOk = false; } const bySess = {}; for (const [sid, h] of sessionHwnd) bySess[h] = sid;- const browsers = ws.filter((w) => /Chrome_WidgetWin/.test(w.className || '')).map((w) => ({ hwnd: w.hwnd, title: w.title, rect: w.rect, z: w.z, kind: /for Testing/.test(w.title || '') ? 'pup/chrome-for-testing' : 'native-browser', abeSession: bySess[w.hwnd] || null }));- resolve({ success: true, output: JSON.stringify({ ok: adOk, count: browsers.length, windows: browsers, totalOsWindows: ws.length, _hint: adOk ? 'Browser windows as the LAPTOP sees them right now (bridge-side enumeration - no --target guessing, DPI-safe). abeSession = a live ABE session owns that hwnd (NEVER force-close it; that thread cleans up its own). kind pup/chrome-for-testing = browser_open/pup litter. Edge windows appear even when Edge has no extension. To close an untracked one: nbrowser_force_close {hwnd} (two-step attestation). If your own desktop_list_windows returned 0/null: most likely AD was mid-restart (it SELF-UPDATES and restarts, sometimes several times a day - anything in flight returns empty), or you targeted the WRONG desktop when multiple ADs are connected (check `targets`; pass --target for the laptop).' : 'AD link is DOWN from the bridge right now (desktop_list_windows unreachable) - window data unavailable, not "0 windows". Check nbrowser_activity for _ad_link entries; retry shortly.' }) });+ const browsers = ws.filter((w) => /Chrome_WidgetWin/.test(w.className || '')).map((w) => ({ hwnd: w.hwnd, title: w.title, rect: w.rect, z: w.z, kind: /for Testing/.test(w.title || '') ? 'pup/chrome-for-testing' : 'native-browser', nbeSession: bySess[w.hwnd] || null }));+ resolve({ success: true, output: JSON.stringify({ ok: adOk, count: browsers.length, windows: browsers, totalOsWindows: ws.length, _hint: adOk ? 'Browser windows as the LAPTOP sees them right now (bridge-side enumeration - no --target guessing, DPI-safe). nbeSession = a live nbe session owns that hwnd (NEVER force-close it; that thread cleans up its own). kind pup/chrome-for-testing = pup_open/pup litter. Edge windows appear even when Edge has no extension. To close an untracked one: nbrowser_force_close {hwnd} (two-step attestation). If your own desktop_list_windows returned 0/null: most likely ab was mid-restart (it SELF-UPDATES and restarts, sometimes several times a day - anything in flight returns empty), or you targeted the WRONG desktop when multiple abs are connected (check `targets`; pass --target for the laptop).' : 'ab link is DOWN from the bridge right now (desktop_list_windows unreachable) - window data unavailable, not "0 windows". Check nbrowser_activity for _ab_link entries; retry shortly.' }) }); })(); return; }@@ -1921,7 +1922,7 @@ function dispatch(verb, args) { m.allowed_origins.push(origin); fs.writeFileSync(mp, JSON.stringify(m, null, 2)); updated.push(mp); } catch {} }- return resolve({ success: true, output: JSON.stringify({ ok: updated.length > 0 || present.length > 0, origin, updatedManifests: updated, alreadyPresent: present, _hint: (updated.length || present.length) ? 'Origin is in the host manifest now. RELOAD the extension in that browser (edge://extensions -> Reload) so it re-reads allowed_origins and can reach the native host; then nbrowser_profiles should list it.' : 'No host-manifest.json found (checked ~/.adom/native-browser and ~/adom-browser-extension/host). Re-run the install, or the manifest is elsewhere.' }) });+ return resolve({ success: true, output: JSON.stringify({ ok: updated.length > 0 || present.length > 0, origin, updatedManifests: updated, alreadyPresent: present, _hint: (updated.length || present.length) ? 'Origin is in the host manifest now. RELOAD the extension in that browser (edge://extensions -> Reload) so it re-reads allowed_origins and can reach the native host; then nbrowser_profiles should list it.' : 'No host-manifest.json found (checked ~/.adom/native-browser and ~/adom-native-browser/host). Re-run the install, or the manifest is elsewhere.' }) }); } // --- taskbar flash (bridge-local; targets the cached driven-window hwnd) --- if (verb === 'nbrowser_flash_mode') {@@ -1965,7 +1966,7 @@ function dispatch(verb, args) { taskbar(hwnd, opts).then(() => resolve({ success: true, output: JSON.stringify({ ok: true, hwnd, applied: Object.keys(opts).length ? Object.keys(opts) : ['overlay:managed'], _hint: 'progress.state: normal(green) | paused(YELLOW - use for captcha/2FA/login human-gates) | error(red) | indeterminate(marquee) | none. overlay.badge: "adom" = the driving badge (the bridge composites the profile\'s CIRCLE avatar + the Adom favicon - never a bare square) | "none" = restore the NATIVE avatar look (never blanks the slot). The badge is AUTOMATIC while driving and idle-expires after ~3 min, so you rarely need to set it. Clear progress+flash at task-end: {progress:{state:"none"},flash:"stop"}.' }) })); return; }- // WGC recording is handled HERE (bridge -> AD direct API, standalone - never HD), not in the+ // WGC recording is handled HERE (bridge -> ab direct API, standalone - never HD), not in the // extension (a browser can't capture another window). method:"wgc". if (verb.startsWith('nbrowser_record_') && args && args.method === 'wgc') { return wgcRecord(verb, args).then(resolve);@@ -1989,10 +1990,10 @@ function dispatch(verb, args) { } // acknowledged fallback → fall through to the extension's getDisplayMedia/tabCapture path below. }- // OS-level window raise/focus is owned by the CORE adom-desktop verb desktop_bring_to_front. The+ // OS-level window raise/focus is owned by the CORE Bridge verb desktop_bring_to_front. The // extension deliberately does NOT implement a focus/raise-window verb - it must never steal the // user's focus. So we don't expose a phantom verb; but an AI coming from pup will guess- // browser_focus_window -> nbrowser_focus_window. Rather than fail opaquely in the extension,+ // pup_focus_window -> nbrowser_focus_window. Rather than fail opaquely in the extension, // redirect it to the core verb (with THIS session's captured hwnd) - it would find it on its own. const RAISE_REDIRECT = { nbrowser_focus_window: 'OS window raise/focus',@@ -2005,13 +2006,13 @@ function dispatch(verb, args) { const target = hwnd ? `{"hwnd":${hwnd}}` : '{"titleContains":"<the driven window\'s title>"}'; return resolve({ success: false,- error: `${verb} is not an extension verb - ${RAISE_REDIRECT[verb]} is owned by the core adom-desktop verb desktop_bring_to_front`,- _hint: `Use the CORE AD verb instead: desktop_bring_to_front ${target}. The extension intentionally never raises or steals the user's focus - nbrowser_focus only activates the tab + flashes its taskbar button; desktop_bring_to_front is the explicit "bring it to the front so the user can watch" action.`,+ error: `${verb} is not an extension verb - ${RAISE_REDIRECT[verb]} is owned by the core Bridge verb desktop_bring_to_front`,+ _hint: `Use the CORE ab verb instead: desktop_bring_to_front ${target}. The extension intentionally never raises or steals the user's focus - nbrowser_focus only activates the tab + flashes its taskbar button; desktop_bring_to_front is the explicit "bring it to the front so the user can watch" action.`, }); } // Other pup verbs with no 1:1 extension verb - redirect to the right one instead of failing opaquely. if (verb === 'nbrowser_alert_window') {- return resolve({ success: false, error: 'nbrowser_alert_window is not an extension verb', _hint: 'Flash the driven window\'s taskbar button with nbrowser_flash {sessionId} (orange "come look" cue; nbrowser_flash_stop to clear). For an arbitrary OS window, the core AD verb is desktop_flash_window {hwnd}.' });+ return resolve({ success: false, error: 'nbrowser_alert_window is not an extension verb', _hint: 'Flash the driven window\'s taskbar button with nbrowser_flash {sessionId} (orange "come look" cue; nbrowser_flash_stop to clear). For an arbitrary OS window, the core ab verb is desktop_flash_window {hwnd}.' }); } if (verb === 'nbrowser_close') { return resolve({ success: false, error: 'nbrowser_close is not an extension verb', _hint: 'Close a specific window with nbrowser_close_window {sessionId} or a tab with nbrowser_close_tab {sessionId,tabId}. There is no global "detach all" - the extension idle-detaches the debugger automatically when no verb is running.' });@@ -2054,7 +2055,7 @@ function dispatch(verb, args) { } if (Array.isArray(args.filePaths) && args.filePaths.length && args.chooserStatus !== true) { // both real modes read the files at serve time; a status probe needs no files const missing = (args.filePaths || []).filter((fp) => { try { return !fs.existsSync(fp); } catch { return true; } });- if (missing.length) return resolve({ success: false, errorCode: 'file_missing', error: `file_missing: ${missing.join(', ')} not found on the laptop`, _hint: 'Paths must be ABSOLUTE paths on the machine running the browser. Push files there first with adom-desktop send_files, or pass files:[{name, contentBase64}] and the bridge stages them for you.' });+ if (missing.length) return resolve({ success: false, errorCode: 'file_missing', error: `file_missing: ${missing.join(', ')} not found on the laptop`, _hint: 'Paths must be ABSOLUTE paths on the machine running the browser. Push files there first with adom-bridge-cli send_files, or pass files:[{name, contentBase64}] and the bridge stages them for you.' }); } } catch (e) { return resolve({ success: false, error: 'upload staging failed: ' + ((e && e.message) || e) }); } }@@ -2136,7 +2137,7 @@ async function onHostFrameInner(frame) { let payload = frame.payload || {}; // Screenshot file-save: huge base64 over the relay times out, so when the caller passes // toFile:true we write the PNG to a laptop temp file here and return its path instead of the- // base64. Pull it with `adom-desktop pull_file` (binary chunked) - fast + no relay timeout.+ // base64. Pull it with `adom-bridge-cli pull_file` (binary chunked) - fast + no relay timeout. const isShot = /screenshot/.test(p.verb); const isRec = p.verb === 'nbrowser_record_stop'; if (payload.base64 && ((isShot && p.toFile) || isRec)) {@@ -2147,7 +2148,7 @@ async function onHostFrameInner(frame) { const file = path.join(dir, `${isRec ? 'rec' : 'shot'}-${Date.now()}.${isRec ? 'webm' : 'png'}`); fs.writeFileSync(file, Buffer.from(payload.base64, 'base64')); const sizeKB = Math.round(Buffer.byteLength(payload.base64, 'base64') / 1024);- payload = { ...payload, path: file, savedToFile: true, sizeKB, _hint: 'Pull this with `adom-desktop pull_file {filePaths:["' + file.replace(/\\/g, '\\\\') + '"]}` (binary, chunked, no relay timeout).' };+ payload = { ...payload, path: file, savedToFile: true, sizeKB, _hint: 'Pull this with `adom-bridge-cli pull_file {filePaths:["' + file.replace(/\\/g, '\\\\') + '"]}` (binary, chunked, no relay timeout).' }; delete payload.base64; } catch (e) { payload._fileError = String((e && e.message) || e); } }@@ -2168,7 +2169,7 @@ async function onHostFrameInner(frame) { const f = payload.finder; if (f.annotatedShot) spill(f.annotatedShot, 'annotated'); if (Array.isArray(f.shots)) f.shots.forEach((s, i) => spill(s, (s.label || 'shot') + '-' + i));- f._hint = 'Pull annotatedShot.path / shots[].path with `adom-desktop pull_file {filePaths:[...]}` (binary, no relay timeout). In the annotated overlay each candidate is a numbered box - ★=chosen, ⛔=occluded - and the blue crosshair is the click point.';+ f._hint = 'Pull annotatedShot.path / shots[].path with `adom-bridge-cli pull_file {filePaths:[...]}` (binary, no relay timeout). In the annotated overlay each candidate is a numbered box - ★=chosen, ⛔=occluded - and the blue crosshair is the click point.'; } catch (e) { payload._finderFileError = String((e && e.message) || e); } } // taskbar: capture the driven window's hwnd at control-start, auto-flash per mode, and TEACH the@@ -2194,7 +2195,7 @@ async function onHostFrameInner(frame) { await new Promise((r) => setTimeout(r, 250)); } payload = { ...payload, ...(neu ? { hwnd: neu } : {}), _taskbarHint: flashHint(),- _screenshotHint: 'To SEE this window without foregrounding it: nbrowser_screenshot {sessionId} = the PAGE bitmap (fast, default). To see the WINDOW FRAME or a native popup/dialog (Chrome password dropdown, print dialog, alert/confirm), use AD desktop_screenshot_window {hwnd:' + (neu || '<from desktop_list_windows by title>') + '} - it captures the window + its OWNED CHILD windows (parent/child hwnds). NEVER a fullscreen/monitor screenshot to inspect a browser window.' };+ _screenshotHint: 'To SEE this window without foregrounding it: nbrowser_screenshot {sessionId} = the PAGE bitmap (fast, default). To see the WINDOW FRAME or a native popup/dialog (Chrome password dropdown, print dialog, alert/confirm), use ab desktop_screenshot_window {hwnd:' + (neu || '<from desktop_list_windows by title>') + '} - it captures the window + its OWNED CHILD windows (parent/child hwnds). NEVER a fullscreen/monitor screenshot to inspect a browser window.' }; } // NOTE: list_windows/owned deliberately do NOT badge here anymore — badging an owned-but-IDLE window // is what left a "driving" badge on a profile no thread was actually driving. The badge is applied@@ -2237,12 +2238,12 @@ async function onHostFrameInner(frame) { } function resolve_ok(resolve, payload) { resolve({ success: true, output: JSON.stringify(payload) }); } -// Loopback bind host. AD (v1.9.63+) passes ADOM_BIND_HOST (always 127.0.0.1) to every bridge it+// 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?" // dialog. Both our listeners are loopback-only regardless. const BIND = process.env.ADOM_BIND_HOST || '127.0.0.1'; -// ---- HTTP server for AD ----------------------------------------------------+// ---- HTTP server for ab ---------------------------------------------------- const httpSrv = http.createServer((req, res) => { const reply = (code, obj) => { const body = Buffer.from(JSON.stringify(obj));@@ -2262,14 +2263,14 @@ const httpSrv = http.createServer((req, res) => { try { parsed = JSON.parse(body || '{}'); } catch { return reply(400, { success: false, error: 'bad json' }); } const verb = parsed.command || parsed.verb; if (!verb) return reply(400, { success: false, error: 'missing command/verb' });- // CALLER PROVENANCE (AD 1.9.180+, wiki #345/#351): AD stamps WHO asked on every relayed request.+ // CALLER PROVENANCE (ab 1.9.180+, wiki #345/#351): ab stamps WHO asked on every relayed request. // SELF-ASSERTED - attribution/logging/UX/arbitration only, NEVER authorization. const caller = { thread: String(req.headers['x-adom-caller-thread'] || (parsed.args && parsed.args.caller && parsed.args.caller.aiThread) || '').slice(0, 120), container: String(req.headers['x-adom-caller-container'] || (parsed.args && parsed.args.caller && parsed.args.caller.containerName) || '').slice(0, 120), reason: String(req.headers['x-adom-caller-reason'] || '').slice(0, 200), };- // Run the verb inside an ambient caller context so EVERY AD callback we make while carrying it+ // Run the verb inside an ambient caller context so EVERY ab callback we make while carrying it // out (taskbar badge, caption, window enumeration) forwards that identity automatically. The // bridge is deeply async, so threading it by hand through every call site would rot immediately. reply(200, await withCaller(caller, () => dispatch(verb, parsed.args)));@@ -2330,13 +2331,13 @@ const hostSrv = net.createServer((sock) => { function writeHostFile(hostPort) { fs.mkdirSync(path.dirname(HOSTFILE), { recursive: true }); fs.writeFileSync(HOSTFILE, JSON.stringify({ host: '127.0.0.1', hostPort, pid: process.pid, version: VERSION }));- // Publish the Node AD gave us so the Chrome-launched host can reuse it (see NODEPATHFILE note above).+ // Publish the Node ab gave us so the Chrome-launched host can reuse it (see NODEPATHFILE note above). // Normalize to native Windows backslashes - node-path.txt is consumed by a .bat, and a quoted // forward-slash exe path is not reliably executable in cmd.exe. try { fs.writeFileSync(NODEPATHFILE, process.execPath.split('/').join('\\')); } catch {} } // PID-aware cleanup: ONLY delete host.json if it still points at *this* process. Otherwise a churning-// instance (AD respawn, a manual run, a crash-restart) deletes the discovery file that another LIVE+// instance (ab respawn, a manual run, a crash-restart) deletes the discovery file that another LIVE // bridge owns - exactly what stranded the extension (host.json vanished while a bridge was up). Never // blow away a sibling's file. function cleanup() {@@ -2355,7 +2356,7 @@ function argPort() { return 0; // ephemeral } -// Resilient bind. If AD hands us a --port that's already held (HNS reservation, a zombie bridge, a+// Resilient bind. If ab hands us a --port that's already held (HNS reservation, a zombie bridge, a // double-spawn), DON'T die silently - fall back to an ephemeral port and keep serving. The hostSrv + // discovery file get written regardless, so the extension always has a live port to dial. let httpFellBack = false;@@ -2373,14 +2374,14 @@ function startHttp(p) { httpSrv.listen(p, BIND, () => { const port = httpSrv.address().port; if (hostSrv.listening) { // already came up on a prior attempt; just log the new http port- process.stdout.write(`[native-browser] bridge up: AD http :${port} host tcp :${hostSrv.address().port}\n`);+ process.stdout.write(`[native-browser] bridge up: ab http :${port} host tcp :${hostSrv.address().port}\n`); return; } hostSrv.once('error', (e) => process.stderr.write(`[native-browser] host listen error: ${e && e.code}\n`)); hostSrv.listen(0, BIND, () => { const hostPort = hostSrv.address().port; writeHostFile(hostPort);- process.stdout.write(`[native-browser] bridge up: AD http :${port} host tcp :${hostPort}\n`);+ process.stdout.write(`[native-browser] bridge up: ab http :${port} host tcp :${hostPort}\n`); }); }); }@@ -2428,7 +2429,7 @@ function adomExtStateForProfile(browser, profileDir) { // A LOAD-UNPACKED extension often has an EMPTY manifest.name in Secure Preferences, so matching // on the name alone reported installed:false for a profile that HAD it (real bug, 2026-07-21). // Match on our pinned extension ID or the source path too.- const hit = /adom/i.test(nm) || id.startsWith(ADOM_EXT_ID_PREFIX) || /adom-browser-extension|[\\/]adom[\\/]/i.test(pth);+ const hit = /adom/i.test(nm) || id.startsWith(ADOM_EXT_ID_PREFIX) || /adom-native-browser|adom-browser-extension|[\\/]adom[\\/]/i.test(pth); if (!hit) continue; return { checked: true, installed: true, id, name: nm || 'Adom (unpacked)', version: (ent.manifest && ent.manifest.version) || null, enabled: ent.state !== 0, unpackedPath: ent.path || null, unpacked: !nm }; }@@ -2486,7 +2487,7 @@ async function reapClosedWindows() { if (!sessionHwnd.size && !spawnHwnd.size) return; let live; try { live = new Set((await chromeWindowsDetailed()).map((w) => w.hwnd)); } catch { return; }- if (!live.size) return; // AD blind - never reap on no data+ if (!live.size) return; // ab blind - never reap on no data let reaped = 0; for (const [sid, h] of [...sessionHwnd]) { if (live.has(h)) continue;
dev-skills/cross-platform-dev/SKILL.md+4−4@@ -1,6 +1,6 @@ --- name: cross-platform-dev-description: How adom-browser-extension ships across Windows and macOS - Windows-first, Kyle ports to Mac. Read before adding platform-specific code.+description: How adom-native-browser ships across Windows and macOS - Windows-first, Kyle ports to Mac. Read before adding platform-specific code. --- # Cross-platform dev flow (Windows-first → Kyle ports to Mac)@@ -9,10 +9,10 @@ Adom's standard flow, applied to this repo: **John builds and ships on Windows; as a fast-follow.** John is a Windows user, Kyle is the Mac user. Design accordingly so Kyle's port is a thin layer, never a rewrite. -> **Shipping a bridge update so AD picks it up** (build the manifest + zip, the `.zip`-blocked-use-`.bin`+> **Shipping a bridge update so ab picks it up** (build the manifest + zip, the `.zip`-blocked-use-`.bin` > trap, the canonical URL, `bridge_install` vs `refresh_bridges`) is general bridge-author knowledge and > lives on the bridges hub: the **`shipping-a-bridge`** skill at-> [adom/adom-desktop-bridges](https://wiki.adom.inc/adom/adom-desktop-bridges). Read it before cutting a+> [adom/adom-bridge-sdk](https://wiki.adom.inc/adom/adom-bridge-sdk). Read it before cutting a > new `bridge/` version. ## The rule@@ -25,7 +25,7 @@ Windows-specific to the **install/registration layer** only: |---|---|---| | Native-messaging host manifest | `HKCU\Software\<vendor>\NativeMessagingHosts\inc.adom.native_browser` | `~/Library/Application Support/<Google/Chrome \| Microsoft Edge>/NativeMessagingHosts/inc.adom.native_browser.json` | | Host launcher | `native-host.bat` → `node native-host.js` | `native-host.sh` → `node native-host.js` (chmod +x) |-| Bridge cache path | `%LOCALAPPDATA%\Adom Desktop\bridges-cache\native-browser\` | `~/Library/Application Support/Adom Desktop/bridges-cache/native-browser/` |+| Bridge cache path | `%LOCALAPPDATA%\Adom Bridge\bridges-cache\native-browser\` | `~/Library/Application Support/Adom Bridge/bridges-cache/native-browser/` | | Extension force-install | `HKLM\...\ExtensionInstallForcelist` | configuration profile / MDM `ExtensionInstallForcelist` | | Discovery file | `os.homedir()/.adom/bridges/native-browser/host.json` | identical (uses `os.homedir()` - already cross-platform) |
dev-skills/developing-the-bridge/SKILL.md+95−41@@ -1,14 +1,14 @@ --- name: developing-the-bridge-description: "DEVELOPER skill for maintainers of the adom-native-browser (nb/abe) bridge - NOT for driving it. Read this before editing bridge/server.js, host/native-host.js, or extension/. Covers the architecture (bridge + native-messaging host + MV3 extension, and how AD provisions Node), the #1 rule that every nbrowser_* verb must return rich _hint/_next/related/pitfalls via the VERB_META table, the verb contract, local testing, the AD-vs-bridge boundary, hard-won runtime gotchas (chrome.tabs.goBack fails on debugger-driven tabs; UIA Invoke fails on Win32 dialogs), and the safety invariants. Everyday users never need this."+description: "DEVELOPER skill for maintainers of the adom-native-browser (nb/nbe) bridge - NOT for driving it. Read this before editing bridge/server.js, host/native-host.js, or extension/. Covers the architecture (bridge + native-messaging host + MV3 extension, and how ab provisions Node), the #1 rule that every nbrowser_* verb must return rich _hint/_next/related/pitfalls via the VERB_META table, the verb contract, local testing, the ab-vs-bridge boundary, hard-won runtime gotchas (chrome.tabs.goBack fails on debugger-driven tabs; UIA Invoke fails on Win32 dialogs), and the safety invariants. Everyday users never need this." --- Parent skill: **adom-native-browser**. This is a maintainer-only skill (not user-invocable). Bridge-SDK: https://wiki.adom.inc/adom/adom-desktop-bridges+SDK: https://wiki.adom.inc/adom/adom-bridge-sdk # Developing the native-browser bridge ## ⭐ THE loudest rule: the AI reads your verb OUTPUT, not this file-AD relays every field of a verb response verbatim to the calling AI. A verb that returns+ab relays every field of a verb response verbatim to the calling AI. A verb that returns `{success:true}` and nothing else makes the AI guess and retry-loop. EVERY `nbrowser_*` response MUST carry a rich **`_hint`** (what just happened + the obvious next verb + the trap) plus `_next` / `related` / `pitfalls`. This is the single highest-leverage thing in the whole bridge.@@ -19,14 +19,14 @@ top-level `_hint`/`related`/`_next` onto EVERY dispatch response. Add a verb → in the same commit, or it ships hint-less. ## The 3 components (know which one you're touching)-1. **The bridge** (`bridge/server.js`) — AD spawns it (`spawn.kind:"node"`), so **AD provisions Node**- (v1.9.63 runtime contract; you never install Node for it). Binds `127.0.0.1:0` on+1. **The bridge** (`bridge/server.js`) — ab spawns it (`spawn.kind:"node"`), so **ab provisions Node**+ (SDK runtime contract; you never install Node for it). Binds `127.0.0.1:0` on `process.env.ADOM_BIND_HOST || '127.0.0.1'` (never `0.0.0.0` → no firewall prompt). Writes a discovery file `~/.adom/bridges/native-browser/host.json` + `node-path.txt`. Serves `/status`- (health chip: led/summary/tooltip, AND a top-level `version` AD reads as `liveVersion`) + `/command`- for AD, plus a loopback TCP listener the host dials.+ (health chip: led/summary/tooltip, AND a top-level `version` ab reads as `liveVersion`) + `/command`+ for ab, plus a loopback TCP listener the host dials. 2. **The native-messaging host** (`host/native-host.js` via `native-host.bat`) — **Chrome launches it**,- NOT AD, so it does NOT inherit AD's managed-runtime PATH. It reuses AD's Node via+ NOT ab, so it does NOT inherit ab's managed-runtime PATH. It reuses ab's Node via `node-path.txt` (published by the bridge = `process.execPath`, backslash-normalized) and falls back to PATH `node`. This is the ONLY place system-Node dependence could bite a fresh machine — keep the `.bat`'s two-branch resolution intact.@@ -34,7 +34,7 @@ in the same commit, or it ships hint-less. host → bridge loopback. Deterministic ID (`manifest.json` pins `key`) so the host manifest's `allowed_origins` is known ahead of time. -Data path: container → relay → AD → bridge `/command` → loopback → host stdio → extension SW → CDP/native.+Data path: container → relay → ab → bridge `/command` → loopback → host stdio → extension SW → CDP/native. ## Hard-won runtime gotchas (extension side) - **`chrome.tabs.goBack`/`goForward` throw on tabs we drive.** On a tab navigated via@@ -47,22 +47,22 @@ Data path: container → relay → AD → bridge `/command` → loopback → hos (UIA Invoke) returns `success:true` but the dialog's OK/Select-Folder/Reload button doesn't actuate. Fall back to a foreground `desktop_click {x,y}` (SendInput) at the control's screen rect. (See installing-the-extension for the picker + Reload cases.)-- **Keyboard chords route through AD's `desktop_press_key`** (full modifier chords) - don't reimplement+- **Keyboard chords route through ab's `desktop_press_key`** (full modifier chords) - don't reimplement OS-level modifier handling in the bridge. `nbrowser_press_key` is for in-page key events (CDP). ## Verb contract Request `{command, args}`; response `{success, output(JSON string), error?, errorCode?}` + the hints. `nbrowser_readiness` is READ-ONLY (never spawns/installs) — mirror `bridge_readiness`. Long ops: return a job id + `statusVerb`, don't block. Declare per-verb budgets >60s in `bridge.json` `timeouts`-(v1.9.79) so AD doesn't cut them at 60s. `nbrowser_describe` returns the full catalog.+(v1.9.79) so ab doesn't cut them at 60s. `nbrowser_describe` returns the full catalog. ## Test locally `node -c bridge/server.js` (syntax). Run `server.js` standalone → it binds ephemeral + writes the discovery file; `GET 127.0.0.1:<port>/status` → led/summary/tooltip + `version`. End-to-end:-`nbrowser_readiness` through AD (`--target <laptop>`), then open bg window → screenshot → close.+`nbrowser_readiness` through ab (`--target <laptop>`), then open bg window → screenshot → close. **Testing on John's VMs (winvm / ADOMBASELINE):** John ALWAYS keeps an RDP session open to his VMs —-**assume it; don't pause to confirm his RDP view before driving a VM over the `--target <vm>` AD path.**+**assume it; don't pause to confirm his RDP view before driving a VM over the `--target <vm>` ab path.** Full fresh-VM onboarding = install bridge (`bridge_install {manifestUrl}`), deploy `extension/`+`host/`, write `host/host-manifest.json` (path→`native-host.bat`, `allowed_origins`→`chrome-extension://<keyed-id>/`), register the native host in HKCU (`…\Microsoft\Edge\NativeMessagingHosts` / `…\Google\Chrome\…`), then load@@ -72,20 +72,20 @@ matches regardless of load method. ## Reaching the user: NEVER notify only the local desktop The bridge can run on a **VM** while the user is on their **laptop** - a `notify_user` that fires on the-LOCAL AD then lands on an unwatched VM desktop and the user never sees it (real bug: Chrome-install UAC-toast fired on the VM, John on his laptop saw nothing). **Route user notifications cross-AD:** query+LOCAL ab then lands on an unwatched VM desktop and the user never sees it (real bug: Chrome-install UAC+toast fired on the VM, John on his laptop saw nothing). **Route user notifications cross-ab:** query `targets` via the direct-API; if any PEER desktop exists, send `notify_user`/`notify_response` with `target:"all"` (top-level field on the `/command` payload) so it reaches the user wherever they're attended; no peers (bridge on the user's own machine) = a local toast. `adCommand(app,cmd,args,target)` adds `payload.target`. The toast BODY names the host (`…on ${host}`) since the action (e.g. a UAC) is on the bridge's host but the alert is on the user's laptop - they keep an RDP session open to approve it. -## The boundary with AD (don't file bridge bugs as AD bugs)-AD owns: spawn/reap (`refresh_bridges` force-restarts a running bridge as of v1.9.76; `restart_bridge`+## The boundary with ab (don't file bridge bugs as ab bugs)+ab owns: spawn/reap (`refresh_bridges` force-restarts a running bridge as of v1.9.76; `restart_bridge` is the explicit primitive), stable port, supervisor, cache install + auto-update, status classification, the GUI card, `liveVersion`/`staleProcess` reporting. YOU own: verbs + hints, `/status`, `nbrowser_describe`, the wiki page + triggers, the manifest + zip, the skills pkg. A generic-missing capability (new lifecycle verb, relay field) → file against AD; a misbehaving `nbrowser_*` verb+missing capability (new lifecycle verb, relay field) → file against ab; a misbehaving `nbrowser_*` verb → fix here. ## Safety invariants (never regress — one wiped a user's other AI threads)@@ -117,18 +117,18 @@ days when >8MB; it also sweeps stale composited overlay icons (>30 days) from th add a new persistent artifact (log, cache, icon, temp file), you MUST add it to the janitor in the same PR - unbounded growth on the user's machine is a regression, not a nice-to-have. -## ⭐ AD self-updates rotate the control port under you (2026-07-18 incident)-The env vars AD injects at spawn (ADOM_DIRECT_API_URL/_PORT) freeze the port AD had THEN. AD+## ⭐ ab self-updates rotate the control port under you (2026-07-18 incident)+The env vars ab injects at spawn (ADOM_DIRECT_API_URL/_PORT) freeze the port ab had THEN. ab self-updates restart it on a new port while your process keeps running - and every adCommand (desktop_list_windows, desktop_taskbar, notify) starts failing. If those failures are swallowed, whole features (badge, flash, hwnd correlation) die SILENTLY for hours; a Jul-16 bridge process did exactly that until the user noticed a missing badge. Rules: (1) adCommand must re-discover from the ~/.adom/direct-api-port FILE on connection failure, retry, and cache the live base; (2) NEVER swallow an-AD-link failure silently - log the outage + recovery to the bridge log AND the activity trail (_ad_link+ab-link failure silently - log the outage + recovery to the bridge log AND the activity trail (_ab_link entries) so a missing badge is diagnosable in one query; (3) suspect stale strays: multiple node.exe-bridge processes days old = AD lost track of them - kill by PID, never by image name.+bridge processes days old = ab lost track of them - kill by PID, never by image name. -## ⭐ The graceful-degradation ladder + ABE upsell (John, 2026-07-19 - "this is gold")+## ⭐ The graceful-degradation ladder + nbe upsell (John, 2026-07-19 - "this is gold") Getting a user to actually INSTALL the extension is hard. Do not let that block the value: there is a LADDER of capability, and the bridge must serve every rung while nudging users up it. @@ -149,7 +149,7 @@ user rarely needs more - so serving this WITHOUT the install is pure value. **The upsell rule:** every rung-1/rung-2 response carries a hint that (a) states honestly what the bridge CANNOT do right now ("I opened it but I cannot see or drive it - the user must click"), and-(b) names the upgrade + its payoff ("with ABE installed I could fill this form, read the result, and+(b) names the upgrade + its payoff ("with nbe installed I could fill this form, read the result, and clean up - see installing-the-extension; it installs in about a minute"). The AI relays the upsell to the user at a NATURAL moment - never mid-auth, never nagging. Hints are how the bridge teaches; a degraded mode that fails silently or begs constantly both lose the install.@@ -159,7 +159,7 @@ Post-launch minimize (ShowWindow SW_MINIMIZE returns focus to the user automatic is the pattern - the user opens the window when THEY are ready. foreground:true stays gated behind foregroundReason + notify, exactly like the extension path. -## ⭐ Opening native browser windows without ABE — the hard lessons (John, 2026-07-19)+## ⭐ Opening native browser windows without nbe — the hard lessons (John, 2026-07-19) Spent a whole session flailing at "open a background window in the live profile" and CRASHED the user's Edge (and the AI thread running in it) FOUR times. The final root cause was small and the user named it before I did. Read this before writing any browser-launch code.@@ -216,12 +216,12 @@ user's machine at all when you can avoid it. (one process, one MainWindow) — use `EnumWindows` + `GetWindowText` to truly enumerate windows. And `$pid` is a READ-ONLY PowerShell automatic variable (the shell's own PID) — never assign to it; your "pid" readouts will be garbage. When checking "did I just kill a window", enumerate windows directly;-`desktop_list_windows` returning 0 during AD churn is "can't see", not "gone".+`desktop_list_windows` returning 0 during ab churn is "can't see", not "gone". **The honest product truth to keep surfacing:** for the user's REAL signed-in session, opening a window-via **ABE** (`chrome.windows.create` FROM INSIDE the running browser — no process launch at all) is+via **nbe** (`chrome.windows.create` FROM INSIDE the running browser — no process launch at all) is strictly safer than any external launch. Extension-free `open_os_window` is the graceful-degradation-wedge; ABE is the clean path. Say so.+wedge; nbe is the clean path. Say so. ## ⭐ The badge MUST land on the driven window, never a same-profile PWA (John, 2026-07-21) The taskbar badge is a SECURITY signal — the user has to know which exact browser window an AI is@@ -241,17 +241,17 @@ wrong-PWA bug that `touchBadge` warns about, reintroduced in the correlation hel I restarted the bridge ~20× shipping other fixes that day, so every active thread was un-badged until its next verb — expected, but verify the self-heal actually re-correlates to the RIGHT window. -## ⭐ A non-2xx from AD is a FAILURE — never treat it as success (2026-07-21, fusion-bridge report)+## ⭐ A non-2xx from ab is a FAILURE — never treat it as success (2026-07-21, fusion-bridge report) `adCommand` only treated a *connection* failure (`status === 0`) as an error. Any HTTP error (403, 500) fell through as "success" with an error body, so `chromeWindowsDetailed()` parsed no windows and returned-`[]`. Result: `nbrowser_os_windows` reported **0 windows while AD saw 21**, and — far worse — the taskbar+`[]`. Result: `nbrowser_os_windows` reported **0 windows while ab saw 21**, and — far worse — the taskbar BADGE and FLASH silently stopped working, because they go through the same call. Nothing logged.-- **Root cause: the bridge token.** AD injects `ADOM_BRIDGE_TOKEN` at spawn and **rotates it when AD- self-updates**. A long-running bridge keeps sending the stale one and AD answers **403 to every call**+- **Root cause: the bridge token.** ab injects `ADOM_BRIDGE_TOKEN` at spawn and **rotates it when ab+ self-updates**. A long-running bridge keeps sending the stale one and ab answers **403 to every call** (verified: bogus token → 403; no token → 200 + 26 windows). The token is **attribution-only**, so the correct recovery is to **DROP it and retry**, then keep going token-less (`adTokenRejected`).-- **Rules:** treat only 2xx as success; on 401/403 retry without the token; log every AD-link- outage/recovery (`_ad_link`) so degradation is never silent. AD self-updates several times a day, so+- **Rules:** treat only 2xx as success; on 401/403 retry without the token; log every ab-link+ outage/recovery (`_ab_link`) so degradation is never silent. ab self-updates several times a day, so assume the token, the port, AND the API surface can all shift under a running bridge. ## ⭐ Log every badge paint + flash (John, 2026-07-21)@@ -259,7 +259,7 @@ The taskbar badge is a security signal; a silent failure is indistinguishable fr `taskbar()` now logs an `_taskbar` activity entry for EVERY call — `hwnd`, `ops` (`overlay:composite|overlay:adom|overlay:none|flash:until_focused|progress:x`), `ok`, and the HTTP status on failure — and `nbrowser_activity` carries `hwnd`/`ops` through so you can query-`{verb:"_taskbar"}` and see exactly what was painted where and whether AD accepted it.+`{verb:"_taskbar"}` and see exactly what was painted where and whether ab accepted it. ## ⭐ A load-unpacked extension has an EMPTY manifest.name on disk (2026-07-21) `adomExtStateForProfile` matched `extensions.settings[*].manifest.name =~ /adom/`, but a **Load-unpacked**@@ -315,19 +315,19 @@ competing with them from the far edge of the screen. **The reason is never optional and never blank:** it falls back to the session's `purpose`, which registration already makes mandatory. Worst case it reads "no reason given", which is itself a useful signal that a calling thread is being sloppy. Every caption is logged (`_open_announce`) so you can audit-which thread announced what, and whether AD actually rendered it.+which thread announced what, and whether ab actually rendered it. **Do NOT force-minimize what the browser intends to surface.** Background stays the default when a genuinely new window IS created (verify `minimized:true`); when it cannot be honored, announce instead of wrestling the window manager. -## ⭐ Caller provenance: WHO asked (AD 1.9.180+, wiki #345/#351)-AD stamps `X-Adom-Caller-Thread` / `-Container` / `-Reason` on every relayed request. **Self-asserted:+## ⭐ Caller provenance: WHO asked (ab 1.9.180+, wiki #345/#351)+ab stamps `X-Adom-Caller-Thread` / `-Container` / `-Reason` on every relayed request. **Self-asserted: attribution, logging, UX, arbitration hints - NEVER authorization.** What this bridge does with it: 1. **Log it** - `logActivity` stamps the calling thread on every entry (`nbrowser_activity` returns `caller`, filterable with `callerThread`).-2. **Forward it** on every AD callback made while carrying out that thread's verb, plus- `X-Adom-Caller-Delegate: native-browser`, so AD shows `chip-fetcher tab 3 (via native-browser)`.+2. **Forward it** on every ab callback made while carrying out that thread's verb, plus+ `X-Adom-Caller-Delegate: native-browser`, so ab shows `chip-fetcher tab 3 (via native-browser)`. Self-initiated work (timers, sweeps, reapers) sends `native-browser bridge (self)` instead - never a stale identity. 3. **Arbitrate** - `sessionCaller` records the owning thread; a DIFFERENT thread driving that session@@ -337,7 +337,7 @@ attribution, logging, UX, arbitration hints - NEVER authorization.** What this b 'Adom · <thread> · <purpose>'}` so a glance at the taskbar says which conversation owns which window. **Three traps this cost me, all invisible until tested end-to-end:**-- **`args.caller` is RESERVED.** AD/the CLI inject the identity there on EVERY call. I used `args.caller`+- **`args.caller` is RESERVED.** ab/the CLI inject the identity there on EVERY call. I used `args.caller` as an activity FILTER and silently emptied the trail (8 logged, 0 returned). Use your own arg name. - **AsyncLocalStorage does NOT survive the socket reply path.** Verbs resolve through the extension's `pending` map, outside the HTTP request's async scope, so ALS is empty in `onHostFrame` - every badge@@ -348,3 +348,57 @@ attribution, logging, UX, arbitration hints - NEVER authorization.** What this b - **And the standing one:** widening what you LOG means nothing if the query PROJECTION drops the field. I have now hit that exact bug three times (`hwnd`/`ops`, then `caller`). Update the projection in the same commit as the log field.++## ⭐ bridge.json `verbs[]` is what ab can SEE, and it silently rots (John, 2026-08-08)++`bridge/bridge.json` is the manifest ab reads to learn our surface. A verb that is implemented in+`VERB_META` but missing from `verbs[]` is **invisible to every caller** even though the code is right+there and works. Nothing errors. Nothing warns. It just does not exist as far as ab is concerned.++Because the manifest was hand-maintained, **12 verbs drifted out of it**, including+`nbrowser_events` (the ONLY channel by which spawned-window and dialog events reach the AI),+`nbrowser_autologin`, `nbrowser_login_form`, `nbrowser_hover`, `nbrowser_download` and+`nbrowser_caption`. They had been shipping unusable for multiple releases. John caught it as+"update your manifest for ab to read all your latest settings", not as a bug report, because from+the outside it looks like the feature was never built.++THE RULE: never hand-edit `verbs[]`. `VERB_META` in `bridge/server.js` is the single source of+truth; regenerate the manifest from it, order included:++```bash+node -e '...read VERB_META keys, write bridge.json verbs[]...' # see the publish skill+node tools/check-guard-coverage.mjs # now FAILS on any drift+```++`tools/check-guard-coverage.mjs` gained a MANIFEST PARITY gate that fails the build on an+undeclared verb, a dead declared verb, an out-of-order list, or a `bridge.json` version that does+not match `BRIDGE_VERSION`. Adding a verb is now: write `VERB_META` → classify it in+`verbs-policy.json` → regenerate `verbs[]` → the gate goes green. Skipping the regenerate is the+failure mode, so let the gate catch it rather than trusting a checklist.++The generalisable lesson: **any file that duplicates a list the code already owns will drift.**+Either generate it or gate it. Preferably both.++## ⭐ Naming: nb is layer 1, nbe is layer 2 (John, 2026-08-08, definitions v1.9.0)++Locked, and it answers the extension display-name question:++- **nb, Adom Native Browser** is the PARENT and the layer-1 product: extension-free OS-level control.+- **nbe, Adom Native Browser Extension** is layer 2, the installed extension. **`abe` is dead.**+- **ab** = Bridge, **ah** = Hydrogen. Single-letter shorthands are gone; every shorthand is 2+ chars,+ and a would-be single letter takes the Adom `a` prefix (w→aw, h→ah, b→ab). OS variants: `ab win`,+ `ah mac`, etc.+- CLI is **`adom-bridge-cli`** in the container; `adom-bridge` is the host-OS app launcher. The+ `adom-desktop` binary is GONE, not aliased.+- pup's verbs are **`pup_*`**, renamed from `browser_*`, no aliases.++The extension's `manifest.json` name is **"Adom Native Browser Extension"** and `nbrowser_status`+reports `ext:"nbe"` (was `"adom-browser-extension"`). Per the hard-cutover policy we broke both+loudly rather than aliasing: a caller matching the old string SHOULD fail so it gets found and fixed.++WATCH OUT when sweeping names: a blanket "Adom Desktop"→"Bridge" rewrite corrupts **real+filesystem paths**. ab's data dir is `%LOCALAPPDATA%\Adom Bridge\bridges-cache\` (productName+"Adom Bridge"), NOT `\Bridge\`, and its recordings moved to `%TEMP%\adom-bridge-recordings`. The+`.ps1` installers hardcode that cache path, so they silently install into a directory ab no longer+reads. Verify every path literal against the ab source, and keep binaries (`.gif` too, not just+`.png`) out of any bulk text rewrite.
docs/DEVELOPING.md+3−3@@ -9,7 +9,7 @@ public, open-source repo. The extension lives as a wiki page + git repo at **[wiki.adom.inc/adom/adom-native-browser](https://wiki.adom.inc/adom/adom-native-browser)**. - **Fork / propose changes upstream** — use the page's **Pull Requests** tab to propose edits back, or-- **Clone the source** — grab the repo from the **Files** tab (or `adompkg install adom/adom-native-browser`),+- **Clone the source** — grab the repo from the **Files** tab (or `adom-wiki pkg install adom/adom-native-browser`), then copy the **`extension/`** folder somewhere you can edit. The folder you load into the browser is **`extension/`** — it's the one with `manifest.json`.@@ -34,7 +34,7 @@ Same idea, slightly different layout:  -## 4. Or — let adom-desktop install it for you+## 4. Or — let Bridge install it for you You don't have to click through it. Ask your Adom AI assistant to do the load-unpacked for you — it drives the `chrome://extensions` / `edge://extensions` page at the **OS level** (via `desktop_navigate`@@ -54,7 +54,7 @@ Load my local adom-native-browser from C:\path\to\extension into Chrome via deve `reload my extension`, which runs `nbrowser_dev_reload`). - The native-messaging **host** and the **`native-browser` bridge** reconnect automatically; run `nbrowser_status` to confirm (expect a ~3–6 s reconnect after a reload).-- Layout: `extension/` (the MV3 extension), `bridge/` (the adom-desktop bridge), `host/` (the+- Layout: `extension/` (the MV3 extension), `bridge/` (the native-browser bridge), `host/` (the native-messaging host). The verb surface is documented in the **`driving-the-extension`** skill. ## 6. Contribute back
docs/INSTALL-FLOW.md+39−39@@ -6,10 +6,10 @@ actually happens, step by step, the very first time?** There are several moving on two different machines, so it is worth laying out precisely. > One-paragraph version: the **container never talks to the browser extension directly**. It talks to-> **adom-desktop (AD)** on the user's laptop over the relay, using the `adom-desktop nbrowser_*` CLI.-> On first use you ask AD to install the **bridge** (one call), the user (or AD) gets the **extension**-> loaded into Chrome/Edge, AD registers the tiny **native-messaging host**, and from then on every-> action is just `adom-desktop nbrowser_*`. The container's only local artifact is the **skills** that+> **Bridge (ab)** on the user's laptop over the relay, using the `adom-bridge-cli nbrowser_*` CLI.+> On first use you ask ab to install the **bridge** (one call), the user (or ab) gets the **extension**+> loaded into Chrome/Edge, ab registers the tiny **native-messaging host**, and from then on every+> action is just `adom-bridge-cli nbrowser_*`. The container's only local artifact is the **skills** that > teach you those verbs. Nothing in the container listens for the browser; the laptop reaches back > through the relay. @@ -18,17 +18,17 @@ on two different machines, so it is worth laying out precisely. | Piece | Lives on | Role | |---|---|---| | **Your AI session + skills** | the **container** (cloud or HD/WSL2) | You. The installed skills (`driving-the-extension`, `native-browser-safety`, ...) teach you the `nbrowser_*` verbs and the safety rules. |-| **`adom-desktop` (AD)** | the user's **laptop** | The hub. Receives your verbs over the relay, routes `nbrowser_*` to the bridge, ships results back. Always present. |-| **The bridge** (`bridge/server.js`) | the **laptop** (in AD's `bridges-cache`) | AD-spawned Node server. Binds an ephemeral loopback port, exposes `/command` to AD, and a raw-TCP loopback the host dials. Handles `nbrowser_*` and delegates `wgc` recording to AD. |+| **`adom-bridge-cli` (ab)** | the user's **laptop** | The hub. Receives your verbs over the relay, routes `nbrowser_*` to the bridge, ships results back. Always present. |+| **The bridge** (`bridge/server.js`) | the **laptop** (in ab's `bridges-cache`) | ab-spawned Node server. Binds an ephemeral loopback port, exposes `/command` to ab, and a raw-TCP loopback the host dials. Handles `nbrowser_*` and delegates `wgc` recording to ab. | | **The native-messaging host** (`host/native-host.js`) | the **laptop** | A tiny stdio<->TCP pump. Chrome/Edge launch it when the extension calls `connectNative`; it reads the bridge's discovery file and connects the extension to the bridge. | | **The extension** (`extension/`) | the user's **Chrome/Edge** | The hands. MV3 service worker that runs CDP + native `chrome.*` calls inside the user's real, signed-in profile. | ``` CONTAINER (cloud or HD/WSL2) USER'S LAPTOP +---------------------------+ relay +-------------------------------------------+- | your AI session | wss://...:8765 | adom-desktop (AD) |+ | your AI session | wss://...:8765 | Bridge (ab) | | + installed skills | ============> | | routes nbrowser_* |- | `adom-desktop nbrowser_*`| | v |+ | `adom-bridge-cli nbrowser_*`| | v | | `adom-wiki ...` (install)| | bridge/server.js (ephemeral loopback) | +---------------------------+ | ^ raw-TCP loopback | | | |@@ -41,8 +41,8 @@ on two different machines, so it is worth laying out precisely. ## Endpoints: what talks to what (and what does NOT) -- **Container -> AD:** the relay, `wss://<container>.adom.cloud:8765`. You speak `adom-desktop nbrowser_*`.-- **AD -> bridge:** HTTP `POST /command` on the bridge's ephemeral `127.0.0.1` port (written to a+- **Container -> ab:** the relay, `wss://<container>.adom.cloud:8765`. You speak `adom-bridge-cli nbrowser_*`.+- **ab -> bridge:** HTTP `POST /command` on the bridge's ephemeral `127.0.0.1` port (written to a discovery file; PORTS.md-compliant, never a fixed port). - **host -> bridge:** raw TCP on a second ephemeral loopback port the bridge writes to the same discovery file. The host reads the file; the extension never learns a port.@@ -50,35 +50,35 @@ on two different machines, so it is worth laying out precisely. - **What is NOT an endpoint:** there is **no container-side port to the extension**, and the extension **never opens a socket**. An MV3 extension cannot host a port, and per PORTS.md no fixed port survives Windows HNS anyway, so native messaging is the only compliant transport. The laptop reaches- back to the container through the same relay the rest of AD uses.+ back to the container through the same relay the rest of ab uses. So to your earlier question: **no, the container does not run a CLI that talks to the extension.** It-runs `adom-desktop`, and AD owns the rest of the chain.+runs `Bridge`, and ab owns the rest of the chain. ## First-time install, step by step (through your first useful action) ### 0. Discovery (before anything is installed) The user says something like "drive my real Chrome" / "log into DigiKey for me" / "control my browser." The wiki discovery layer matches that intent to this page via its `discovery_triggers`-(`adom-wiki discover find "..."`), so you find **adom/adom-browser-extension** without it being+(`adom-wiki discover find "..."`), so you find **adom/adom-native-browser** without it being pre-installed. ### 1. Install the package (skills land in the container) ```-adom-wiki pkg install adom/adom-browser-extension+adom-wiki pkg install adom/adom-native-browser ``` This runs `install.sh`, which copies the bundled skills into `~/.claude/skills/` **in the container**. Now your session knows the `nbrowser_*` surface and the safety doctrine. (Details: see *How skills propagate*, below.) This step is container-local; it does not touch the laptop. -### 2. Install the bridge on the laptop (one AD call)+### 2. Install the bridge on the laptop (one ab call) ```-adom-desktop bridge_install '{"manifestUrl":"https://wiki.adom.inc/api/pages/adom/adom-browser-extension/files/native-browser-bridge-manifest.json"}'+adom-bridge-cli bridge_install '{"manifestUrl":"https://wiki.adom.inc/api/pages/adom/adom-native-browser/files/native-browser-bridge-manifest.json"}' ```-AD downloads the bridge zip the manifest points to, sha256-verifies it, and extracts it into-`%LOCALAPPDATA%\Adom Desktop\bridges-cache\native-browser\`. AD's registry auto-discovers it and-routes the `nbrowser_` prefix to it. **You only do this once** - AD persists the manifest URL and-**auto-updates** the bridge on launch and every 4 hours (AD 1.9.26+), so future bridge versions land+ab downloads the bridge zip the manifest points to, sha256-verifies it, and extracts it into+`%LOCALAPPDATA%\Adom Bridge\bridges-cache\native-browser\`. ab's registry auto-discovers it and+routes the `nbrowser_` prefix to it. **You only do this once** - ab persists the manifest URL and+**auto-updates** the bridge on launch and every 4 hours (ab 1.9.26+), so future bridge versions land without a reinstall. ### 3. Register the native-messaging host (laptop, no admin)@@ -91,19 +91,19 @@ by `install/install.ps1`, which: the Edge key with `-Edge`). HKCU means **no UAC prompt**. ### 4. Get the extension into Chrome and/or Edge-This is the piece that is **not** done by the bridge, and - important - **not built into adom-desktop-either**. AD deliberately stays **generic**: it exposes only OS-automation primitives+This is the piece that is **not** done by the bridge, and - important - **not built into Bridge+either**. ab deliberately stays **generic**: it exposes only OS-automation primitives (`desktop_navigate`, `desktop_find_control`, `desktop_ui_click`, `desktop_ui_set`, `desktop_screenshot`). The **extension-specific dev-mode install is orchestrated by code WE own in this repo** (the `installing-the-extension` + `driving-the-desktop` skills), which drive those generic verbs-to perform the load-unpacked. AD provides the clicks; we own the recipe. Three ways the extension gets+to perform the load-unpacked. ab provides the clicks; we own the recipe. Three ways the extension gets loaded: - **Chrome Web Store / Edge Add-ons (production) - but ~2 months out.** The user installs it (or a managed policy force-installs it by store ID); required for silent install on a normal, non-enterprise machine. Store review is slow (budget ~2 months for the broad-permission set), **so until then, dev-mode load-unpacked below is the day-one install path for real users.**-- **Dev-mode "Load unpacked", driven by our own orchestration over AD's generic verbs.** Our installer+- **Dev-mode "Load unpacked", driven by our own orchestration over ab's generic verbs.** Our installer (see `installing-the-extension`) opens `chrome://extensions` / `edge://extensions` in a background window, toggles **Developer mode**, clicks **Load unpacked**, fills the native folder picker to the `extension/` folder, then **verifies the card loaded + captures the extension ID**, syncs the host@@ -134,14 +134,14 @@ and (after install) the **Adom** card loaded clean: Enabled, the pinned ID, an a errors, so always read it. (Tip: after `desktop_navigate`, verify the page actually rendered - the tab title becomes "Extensions" - not just that the omnibox shows the URL.) -+ **2. The Load-unpacked folder picker, pointed at the `extension/` folder** (`icons`, `src`, Folder = "extension"). It opens in an arbitrary last-used directory, so the agent **brings it to the front and types the path** (`bring_to_front` -> `ctrl+l` -> type -> Enter), then **screenshot-verifies the path before** clicking **Select Folder** - the wrong folder loads a broken/old build silently: -+ Chrome and Edge are independent: load it in whichever the user wants, or both. The host is registered per browser (step 3), so each browser the extension runs in connects its own host.@@ -152,8 +152,8 @@ window in a driven profile), it calls `connectNative('inc.adom.native_browser')` host, the host reads the bridge discovery file and dials the bridge's loopback, and sends a `hello` with the profile id. **That is the moment the bridge knows the extension is live.** Check it: ```-adom-desktop nbrowser_status # ext connected? which browser/profile?-adom-desktop nbrowser_profiles # every BROWSER x PROFILE you can drive+adom-bridge-cli nbrowser_status # ext connected? which browser/profile?+adom-bridge-cli nbrowser_profiles # every BROWSER x PROFILE you can drive ``` If `nbrowser_status` says not connected, the usual cause is that the profile has **no open window** (an MV3 service worker sleeps when its profile has no windows). Opening a window wakes it. The bridge@@ -162,13 +162,13 @@ knows the extension is **connected**; it cannot see an installed-but-asleep exte ### 6. Your first useful action Always work in a window **you** open (never the user's existing ones - see `native-browser-safety`): ```-adom-desktop nbrowser_open_window '{"url":"https://www.digikey.com","focused":false}' # background; returns a sessionId-adom-desktop nbrowser_screenshot '{"sessionId":"<id>","maxWidth":1500}' # see the page without foregrounding it-adom-desktop nbrowser_credentials '{"match":"digikey"}' # is there a saved login?+adom-bridge-cli nbrowser_open_window '{"url":"https://www.digikey.com","focused":false}' # background; returns a sessionId+adom-bridge-cli nbrowser_screenshot '{"sessionId":"<id>","maxWidth":1500}' # see the page without foregrounding it+adom-bridge-cli nbrowser_credentials '{"match":"digikey"}' # is there a saved login? # ...click the sign-in field (Chrome autofills), submit, then fetch the CAD file behind the login.-adom-desktop nbrowser_close_window '{"sessionId":"<id>"}'+adom-bridge-cli nbrowser_close_window '{"sessionId":"<id>"}' ```-That round-trip - container -> relay -> AD -> bridge -> host -> extension -> back - is the whole point:+That round-trip - container -> relay -> ab -> bridge -> host -> extension -> back - is the whole point: it runs in the user's real, signed-in profile, so cookies/SSO/saved-logins/captcha all just work. ## Who installs what (the short answer to "does the bridge install the extension?")@@ -176,18 +176,18 @@ it runs in the user's real, signed-in profile, so cookies/SSO/saved-logins/captc | Component | Installed by | Notes | |---|---|---| | Skills (in the container) | `adom-wiki pkg install` -> `install.sh` | Container-local; this is all the container keeps. |-| Bridge (laptop cache) | `adom-desktop bridge_install` (once) | Auto-updates after that. |+| Bridge (laptop cache) | `adom-bridge-cli bridge_install` (once) | Auto-updates after that. | | Native-messaging host (HKCU) | `install/install.ps1` (or the golden-image bake) | Per-browser; no UAC. |-| Extension (in Chrome/Edge) | Web Store / managed policy, **or** AD-driven `Load unpacked`, **or** the user | **Not the bridge.** The bridge is the runtime server; it does not push the extension into the browser. |+| Extension (in Chrome/Edge) | Web Store / managed policy, **or** ab-driven `Load unpacked`, **or** the user | **Not the bridge.** The bridge is the runtime server; it does not push the extension into the browser. | **No, the bridge does not install the extension into the browser**, and it does not need to: the-extension arrives via the store or via AD driving `chrome://extensions`, and the bridge only ever+extension arrives via the store or via ab driving `chrome://extensions`, and the bridge only ever *talks to* it once it connects. -## After the first install: it is all `adom-desktop nbrowser_*`+## After the first install: it is all `adom-bridge-cli nbrowser_*` Once the four laptop pieces exist (bridge + host + extension + the connection), the container side is-just the `adom-desktop nbrowser_*` CLI plus the skills that teach it. No re-install per task. The+just the `adom-bridge-cli nbrowser_*` CLI plus the skills that teach it. No re-install per task. The bridge auto-updates; the extension auto-updates from the store (or you re-`Load unpacked` in dev). ## How skills propagate@@ -223,7 +223,7 @@ the site, the selectors, the multi-step flow, and the gotchas - written so an AI ## Honest status (what is live vs pending) -- **Live:** the container -> relay -> AD -> bridge -> host -> extension round-trip, the `nbrowser_*`+- **Live:** the container -> relay -> ab -> bridge -> host -> extension round-trip, the `nbrowser_*` surface, `bridge_install` + auto-update, HKCU host registration, load-unpacked in Chrome and Edge, skill install via the package. - **Pending:** Chrome Web Store / Edge Add-ons publication (required for silent install on normal,
docs/UNINSTALL-FLOW.md+6−6@@ -1,8 +1,8 @@ # Uninstalling the extension (the background-first process) -How the Adom browser extension is removed from the user's real Chrome/Edge. Like install, this is **our**-orchestration over adom-desktop's **generic** OS verbs (`desktop_navigate`, `desktop_find_control`,-`desktop_ui_click`, `desktop_ui_set`, `desktop_screenshot_window`) - AD has no "uninstall extension"+How the Adom Native Browser Extension is removed from the user's real Chrome/Edge. Like install, this is **our**+orchestration over Bridge's **generic** OS verbs (`desktop_navigate`, `desktop_find_control`,+`desktop_ui_click`, `desktop_ui_set`, `desktop_screenshot_window`) - ab has no "uninstall extension" feature. Full step detail + recovery lives in the **`uninstalling-the-extension`** skill; this page is the walkthrough with screenshots. @@ -27,7 +27,7 @@ Set the search box (background `desktop_ui_set {contains:"Search extensions", te Adom card remains. **Screenshot and confirm exactly one card shows** before going further - this is what makes the Remove button unambiguous: -+ ## Step 3 - Remove + confirm (the one brief, narrated foreground beat) The confirm bubble (`Remove "Adom for Chrome & Edge"?`) is a Chromium **owned popup that dies on focus@@ -37,7 +37,7 @@ unavoidable, so caption the user ("one moment, please don't type"), then do it f role:"button", name:"Remove"}` (confirm). Do **not** insert a screenshot between trigger and confirm - the bubble auto-closes in ~1-2s. -+ To photograph the bubble (as above) without removing anything, do a separate pass: trigger Remove, then `desktop_screenshot_window {hwnd:<browser>}` - the bubble rides back in the response's `screenshots[]`@@ -52,7 +52,7 @@ array (it is an owned popup) - then click **Cancel** instead of Remove. - **Edge:** identical via `edge://extensions`; verify the `edge:` key drops. - **Just unload vs full decommission:** removing the card unloads the extension. To fully remove it, also delete the HKCU native-messaging host (`...\NativeMessagingHosts\inc.adom.native_browser`, plus the Edge- key) and, if wanted, `adom-desktop bridge_uninstall {name:"native-browser"}`. For "take it out of my+ key) and, if wanted, `adom-bridge-cli bridge_uninstall {name:"native-browser"}`. For "take it out of my browser," Steps 1-4 are enough. ## Reinstall
extension/manifest.json+4−4@@ -1,9 +1,9 @@ { "manifest_version": 3,- "name": "Adom for Chrome & Edge",+ "name": "Adom Native Browser Extension", "short_name": "Adom",- "version": "0.1.3",- "description": "Drive your real, logged-in Chrome/Edge from the Adom ecosystem \u2014 the native-browser counterpart to pup.",+ "version": "0.2.0",+ "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": { "service_worker": "src/sw.js",@@ -43,4 +43,4 @@ "48": "icons/icon48.png", "128": "icons/icon128.png" }-}+}
extension/src/cdp.js+2−2@@ -1,4 +1,4 @@-// adom-browser-extension :: CDP helpers (chrome.debugger)+// adom-native-browser :: CDP helpers (chrome.debugger) // -------------------------------------------------------- // Used ONLY for verbs that need DevTools Protocol parity with pup: full-page screenshots and // arbitrary expression eval. Navigation/tabs/windows go through native chrome.* APIs (native.js) so@@ -933,7 +933,7 @@ export async function setFileInput(tabId, args) { const s = _chooserServed.get(Number(tabId)) || null; return { ok: true, armed: _chooserArm.has(Number(tabId)), served: !!s, ...(s ? { files: s.files, servedAgoMs: Date.now() - s.ts } : {}), _hint: s ? 'The intercepted file chooser was satisfied with the staged files.' : (_chooserArm.has(Number(tabId)) ? 'Still armed - click the Browse/Upload control now (nbrowser_click), then re-check.' : 'Not armed and nothing served. Arm with {armForNextChooser:true, filePaths} first.') }; }- if (!files.length) return { ok: false, errorCode: 'no_files', error: 'filePaths is empty', _hint: 'Pass filePaths:["C:/abs/path/on/the/laptop", ...]. Push files to the laptop first with adom-desktop send_files (or use files:[{name,contentBase64}] - the bridge stages them for you).' };+ if (!files.length) return { ok: false, errorCode: 'no_files', error: 'filePaths is empty', _hint: 'Pass filePaths:["C:/abs/path/on/the/laptop", ...]. Push files to the laptop first with adom-bridge-cli send_files (or use files:[{name,contentBase64}] - the bridge stages them for you).' }; if (args.armForNextChooser === true) { await send(tabId, 'Page.setInterceptFileChooserDialog', { enabled: true }); const prev = _chooserArm.get(Number(tabId)); if (prev && prev.timer) clearTimeout(prev.timer);
extension/src/native.js+6−6@@ -1,4 +1,4 @@-// adom-browser-extension :: native-API verbs (chrome.tabs / chrome.windows / downloads / fetch)+// adom-native-browser :: native-API verbs (chrome.tabs / chrome.windows / downloads / fetch) // -------------------------------------------------------------------------------------------- // No debugger attach → no banner. A "session" maps to a real Chrome window; tabId in our API is the // real chrome tab id (stringified). The session→window map is PERSISTED to chrome.storage.local so it@@ -338,10 +338,10 @@ const FOREGROUND_JUSTIFY_HINT = 'password dropdown). Background is where automation SUCCEEDS (the user keeps working; screenshots, eval, clicks, ' + 'and typing all work on a background window). To genuinely foreground you must: (1) pass foregroundReason:"<why ' + 'the user must see this, or which browser/auth feature requires a foreground window>", and (2) FIRST warn the ' +- 'user with an AD notify (adom-desktop notify_user {message:"Adom is about to bring a browser window forward for <reason>"}). ' ++ 'user with a Bridge notify (adom-bridge-cli notify_user {message:"Adom is about to bring a browser window forward for <reason>"}). ' + 'Only then set userRequestedForeground:true + foregroundReason.'; // Note on the response when a raise IS granted — remind the AI it should have notified the user.-const FOREGROUND_GRANTED_HINT = 'Raised to the FOREGROUND (you justified it). This interrupts the user — make sure you sent an AD notify first, do the ONE thing you needed the foreground for, then return to the background so you stop capturing their focus.';+const FOREGROUND_GRANTED_HINT = 'Raised to the FOREGROUND (you justified it). This interrupts the user — make sure you sent a Bridge notify first, do the ONE thing you needed the foreground for, then return to the background so you stop capturing their focus.'; // Attach the teaching warning (unjustified → stayed background) or log the justified raise. Call on every // foreground-capable verb's result. function foregroundNote(args, res, sid) {@@ -503,7 +503,7 @@ export async function openWindow(args = {}) { const res = { ok: true, sessionId: sid, tabId: String(tab.id), windowId: win.id, url: tab.url || args.url, title: tab.title, bounds, _hint: 'New window opened in the BACKGROUND — the user\'s focus is deliberately NOT stolen. ' + POLITE_HINT +- ' Screenshot it where it sits with nbrowser_screenshot (CDP) or AD desktop_screenshot_window (WGC); both capture a background/occluded window, so NEVER foreground it to grab a shot. To raise it you must justify it (userRequestedForeground:true + foregroundReason) AND warn the user first. Remember to nbrowser_close_window {sessionId:"' + sid + '"} when done so you don\'t leave abandoned windows.',+ ' Screenshot it where it sits with nbrowser_screenshot (CDP) or ab desktop_screenshot_window (WGC); both capture a background/occluded window, so NEVER foreground it to grab a shot. To raise it you must justify it (userRequestedForeground:true + foregroundReason) AND warn the user first. Remember to nbrowser_close_window {sessionId:"' + sid + '"} when done so you don\'t leave abandoned windows.', }; return foregroundNote(args, withWarn(res, warn), sid); }@@ -673,7 +673,7 @@ export async function cleanup(args = {}) { _hint: `Closed ${closed.length} spawned window(s)/tab(s) for session '${sid}'. Your primary window (if any) is KEPT and the user's own windows were never touched. At task end also nbrowser_close_window {sessionId:"${sid}"} (or use includeSpawned:true) to close your primary window.` }; } -// nbrowser_force_close — LAST-RESORT override for a TRUE ORPHAN (a window ABE cannot attribute to any live+// nbrowser_force_close — LAST-RESORT override for a TRUE ORPHAN (a window nbe cannot attribute to any live // session) or the CALLING session's own window. Two-step attestation: call 1 returns a challenge ticket + // exactly what will be destroyed; call 2 with the ticket + attestation destroys it. It can NEVER close a // window owned by a DIFFERENT live thread — cross-thread protection is absolute and not overridable.@@ -963,7 +963,7 @@ export async function download(args = {}) { // ---- demo aids: on-page captions (subtitle | alert) + bring-to-front ------- // Injected into the page (self-contained; chrome.scripting serializes via toString()). // style 'subtitle' = clean bottom narration (HD / web-hydrogen feel);-// style 'alert' = red PULSING attention banner (AD feel). pulse via Web Animations API+// style 'alert' = red PULSING attention banner (ab feel). pulse via Web Animations API // so page CSP can't block it. function injectCaption(o) { var id = '__adom_caption__';
extension/src/popup.js+2−2@@ -6,12 +6,12 @@ async function renderStatus() { const detail = document.getElementById('detail'); if (connected) { dot.classList.add('on');- status.textContent = 'Connected to adom-desktop';+ status.textContent = 'Connected to Bridge'; detail.textContent = since ? `since ${new Date(since).toLocaleTimeString()}` : ''; } else { dot.classList.remove('on'); status.textContent = 'Not connected';- detail.textContent = 'Is adom-desktop running with the native-browser bridge installed?';+ detail.textContent = 'Is Bridge running with the native-browser bridge installed?'; } } renderStatus();
extension/src/record.js+1−1@@ -3,7 +3,7 @@ // 🎥 DOCTRINE — why it's built this way (full detail: the native-browser-recording skill + README "Recording"): // GOAL: demos must look AMAZING → full-res, HIGH FRAMERATE (20–60fps). The enemy is PAINT-THROTTLING, not the // codec: Chrome correctly stops painting occluded/background/static windows (battery) → capture freezes/drops fps.-// (CDP screencast is NOT garbage — it's pup's browser_record_start engine, 20–30fps when the page paints.)+// (CDP screencast is NOT garbage — it's pup's pup_record_start engine, 20–30fps when the page paints.) // The NATIVE-browser methods here are 'wgc' + 'getdisplaymedia' (real high-fps engines). Background capture of // the user's REAL Chrome (the only browser with their logins) needs it to PAINT while occluded — which means // launching it with the occlusion/throttle-disable flags (--disable-features=CalculateNativeWinOcclusion
extension/src/sw.js+3−3@@ -1,6 +1,6 @@-// adom-browser-extension :: MV3 service worker+// adom-native-browser :: MV3 service worker // ----------------------------------------------// Holds the native-messaging port to the AD bridge (via the native host), routes nbrowser_* commands+// Holds the native-messaging port to the ab bridge (via the native host), routes nbrowser_* commands // to native.js (chrome.tabs/windows — no banner) or cdp.js (chrome.debugger — screenshot/eval), and // answers the host's keepalive ping (which keeps this service worker resident). @@ -150,7 +150,7 @@ async function dispatch(verb, args) { // --- meta ------------------------------------------------------------- case 'nbrowser_status': { const own = native.ownershipSummary(); // dirty_session nag: surfaced while owned/spawned windows remain- return { ok: true, ext: 'adom-browser-extension', version: VERSION, engine: 'native', browser: detectBrowser(), ownedWindows: own.ownedWindows, spawnedWindows: own.spawnedWindows, sessions: own.sessions, _hint: own._nag };+ return { ok: true, ext: 'nbe', version: VERSION, engine: 'native', browser: detectBrowser(), ownedWindows: own.ownedWindows, spawnedWindows: own.spawnedWindows, sessions: own.sessions, _hint: own._nag }; } case 'nbrowser_whoami': { // Force a fresh Google-email resolve + RE-announce, so the bridge re-keys this connection to
host/host-manifest.template.json+1−1@@ -1,6 +1,6 @@ { "name": "inc.adom.native_browser",- "description": "Adom native-browser bridge host (stdio <-> adom-desktop)",+ "description": "Adom native-browser bridge host (stdio <-> Bridge)", "path": "__ABSOLUTE_PATH_TO__\\native-host.bat", "type": "stdio", "allowed_origins": [
host/native-host.bat+3−3@@ -1,11 +1,11 @@ @echo off REM Chrome/Edge launch this as the native-messaging host "path". It runs the Node stdio<->bridge pump. REM-REM Node resolution order (Chrome does NOT inherit adom-desktop's managed-runtime PATH, so we must NOT+REM Node resolution order (Chrome does NOT inherit Bridge's managed-runtime PATH, so we must NOT REM assume `node` is on PATH on a machine with no system Node install):-REM 1. node-path.txt published by the AD-spawned bridge = the EXACT Node adom-desktop provisioned+REM 1. node-path.txt published by the ab-spawned bridge = the EXACT Node Bridge provisioned REM (system, or its portable no-UAC copy). Self-healing: the bridge rewrites it on every respawn,-REM so an AD runtime upgrade never leaves us pointing at a dead node.exe.+REM so a Bridge runtime upgrade never leaves us pointing at a dead node.exe. REM 2. `node` from PATH (a system install), as a fallback. set "NODE_EXE=" set "NPFILE=%USERPROFILE%\.adom\bridges\native-browser\node-path.txt"
host/native-host.js+3−3@@ -1,7 +1,7 @@ #!/usr/bin/env node 'use strict'; /*- * adom-browser-extension :: native-messaging host+ * adom-native-browser :: native-messaging host * ------------------------------------------------ * Chrome/Edge launch this process when the extension calls * chrome.runtime.connectNative('inc.adom.native_browser')@@ -151,11 +151,11 @@ function detectBrowserFromTree() { let bridge = null; let inbuf = Buffer.alloc(0); // ---- saved-password decryption (runs in CHROME's REAL context) ---------------------------------------// The AD-spawned bridge is sandboxed: its DPAPI master-key scope differs from John's real profile, so it+// The ab-spawned bridge is sandboxed: its DPAPI master-key scope differs from John's real profile, so it // gets "parameter is incorrect" on Chrome's blobs (verified 2026-06-23). THIS host is spawned by Chrome, // so it shares Chrome's DPAPI scope and CAN decrypt. SECURITY: the plaintext only ever flows DOWNSTREAM // to the extension via stdio (this same process → Chrome → extension); it never goes back to the bridge,-// AD, the relay, or the AI. Only {ok,loggedIn,...} returns upstream.+// ab, the relay, or the AI. Only {ok,loggedIn,...} returns upstream. const crypto = require('crypto'); const { execFileSync } = require('child_process'); const LOCALAPP = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
install.sh+1−1@@ -17,4 +17,4 @@ for HOME_DIR in "${HOME}/.claude/skills" "${HOME}/.codex/skills"; do done echo "installed ${SKILLS// /, } -> $HOME_DIR" done-echo "Skills installed to Claude + Codex homes. Nothing else lands here: the bridge runtime auto-installs on your desktop via Adom Desktop, and your AI can install the browser extension into Chrome/Edge for you - just ask it."+echo "Skills installed to Claude + Codex homes. Nothing else lands here: the bridge runtime auto-installs on your desktop via Bridge, and your AI can install the browser extension into Chrome/Edge for you - just ask it."
install/README.md+8−8@@ -2,8 +2,8 @@ Three registrations on a managed Windows machine (golden image / HD / winvm): -1. **Bridge → AD `bridges-cache`** — `bridge/` copied to `%LOCALAPPDATA%\Adom Desktop\bridges-cache\- native-browser\`; adom-desktop's registry auto-discovers it and routes `nbrowser_*` to it.+1. **Bridge → ab `bridges-cache`** — `bridge/` copied to `%LOCALAPPDATA%\Adom Bridge\bridges-cache\+ native-browser\`; Bridge's registry auto-discovers it and routes `nbrowser_*` to it. 2. **Native-messaging host (HKCU, no UAC)** — render `host-manifest.json` from the template (absolute `native-host.bat` path + the extension ID in `allowed_origins`) and register it under `HKCU\Software\<vendor>\NativeMessagingHosts\inc.adom.native_browser` for Chrome and/or Edge.@@ -11,23 +11,23 @@ Three registrations on a managed Windows machine (golden image / HD / winvm): `install.ps1` does all three. **Run it OUTSIDE Claude Code.** -## ⚠ The wcifs / AppContainer trap (read adom-desktop/CLAUDE.md)+## ⚠ The wcifs / AppContainer trap (read adom-bridge/CLAUDE.md) -For Claude-spawned tooling, `%LOCALAPPDATA%\Adom Desktop\...` is silently redirected to a per-package+For Claude-spawned tooling, `%LOCALAPPDATA%\Adom Bridge\...` is silently redirected to a per-package shadow — PowerShell, `Start-Process`, and the Write/Edit tool all hit the shadow, not the real path. So **from a Claude session, do the bridges-cache copy via WSL2 `cp`** (bypasses wcifs): ```bash-cp -r bridge/* "/c/Users/<user>/AppData/Local/Adom Desktop/bridges-cache/native-browser/"+cp -r bridge/* "/c/Users/<user>/AppData/Local/Adom Bridge/bridges-cache/native-browser/" ``` -…or via `adom-desktop shell_execute`. The HKCU/HKLM registry writes and the `.bat`/manifest writes+…or via `adom-bridge-cli shell_execute`. The HKCU/HKLM registry writes and the `.bat`/manifest writes (under this repo) are NOT in the trapped paths and are fine from PowerShell run outside Claude. ## Better long-term: wiki-streamed bridge -adom-desktop already streams bridges from the wiki (`bridge_cache.rs` / `WIKI_STATIC_BASE`). Publishing-`bridge/` as a wiki package lets AD pull + cache it with no manual copy — the preferred delivery once+Bridge already streams bridges from the wiki (`bridge_cache.rs` / `WIKI_STATIC_BASE`). Publishing+`bridge/` as a wiki package lets ab pull + cache it with no manual copy — the preferred delivery once this stabilizes. See the plan's distribution section. ## update.xml
install/ab-safety-check.ps1added+67@@ -0,0 +1,67 @@+<#+ adom-native-browser :: ab-safety neutralize + verify+ -------------------------------------------------------+ Run this LOCALLY on the Windows laptop (NOT through Bridge) BEFORE re-opening ab, after the+ 2026-06-19 port-exhaustion incident. No admin / UAC needed — it only touches HKCU + the user profile.++ It ONLY touches adom-native-browser artifacts:+ - the native-messaging host registration named 'inc.adom.native_browser' (Chrome + Edge)+ - the 'native-browser' bridge dir in ab's bridges-cache+ - the bridge discovery/pidfiles under ~/.adom/bridges/native-browser+ - node.exe processes running native-host.js+ Nothing else on the machine is read or modified.++ Removing the host registration + the cached bridge makes the runaway loop PHYSICALLY impossible:+ Chrome/Edge can no longer spawn the host, and ab can no longer auto-spawn the bridge.++ Usage (PowerShell, normal window — no admin):+ powershell -ExecutionPolicy Bypass -File ab-safety-check.ps1+#>+$ErrorActionPreference = 'SilentlyContinue'+$HostName = 'inc.adom.native_browser'+$pending = 0+Write-Host "=== adom-native-browser safety check $(Get-Date) ===" -ForegroundColor Cyan++# 1) KILL-SWITCH: native-messaging host registration (Chrome + Edge).+# Without this, connectNative() cannot launch a host -> the loop cannot start.+foreach ($v in 'Google\Chrome','Microsoft\Edge') {+ $key = "HKCU:\Software\$v\NativeMessagingHosts\$HostName"+ if (Test-Path $key) {+ Write-Host "[FOUND] host registration: $key -> removing" -ForegroundColor Yellow+ Remove-Item $key -Force+ } else { Write-Host "[clean] no host registration under $v" -ForegroundColor Green }+}++# 2) Bridge files in ab's cache (so ab cannot auto-spawn the broken bridge on startup).+$cache = Join-Path $env:LOCALAPPDATA 'Adom Bridge\bridges-cache\native-browser'+if (Test-Path $cache) {+ Write-Host "[FOUND] bridge in cache: $cache -> removing" -ForegroundColor Yellow+ Remove-Item $cache -Recurse -Force+} else { Write-Host "[clean] no native-browser bridge in bridges-cache" -ForegroundColor Green }++# 3) Discovery file + pidfile (host.json / host.pid).+$bdir = Join-Path $env:USERPROFILE '.adom\bridges\native-browser'+if (Test-Path $bdir) {+ Write-Host "[FOUND] discovery dir: $bdir -> removing" -ForegroundColor Yellow+ Get-ChildItem $bdir | ForEach-Object { Write-Host " $($_.Name)" }+ Remove-Item $bdir -Recurse -Force+} else { Write-Host "[clean] no discovery/pidfiles" -ForegroundColor Green }++# 4) Running looping hosts (node.exe -> native-host.js).+$hosts = Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Where-Object { $_.CommandLine -like '*native-host.js*' }+if ($hosts) {+ Write-Host "[FOUND] $($hosts.Count) native-host process(es) running -> killing" -ForegroundColor Yellow+ $hosts | ForEach-Object { Write-Host " pid $($_.ProcessId)"; Stop-Process -Id $_.ProcessId -Force }+} else { Write-Host "[clean] no native-host.js processes running" -ForegroundColor Green }++# 5) Socket health (did the machine recover from the WSAENOBUFS / 10055 exhaustion?).+$tcp = (Get-NetTCPConnection).Count+$node = (Get-CimInstance Win32_Process -Filter "Name='node.exe'").Count+Write-Host "[info] total TCP connections: $tcp (healthy idle machine is typically a few hundred)"+Write-Host "[info] total node.exe processes: $node"++Write-Host "`n=== RESULT ===" -ForegroundColor Cyan+Write-Host "All [clean]/[info] -> SAFE to open ab:" -ForegroundColor Green+Write-Host " * Chrome/Edge can no longer spawn the host (registration removed)"+Write-Host " * ab can no longer auto-spawn the bridge (cache removed)"+Write-Host "Open ab, let it reconnect to galliaApril, then tell Claude 'ab is up'."
install/install.ps1+8−8@@ -1,9 +1,9 @@ <#- adom-browser-extension :: Windows install (managed machine / golden-image bake)+ adom-native-browser :: Windows install (managed machine / golden-image bake) --------------------------------------------------------------------------------- Registers the three pieces. Run OUTSIDE Claude Code (the %LOCALAPPDATA%\Adom Desktop path is- AppContainer/wcifs-virtualized for Claude-spawned tools — see adom-desktop/CLAUDE.md). From a- Claude session, do the bridges-cache copy via WSL2 `cp` or `adom-desktop shell_execute` instead.+ Registers the three pieces. Run OUTSIDE Claude Code (the %LOCALAPPDATA%\Adom Bridge path is+ AppContainer/wcifs-virtualized for Claude-spawned tools — see Bridge/CLAUDE.md). From a+ Claude session, do the bridges-cache copy via WSL2 `cp` or `adom-bridge-cli shell_execute` instead. Usage: powershell -ExecutionPolicy Bypass -File install.ps1 -ExtensionId <id> [-Edge]@@ -19,8 +19,8 @@ $repo = Split-Path -Parent $PSScriptRoot $hostJs = Join-Path $repo 'host\native-host.bat' $HostName = 'inc.adom.native_browser' -# 1) Bridge -> AD bridges-cache (auto-discovered by adom-desktop's registry)-$cache = Join-Path $env:LOCALAPPDATA 'Adom Desktop\bridges-cache\native-browser'+# 1) Bridge -> ab bridges-cache (auto-discovered by Bridge's registry)+$cache = Join-Path $env:LOCALAPPDATA 'Adom Bridge\bridges-cache\native-browser' New-Item -ItemType Directory -Force -Path $cache | Out-Null Copy-Item -Recurse -Force (Join-Path $repo 'bridge\*') $cache Write-Host "bridge -> $cache"@@ -28,7 +28,7 @@ Write-Host "bridge -> $cache" # 2) Native-messaging host manifest (rendered from template) + HKCU registration (no UAC) $manifest = @{ name = $HostName- description = 'Adom native-browser bridge host (stdio <-> adom-desktop)'+ description = 'Adom native-browser bridge host (stdio <-> Bridge)' path = $hostJs type = 'stdio' allowed_origins = @("chrome-extension://$ExtensionId/")@@ -57,4 +57,4 @@ if ($UpdateXmlUrl -ne "") { if ($Edge) { Force-Install 'HKLM:\SOFTWARE\Policies\Microsoft\Edge\ExtensionInstallForcelist' } } -Write-Host "done. Restart the browser; the extension popup should show 'Connected to adom-desktop'."+Write-Host "done. Restart the browser; the extension popup should show 'Connected to Bridge'."
native-browser-bridge-manifest.json+5−5@@ -1,8 +1,8 @@ { "manifest_version": 1,- "version": "0.1.55",- "url": "https://wiki.adom.inc/download/adom/adom-native-browser/0.1.55/native-browser-bridge-v0.1.55.zip",- "sha256": "e33b88161ddbfd072d8d558b73e6e0a9a8ebc0afcd326ec00db94733559ac1fe",- "size": 99231,- "released_at": "2026-08-05T00:00:00Z"+ "version": "0.2.0",+ "url": "https://wiki.adom.inc/download/adom/adom-native-browser/0.2.0/native-browser-bridge-v0.2.0.zip",+ "sha256": "2fe7d7dda61b5f84cde92208664e20562d85797479cc7991009899b0724ecb3f",+ "size": 99359,+ "released_at": "2026-08-08T16:33:56Z" }
package.json+7−3@@ -1,6 +1,6 @@ { "slug": "adom-native-browser",- "version": "0.4.17",+ "version": "0.5.0", "type": "app", "title": "Adom Native Browser", "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.",@@ -27,9 +27,12 @@ "uninstall": "./uninstall.sh" }, "dependencies": {- "adom/adom-desktop": "^1.9.47"+ "adom/adom-bridge": "^2.0.3" }, "tags": [+ "nb",+ "nbe",+ "native-browser", "browser", "extension", "chrome",@@ -37,7 +40,8 @@ "nbrowser", "pup", "native-messaging",- "adom-desktop",+ "bridge",+ "ab", "credentials", "chip-fetcher" ],
page.json+17−6@@ -4,6 +4,10 @@ "summary": "Your AI's hands in your real, signed-in Chrome & Edge. The browser extension that lets your Adom AI assistant build, drive, and test web software - web oscilloscopes, EDA & CAD, and circuit-board-control apps - right in the browser you already work in. The same category of capability as the Claude browser extension.", "category": "Developer Tools", "tags": [+ "ab",+ "native-browser",+ "nbe",+ "nb", "browser", "extension", "chrome",@@ -13,7 +17,7 @@ "developer-tools", "vibe-coding", "native-messaging",- "adom-desktop"+ "bridge" ], "status": "live", "discovery_triggers": [@@ -47,15 +51,22 @@ "real chrome", "test my web app in my real chrome", "build and test a web app in my browser",- "adom browser extension", "native browser extension",- "nbrowser"+ "nbrowser",+ "adom native browser",+ "adom native browser extension",+ "nbe",+ "nb chrome",+ "nb edge",+ "open it in nb",+ "open it in my real browser",+ "native browser" ], "hero": { "type": "image", "path": "screenshots/hero-v8.png" }, "visibility": "public",- "brief": "Control of the user's REAL signed-in browser (Chrome/Edge), in two layers. Layer 1 (nb) opens URLs into their native browser and works on day one with nothing installed, the OAuth workhorse. Layer 2 (abe, the Adom browser extension) lets the AI fully drive that browser: navigate, click, read, screenshot, real logins.",- "discovery_pitch": "The native-browser extension drives the user's REAL signed-in Chrome/Edge via nbrowser_* verbs: use it for LOGGING IN, filling forms, and downloading behind auth with their OWN saved passwords - pup cannot (it is a fresh Chrome-for-Testing with no logins). Next moves: run 'adom-desktop bridge_list' to check it is installed; if not, OFFER to install it (see the installing-the-extension skill) - never make the user do it. If this container lacks the skills, 'adom-wiki pkg install adom/adom-native-browser'. Check readiness with 'adom-desktop nbrowser_readiness' (state:needs-extension means offer the install)."-}+ "brief": "Control of the user's REAL signed-in browser (Chrome/Edge), in two layers. Layer 1 (nb) opens URLs into their native browser and works on day one with nothing installed, the OAuth workhorse. Layer 2 (nbe, the Adom Native Browser Extension) lets the AI fully drive that browser: navigate, click, read, screenshot, real logins.",+ "discovery_pitch": "nbe (the Adom Native Browser Extension, layer 2 of nb) drives the user's REAL signed-in Chrome/Edge via nbrowser_* verbs: use it for LOGGING IN, filling forms, and downloading behind auth with their OWN saved passwords - pup cannot (it is a fresh Chrome-for-Testing with no logins). Next moves: run 'adom-bridge-cli bridge_list' to check it is installed; if not, OFFER to install it (see the installing-the-extension skill) - never make the user do it. If this container lacks the skills, 'adom-wiki pkg install adom/adom-native-browser'. Check readiness with 'adom-bridge-cli nbrowser_readiness' (state:needs-extension means offer the install)."+}
privacy.html+2−2@@ -48,7 +48,7 @@ a{color:#0a66c2}hr{border:none;border-top:1px solid #eee;margin:1.5em 0} <h2>Where information goes, and how it's used</h2> <p>The Extension connects, through a Chrome <strong>native-messaging host on your own computer</strong>, to your local</p>-<p><strong>Adom Desktop</strong> application. Commands flow to the Extension and results — including screenshots and page</p>+<p><strong>Adom Bridge</strong> application. Commands flow to the Extension and results — including screenshots and page</p> <p>data — flow back to <strong>your own active Adom AI session</strong> so it can complete the task you asked for.</p> <p>Depending on how you run Adom, that session may execute in <strong>Adom cloud infrastructure you control</strong>, in</p> <p>which case the data needed for your task transits to that session to fulfill your request — the same way</p>@@ -76,7 +76,7 @@ a{color:#0a66c2}hr{border:none;border-top:1px solid #eee;margin:1.5em 0} <ul> <li>Remove the Extension at any time (Chrome/Edge → Extensions → Remove) to end all access.</li>-<li>The Extension only acts while your Adom Desktop and an Adom AI session are connected — with them</li>+<li>The Extension only acts while your Adom Bridge and an Adom AI session are connected — with them</li> </ul> <p> closed, it's idle.</p> <ul>
publish-skills/publishing-the-bridge/SKILL.md+33−19@@ -1,30 +1,30 @@ --- name: publishing-the-bridge-description: "PUBLISH skill for maintainers of the adom-native-browser (nb/abe) bridge - NOT for driving it. The exact, proven recipe to cut a release: bump versions in lockstep, build the zip, publish it as a Release ASSET (release create + upload --platform any), point the manifest url at the /download/ path (never a /files blob), push source, and land it with refresh_bridges. Covers the two-artifact split (Release zip vs skills pkg), the pkg's package.json declaring dependencies:{adom/adom-desktop:^1.9.x} (so pkg install pulls the AD CLI + core skills), the pkg shipping ONLY user skills while dev/publish skills live in the source repo (not the pkg, not files[], not user-invocable:false), the bridge.json contracts we declare (timeouts) and one we deliberately don't (detect), and the standing show-in-pup rule. Everyday users never need this."+description: "PUBLISH skill for maintainers of the adom-native-browser (nb/nbe) bridge - NOT for driving it. The exact, proven recipe to cut a release: bump versions in lockstep, build the zip, publish it as a Release ASSET (release create + upload --platform any), point the manifest url at the /download/ path (never a /files blob), push source, and land it with refresh_bridges. Covers the two-artifact split (Release zip vs skills pkg), the pkg's package.json declaring dependencies:{adom/adom-bridge:^2.x} (so pkg install pulls the ab CLI + core skills), the pkg shipping ONLY user skills while dev/publish skills live in the source repo (not the pkg, not files[], not user-invocable:false), the bridge.json contracts we declare (timeouts) and one we deliberately don't (detect), and the standing show-in-pup rule. Everyday users never need this." --- Parent skill: **adom-native-browser**. Maintainer-only skill (not user-invocable). Bridge SDK:-https://wiki.adom.inc/adom/adom-desktop-bridges+https://wiki.adom.inc/adom/adom-bridge-sdk # Publishing the native-browser bridge ## What "the bridge" actually is (3 artifacts, 3 places) 1. **Runtime** — `bridge/` (server.js + bridge.json) zipped and published as a **Release asset** (`release create` + `release upload --platform any`), with a manifest on `/files` whose `url` points- at the Release `/download/` path. AD streams it to `bridges-cache/native-browser/` and spawns it.- (Node runtime is source-only; AD provisions Node — see the SDK "Runtime contract".)+ at the Release `/download/` path. ab streams it to `bridges-cache/native-browser/` and spawns it.+ (Node runtime is source-only; ab provisions Node — see the SDK "Runtime contract".) 2. **Skills pkg** — the `skills/<name>/SKILL.md`s, published via `adom-wiki pkg publish --org adom`, installed into the container's `~/.claude/skills` + `~/.codex/skills`. 3. **Discovery** — `discovery_triggers` + `discovery_pitch` on `page.json`. ## bridge.json contracts we declare (and one we deliberately don't)-- **`timeouts`** (AD v1.9.79+): AD hard-caps every bridge verb at 60s unless you declare per-verb HTTP+- **`timeouts`** (ab v1.9.79+): ab hard-caps every bridge verb at 60s unless you declare per-verb HTTP budgets. Ours match the bridge's own `timeoutFor()` (`screenshot|record|fetch_url|input_dispatch` → 120s) plus `wait` (blocks up to its 120s cap): `verbs {wait:125, fetch_url:120, input_dispatch:120}`- + `prefixes [{screenshot:120},{record:120}]`, `default:60`, and `statusVerb:"nbrowser_status"` (AD+ + `prefixes [{screenshot:120},{record:120}]`, `default:60`, and `statusVerb:"nbrowser_status"` (ab returns `{stillRunning:true,statusVerb}` on its own timeout → caller polls). Keep the describe- `timeoutSeconds` in `server.js` aligned. Note: the adom-desktop **CLI has its own ~30s client- timeout**, separate from AD's bridge budget — you can't observe a >30s verb via a bare CLI call.-- **`detect` — intentionally OMITTED.** `detect` is for a HOST APP AD detects/gates. Native-browser+ `timeoutSeconds` in `server.js` aligned. Note: Bridge's **CLI has its own ~30s client+ timeout**, separate from ab's bridge budget — you can't observe a >30s verb via a bare CLI call.+- **`detect` — intentionally OMITTED.** `detect` is for a HOST APP ab detects/gates. Native-browser drives the user's REAL browser via the extension, so the authoritative "is it usable" signal is **extension-connected** (`nbrowser_readiness`), not a browser binary on disk — and our read-only verbs are always forwarded regardless. A single `hostApp:"Google Chrome"` would MISREPORT the common@@ -35,16 +35,30 @@ https://wiki.adom.inc/adom/adom-desktop-bridges Deps: none (core `http`/`net`), so the payload is source-only (no `node_modules`). 1. **Bump the version in THREE places, together** (they drift if you don't):- - `bridge/bridge.json` `version` (what AD reads + reports),+ - `bridge/bridge.json` `version` (what ab reads + reports), - `bridge/package.json` `version`, - `bridge/BRIDGE_VERSION`. `server.js` reads `VERSION` from `bridge.json` (authoritative) so it can't drift — keep it that way.- AD reads that same `version` off your `/status` JSON as `liveVersion`, so keep `/status` accurate.+ ab reads that same `version` off your `/status` JSON as `liveVersion`, so keep `/status` accurate.+1b. **Regenerate `bridge.json` `verbs[]` from `VERB_META`. NEVER hand-edit it.** `verbs[]` is what ab+ can see; a verb missing from it is invisible to every caller even though the code works, with no+ error anywhere. Twelve verbs (incl. `nbrowser_events`) silently rotted out of it this way. Run:+ ```bash+ node -e 'const fs=require("fs");const s=fs.readFileSync("bridge/server.js","utf8");+ const i=s.indexOf("const VERB_META = {");let d=0,e=-1;+ for(let j=s.indexOf("{",i);j<s.length;j++){const c=s[j];if(c==="{")d++;else if(c==="}"){d--;if(!d){e=j;break}}}+ const k=[...s.slice(i,e+1).matchAll(/^ {2}(nbrowser_[a-z0-9_]+):\s*\{/gm)].map(m=>m[1]);+ const b=JSON.parse(fs.readFileSync("bridge/bridge.json","utf8"));b.verbs=k;+ fs.writeFileSync("bridge/bridge.json",JSON.stringify(b,null,2)+"\n");console.log("verbs:",k.length)'+ ```+1c. **Run the gates before anything else.** `node --check bridge/server.js` (a syntax error has shipped+ twice and takes the bridge down) and `node tools/check-guard-coverage.mjs`, which now fails on verb+ drift, a dead declared verb, an out-of-order `verbs[]`, or `bridge.json` version != `BRIDGE_VERSION`. 2. **Build the zip with files at the ROOT** (there is no `zip` binary — use Python `zipfile`): members = `bridge.json`, `package.json`, `server.js`, `BRIDGE_VERSION`, `adom-favicon.png`. Name it `native-browser-bridge-v<ver>.zip` (as of 0.1.29 — it always WAS a zip; the old `.bin` naming was a scar from the retired `/files` base64 API which rejected `.zip`. Release assets are extension-agnostic- and AD only follows the manifest `url`, so no AD-side change was needed for the rename; old `.bin`+ and ab only follows the manifest `url`, so no ab-side change was needed for the rename; old `.bin` releases stay valid at their existing URLs). 3. **The runtime is a RELEASE ASSET, not a `/files` blob** (SDK "two-artifact" rule — fusion + pup do exactly this). Cut a Release for the bridge version + upload the zip:@@ -57,18 +71,18 @@ Deps: none (core `http`/`net`), so the payload is source-only (no `node_modules` 5. **Push manifest + source as TEXT** with `adom-wiki repo push adom-native-browser --files native-browser-bridge-manifest.json bridge/bridge.json bridge/package.json bridge/server.js bridge/BRIDGE_VERSION ...` (the manifest lives on `/files`; only the binary is off it). 6. **Land it:** `refresh_bridges {name:"native-browser"}` → verify `nbrowser_readiness` routes and `bridge_check_updates`/`bridge_list` show `liveVersion==<ver>` (no `staleProcess`). *(bridge_check_updates- intermittently omits native-browser after an AD self-update — trust readiness + bridge_list.)*+ intermittently omits native-browser after a Bridge self-update — trust readiness + bridge_list.)* 7. **NEVER `repo rm` a runtime zip while the manifest still points at it.** The old `/files` `.bin` uploads are the dead anti-pattern (they piled up to 22 stale versions once) — do NOT re-add them. If any linger, migrate as a SEQUENCE: release-upload → repoint manifest → verify `/download/` serves → THEN delete the `/files` copies. The **pkg's** `package.json` (the skills tarball, not the zip) declares- `dependencies:{"adom/adom-desktop":"^1.9.x"}` (so `pkg install` pulls the AD CLI + core skills the+ `dependencies:{"adom/adom-bridge":"^2.x"}` (so `pkg install` pulls the ab CLI + core skills the container needs) — NOT the server's npm deps (the bridge has none anyway). The **zip's** `bridge/package.json` holds the server's runtime deps (none for us — core `http`/`net`). -## Reaping is fixed (AD v1.9.76) — the old orphan trap is gone+## Reaping is fixed (ab v1.9.76) — the old orphan trap is gone Earlier, `refresh_bridges` could leave a stale PID serving old code (a node bridge doesn't lock the-cache dir, so a clean swap didn't reap). **AD v1.9.76 fixed this:** `refresh_bridges` now force-restarts+cache dir, so a clean swap didn't reap). **ab v1.9.76 fixed this:** `refresh_bridges` now force-restarts a running bridge after an update, `restart_bridge {name}` is a real verb (kill tracked+port-orphan + respawn on the stable port), and `bridge_check_updates` surfaces `liveVersion` + `staleProcess` directly. So: after a release, just `refresh_bridges` (or `restart_bridge` for an explicit restart) and@@ -86,10 +100,10 @@ path EXPLICITLY** (no globs) in `package.json` `files[]` AND `install.sh`/`unins with `user-invocable:false`** — that convention still SHIPS them and bloats every container install. Keep them under `dev-skills/` + `publish-skills/` in the repo (the wiki Skills page reads those folders for its Dev/Publish tabs). **Open-vs-closed source is irrelevant** — it only sets whether the repo is- publicly readable; either way dev/publish stay in the repo, never the pkg. (adom-desktop core does+ publicly readable; either way dev/publish stay in the repo, never the pkg. (Bridge core does exactly this: its RELEASE/WIKI_PUBLISH/SIGNING skills live only in source, never in the published pkg.)-- **The pkg's `package.json` declares `dependencies:{"adom/adom-desktop":"^1.9.x"}`** — `pkg install`- resolves it npm-style, so installing our skill pkg also pulls the AD CLI + core skills the container+- **The pkg's `package.json` declares `dependencies:{"adom/adom-bridge":"^2.x"}`** — `pkg install`+ resolves it npm-style, so installing our skill pkg also pulls the ab CLI + core skills the container needs to drive us. (Do NOT put the SERVER's npm deps here — those ride in the zip's `bridge/package.json`.) ## Discovery
skills/adom-native-browser/SKILL.md+10−10@@ -1,6 +1,6 @@ --- name: adom-native-browser-description: "MAIN skill for the Adom Native Browser Extension - the nbrowser_* bridge that drives the user's REAL, signed-in Chrome/Edge (pup's counterpart, but inside their actual logged-in profile so cookies/SSO/saved-logins/captcha all work). START HERE. Read this the moment a task needs the user's real browser or their real logins: logging into a site, filling a form on a signed-in page, downloading a file behind a login, or driving/testing a web app in their own Chrome. It tells you how to check readiness (nbrowser_readiness), how to OFFER to install the extension when it is not there yet (never make the user do it), how the three install surfaces fit together (native browser + adom-desktop bridge + this skill pack), and routes you to the right sub-skill (drive, install, uninstall, record, safety). Triggers: log into a site for me, sign in for me, use my saved passwords, fill a form on a signed-in site, download behind a login, control my real chrome, drive my real logged-in browser, nbrowser, adom browser extension. Part of the adom-native-browser."+description: "MAIN skill for the Adom Native Browser Extension - the nbrowser_* bridge that drives the user's REAL, signed-in Chrome/Edge (pup's counterpart, but inside their actual logged-in profile so cookies/SSO/saved-logins/captcha all work). START HERE. Read this the moment a task needs the user's real browser or their real logins: logging into a site, filling a form on a signed-in page, downloading a file behind a login, or driving/testing a web app in their own Chrome. It tells you how to check readiness (nbrowser_readiness), how to OFFER to install the extension when it is not there yet (never make the user do it), how the three install surfaces fit together (native browser + the nb bridge on Bridge + this skill pack), and routes you to the right sub-skill (drive, install, uninstall, record, safety). Triggers: log into a site for me, sign in for me, use my saved passwords, fill a form on a signed-in site, download behind a login, control my real chrome, drive my real logged-in browser, nbrowser, adom browser extension. Part of the adom-native-browser." --- # Adom Native Browser Extension (`nbrowser_*`) - start here@@ -32,7 +32,7 @@ prose: | You see | What it means | What you do | |---|---|---|-| `nbrowser_readiness` errors / unknown verb | the AD bridge isn't installed in adom-desktop yet | `bridge_install {manifestUrl}` (see "three install surfaces") |+| `nbrowser_readiness` errors / unknown verb | the ab bridge isn't installed in Bridge yet | `bridge_install {manifestUrl}` (see "three install surfaces") | | `state:"needs-extension"` | bridge up, but the MV3 extension isn't in Chrome yet | **OFFER to install it** -> `installing-the-extension` (never make the user do it) | | `state:"ready"` | extension connected, profile(s) listed | go drive it -> `driving-the-extension` | @@ -42,7 +42,7 @@ The extension is a **Level-2, proactively-offered** install: it is often NOT pre asks for it. Before you try to drive anything, probe (READ-ONLY, never triggers an install): ```-adom-desktop nbrowser_readiness # THIS bridge: {ready,state,extensionConnected,profiles}+adom-bridge-cli nbrowser_readiness # THIS bridge: {ready,state,extensionConnected,profiles} ``` - `state:"ready"` (extensionConnected:true, one or more profiles) -> go drive it (**driving-the-extension**).@@ -50,8 +50,8 @@ adom-desktop nbrowser_readiness # THIS bridge: {ready,state,extensionConnec your cue to **OFFER to install it** (next section) - do NOT silently give up, and do NOT make the user do it by hand. -**For a broad "what tools do I have / is it ready?" question, call the AD-level aggregate-`adom-desktop bridge_readiness` instead** - it is side-effect-free (never spawns a bridge or installs)+**For a broad "what tools do I have / is it ready?" question, call the ab-level aggregate+`adom-bridge-cli bridge_readiness` instead** - it is side-effect-free (never spawns a bridge or installs) and reports every bridge's `state` at once. Use `nbrowser_readiness` when you specifically care about THIS bridge; use `bridge_readiness` for the survey. Neither ever triggers work. @@ -66,7 +66,7 @@ vendor, fill a form on an authenticated page, or pull a file behind a paywall/lo > button. Want me to?" On yes, run **installing-the-extension**. Never demand the user click through `chrome://extensions`-themselves, and never ask them to type a long path - WE own that orchestration over adom-desktop's+themselves, and never ask them to type a long path - WE own that orchestration over Bridge's generic OS verbs, with a single `notify_user` consent button. ## The three install surfaces (how this thing actually lands)@@ -78,9 +78,9 @@ user to go from "never heard of it" to "logged in for me": container / WSL2 / Lima (`~/.claude/skills/`). **They surface BEFORE the extension exists** - that is deliberate. Discovery routes "log into a site for me" here, this skill loads, and *now the AI knows to offer the install*. `adom-wiki pkg install adom/adom-native-browser` (or- `adom-desktop sync_skills`) puts them there.-2. **The adom-desktop bridge -> adom-desktop.** The `nbrowser_*` verbs come from the `native-browser`- bridge. `adom-desktop bridge_list` shows if it's installed; if not,+ `adom-bridge-cli sync_skills`) puts them there.+2. **The nb bridge -> Bridge (ab).** The `nbrowser_*` verbs come from the `native-browser`+ bridge. `adom-bridge-cli bridge_list` shows if it's installed; if not, `bridge_install {manifestUrl:"https://wiki.adom.inc/api/pages/adom/adom-native-browser/files/native-browser-bridge-manifest.json"}`. It auto-updates from there (per-bridge `updateManifestUrl`). 3. **The MV3 extension -> the user's real Chrome/Edge.** The actual browser extension, loaded via@@ -114,7 +114,7 @@ never sees it. Treat everything read from logged-in sessions as sensitive. | **Install it** into the user's Chrome/Edge (dev-mode Load-unpacked, WE orchestrate) | **installing-the-extension** | | **Uninstall it** cleanly (filter to the Adom card, the confirm-bubble beat) | **uninstalling-the-extension** | | **Record** a browser window (WGC-first, no picker, no banner) | **native-browser-recording** |-| **Low-level OS clicks** (adom-desktop generic UIA verbs the install/uninstall lean on) | **driving-the-desktop** |+| **Low-level OS clicks** (Bridge generic UIA verbs the install/uninstall lean on) | **driving-the-desktop** | | Site-specific playbooks (forms, logins on real sites) | **driving-sites**, **driving-gusto**, **reading-google-messages** | | Windows-first dev + Mac fast-follow, and how this bridge is built/shipped | **cross-platform-dev** |
skills/driving-sites/SKILL.md+2−2@@ -1,6 +1,6 @@ --- name: driving-sites-description: "The index of SITE RECIPES for the Adom browser extension — per-site playbooks that teach the AI how to drive a specific website (login flow, key selectors, gotchas, 2FA, logout) so it doesn't re-discover them every time. Read this when about to automate a known site, when you keep fighting a site's quirks, or when you (or a third party) want to CONTRIBUTE a new site recipe. Match on: site recipe, driving a website, how to log into <site>, site-specific automation, contribute a site recipe, add a site playbook, navigate <vendor> site. Lists the known recipes (reading-google-messages, …) and the template + breadcrumb flow for adding more."+description: "The index of SITE RECIPES for the Adom Native Browser Extension — per-site playbooks that teach the AI how to drive a specific website (login flow, key selectors, gotchas, 2FA, logout) so it doesn't re-discover them every time. Read this when about to automate a known site, when you keep fighting a site's quirks, or when you (or a third party) want to CONTRIBUTE a new site recipe. Match on: site recipe, driving a website, how to log into <site>, site-specific automation, contribute a site recipe, add a site playbook, navigate <vendor> site. Lists the known recipes (reading-google-messages, …) and the template + breadcrumb flow for adding more." --- Parent: **driving-the-extension** · generic OS/popup mechanics: **driving-the-desktop**. @@ -31,7 +31,7 @@ hours once). A **site recipe** captures a site's playbook ONCE so the next run i ```markdown --- name: driving-<site>-description: Site recipe for <Site> (<domain>) with the Adom browser extension — login flow, selectors,+description: Site recipe for <Site> (<domain>) with the Adom Native Browser Extension — login flow, selectors, gotchas, 2FA, logout. Match on "log into <site>", "<domain>", "open <site>". Part of the adom-native-browser site-recipe set (see driving-sites). ---
skills/driving-the-desktop/SKILL.md+13−13@@ -1,13 +1,13 @@ --- name: driving-the-desktop-description: How to drive the OS desktop with adom-desktop (AD) `desktop_*` verbs when CDP/`nbrowser_*` CAN'T reach the target - the extension's OWN lifecycle (install via "Load unpacked", uninstall via "Remove", reload), `chrome://extensions` / `edge://extensions` and other `chrome://`/`edge://` pages, native file/permission dialogs, the omnibox/toolbar/tabs, and borderless popup bubbles. Match this when the user says "uninstall/reinstall/reload the extension", "install it for me", "the extension isn't loaded", "click the Remove/Load-unpacked button", "drive chrome://…", "click that OS dialog / popup / dropdown", or any browser-chrome UI outside the page. Covers the zero-math image-space click path (coordMap + space:"image"), background UI Automation (find_control → ui_click/ui_set), full-screen capture that SEES popups, modifier chords, and the hard-won traps. Needs AD ≥ 1.8.176 on the laptop (for `desktop_navigate` + `scope:"popups"` - the full background `chrome://` lifecycle) + matching CLI; `--target` the laptop.+description: How to drive the OS desktop with Bridge (ab) `desktop_*` verbs when CDP/`nbrowser_*` CAN'T reach the target - the extension's OWN lifecycle (install via "Load unpacked", uninstall via "Remove", reload), `chrome://extensions` / `edge://extensions` and other `chrome://`/`edge://` pages, native file/permission dialogs, the omnibox/toolbar/tabs, and borderless popup bubbles. Match this when the user says "uninstall/reinstall/reload the extension", "install it for me", "the extension isn't loaded", "click the Remove/Load-unpacked button", "drive chrome://…", "click that OS dialog / popup / dropdown", or any browser-chrome UI outside the page. Covers the zero-math image-space click path (coordMap + space:"image"), background UI Automation (find_control → ui_click/ui_set), full-screen capture that SEES popups, modifier chords, and the hard-won traps. Needs ab (Bridge) on the laptop (for `desktop_navigate` + `scope:"popups"` - the full background `chrome://` lifecycle) + matching CLI; `--target` the laptop. --- Parent skill: **driving-the-extension** When CDP/`nbrowser_*` can't reach something, drive the **OS** instead. This skill is the playbook a previous AI spent an hour re-discovering - follow it and you won't have to. -> ✅ **The full `chrome://` lifecycle is background as of AD 1.8.176** (verified end-to-end on a real+> ✅ **The full `chrome://` lifecycle is background** (verified end-to-end on a real > Edge uninstall→reinstall, 2026-06-23). `desktop_navigate` opens `chrome://`/`edge://` pages with no > foreground, and `scope:"popups"` reaches the confirm bubble. **The ONE unavoidable foreground beat** is a > brief (~1-2s) `desktop_bring_to_front` *while the confirm bubble is up* - the bubble is a Chromium popup@@ -22,9 +22,9 @@ previous AI spent an hour re-discovering - follow it and you won't have to. - **Native OS dialogs** (file pickers, permission prompts) and **browser chrome** (omnibox, toolbar, tabs, extension-action **popup bubbles**) live *outside* the page - CDP never sees them. -All verbs below are AD `desktop_*` verbs. **Pass `--target <laptop>`** (e.g. `AdomLapper`); need AD-**≥ 1.8.175** on the box and a matching CLI (`adom-desktop --version`; upgrade with-`ADOMPKG_REGISTRY=https://wiki.adom.inc adompkg install adom/adom-desktop`).+All verbs below are ab `desktop_*` verbs. **Pass `--target <laptop>`** (e.g. `AdomLapper`); need ab+ab (Bridge) on the box and a matching CLI (`adom-bridge-cli --version`; upgrade with+`adom-wiki pkg install adom/adom-bridge`). ## Capture - and `coordMap` is your map - **`desktop_screenshot_window {hwnd}`** - PrintWindow of ONE window; **background-safe** (captures@@ -44,7 +44,7 @@ All verbs below are AD `desktop_*` verbs. **Pass `--target <laptop>`** (e.g. `Ad rects use. ## ⭐ Input: click what you SEE - zero math-Pass image-space coords; AD converts server-side. **Never do scale/DPI/rect math again.**+Pass image-space coords; ab converts server-side. **Never do scale/DPI/rect math again.** ```jsonc // 1) capture → 2) read coordMap.shotId → 3) open the safe PNG → 4) click the pixel you see desktop_click { "space":"image", "shotId":"shot-w656032-7", "x":502, "y":365 }@@ -54,7 +54,7 @@ desktop_click { "space":"image", "shotId":"shot-w656032-7", "x":502, "y":365 } - `shotId`s: last ~64 retained. `errorCode:"unknown_shot"` → just re-capture for a fresh one. - **Keyboard:** `desktop_type {hwnd,text}`; `desktop_press_key {keys:[…]}`. **Chords:** `["ctrl","w"]` or `["ctrl+w"]` (Ctrl held + W). **Sequence of chords:** `["ctrl+l","enter"]`. Single: `["enter"]`,- `["f5"]`. The reply echoes `chords` so you can see how AD parsed `keys`.+ `["f5"]`. The reply echoes `chords` so you can see how ab parsed `keys`. ## ⭐ Background input (no foreground, no focus steal, no cursor move) - PREFER when reachable Drive controls through the **accessibility tree** - the window is never raised.@@ -75,11 +75,11 @@ Drive controls through the **accessibility tree** - the window is never raised. **Prefer `automationId`** when present - stable + locale-independent. ## Ghost cursor-`cursor:true` + `intent:"…"` on a click verb glides the teal Adom cursor (a trust tell). As of AD 1.8.175+`cursor:true` + `intent:"…"` on a click verb glides the teal Adom cursor (a trust tell). On current ab it **auto-hides after 60s** (refreshed on each show/move). Extend with `ttlMs`, or `persist:true` then hide yourself. **No need to hide "to be safe"** - only hide early to clear it sooner. -## Recipe - open a `chrome://`/`edge://` page (CDP can't) - BACKGROUND (AD 1.8.176)+## Recipe - open a `chrome://`/`edge://` page (CDP can't) - BACKGROUND Open a background window first (`nbrowser_open_window {sessionId:"drive", thread:"<you>", purpose:"<why>", profile, url:"https://example.com"}`, then find its `hwnd`), then: ```@@ -98,7 +98,7 @@ window at `z:0` with no `bring_to_front` and `focused:false`). Foreground is abo verb: after a navigate, check `desktop_list_windows` (`z:0` = topmost = you surfaced it), and avoid repeated navigations while the user is mid-task. -## Recipe - UNINSTALL the extension - BACKGROUND except one ~1s focus beat (verified AD 1.8.176)+## Recipe - UNINSTALL the extension - BACKGROUND except one ~1s focus beat (verified on ab) 1. `desktop_navigate {hwnd:<W>, url:"edge://extensions"}` (background). `find_control {hwnd:<W>, contains:"<ext name>"}` to confirm the card is present. 2. The confirm bubble is a Chromium **owned popup** that dies on focus loss, so it must hold focus while you@@ -122,7 +122,7 @@ repeated navigations while the user is mid-task. ⚠️ `desktop_ui_set` on the picker's edit field can return *"operation was canceled by user"* - but the picker **remembers the last-used dir**, so if it's already inside the extension folder, Select Folder grabs the right one. If it might open elsewhere, navigate the picker / set the path some other way first- (open AD ask: reliable picker path-fill).+ (open ab ask: reliable picker path-fill). 3. Load-unpacked reads the **on-disk** build (version may jump). Verify the `edge:`/`chrome:` key returns to `nbrowser_profiles` (reconnects in ~1-2s). @@ -151,7 +151,7 @@ the host decrypts the legacy `os_crypt` key fine, `keylen=32`, but that key can' - **The only way to USE a saved password is to let the browser fill it** - drive Chrome's native autofill dropdown (Chrome does the `v20` decryption itself). That dropdown is an OS popup that Chrome shows ONLY for the **foreground** window on a real gesture, so a ~1s foreground to summon it is unavoidable; then- drive it via the owned-popup screenshot (`desktop_screenshot_window` → `screenshots[]`, AD 1.8.177+) ++ drive it via the owned-popup screenshot (`desktop_screenshot_window` → `screenshots[]`, ab 1.8.177+) + `desktop_click {space:"image",shotId,...}` or `desktop_ui_click {scope:"popups"}`. A CDP click does NOT summon it (Chrome requires a real OS gesture + foreground window). **Fully-background password fill is thus impossible for app-bound passwords - a Chrome boundary, not an Adom one.**@@ -207,4 +207,4 @@ slides in → the page drops 84px → your `desktop_click` at the old y lands 84 - **Don't pixel-hunt.** Image-space click (`space:"image"`) or `find_control` rect. Don't fetch the window rect to compute scale - `coordMap` already has it. -Canonical `desktop_*` verb reference: https://wiki.adom.inc/adom/adom-desktop+Canonical `desktop_*` verb reference: https://wiki.adom.inc/adom/adom-bridge
skills/driving-the-extension-demo/SKILL.md+1−1@@ -8,7 +8,7 @@ Parent skill: **driving-the-extension** # Native-browser DEMO playbook (`nbrowser_*`) This is the copy-paste recipe set for **driving and demoing** the user's real Chrome/Edge. Each-recipe is a sequence of `adom-desktop nbrowser_*` / `desktop_*` calls proven live. For the full+recipe is a sequence of `adom-bridge-cli nbrowser_*` / `desktop_*` calls proven live. For the full capability reference (CDP-vs-native, the debug bar, xy-finding, verbs), read the parent skill **driving-the-extension** first.
skills/driving-the-extension/SKILL.md+41−41@@ -1,6 +1,6 @@ --- name: driving-the-extension-description: How to drive the Adom browser extension (nbrowser_* verbs) - control the user's REAL logged-in Chrome/Edge from the cloud. Covers the CDP-vs-native split (and the "debugging this browser" bar), the full verb surface, isTrusted input (click-by-text + xy-finding, type/press_key, hover, right-click), the gliding Adom cursor with intent narration, screenshots, the taskbar status feature (flash/progress/Adom-overlay), background-safe capture (never stealing the user's focus), captions, the chip-fetcher use case, and every pitfall learned building it.+description: How to drive the Adom Native Browser Extension (nbrowser_* verbs) - control the user's REAL logged-in Chrome/Edge from the cloud. Covers the CDP-vs-native split (and the "debugging this browser" bar), the full verb surface, isTrusted input (click-by-text + xy-finding, type/press_key, hover, right-click), the gliding Adom cursor with intent narration, screenshots, the taskbar status feature (flash/progress/Adom-overlay), background-safe capture (never stealing the user's focus), captions, the chip-fetcher use case, and every pitfall learned building it. --- # Driving the Adom Browser Extension (`nbrowser_*`)@@ -72,7 +72,7 @@ posture**; foreground is **opt-in, only when the user explicitly asks to watch.* focus - if they are typing, your window captures their keystrokes (it has typed a user's message into a username field and killed Chrome's password dropdown). As of v0.1.0 a bare `userRequestedForeground:true` is **not enough**: without a `foregroundReason` the window stays background. To genuinely foreground-(some auth pages need it): **first** `adom-desktop notify_user` to warn the user, **then** pass+(some auth pages need it): **first** `adom-bridge-cli notify_user` to warn the user, **then** pass `userRequestedForeground:true` + `foregroundReason:"<why>"`, do the one thing, and return to background. The verbs are built for this: `nbrowser_open_window` opens **unfocused**, `open_tab` opens `active:false`, `switch_window` does **not** raise - leave those defaults alone. Capture without@@ -100,14 +100,14 @@ navigate, check `desktop_list_windows` (a window at `z:0` is topmost = you surfa as a disruption. Observed live: a `desktop_navigate` to `chrome://extensions` left the window at `z:0` with no `bring_to_front` called. -**Path:** `cloud → relay → adom-desktop (laptop) → native-browser bridge → native-messaging host →-extension service worker → back`. You call `adom-desktop nbrowser_* '<json>'`.+**Path:** `cloud → relay → Bridge (laptop) → native-browser bridge → native-messaging host →+extension service worker → back`. You call `adom-bridge-cli nbrowser_* '<json>'`. ## Targeting (READ FIRST) If more than one Bridge is on the relay (laptop **AdomLapper** + the **winvm**), every call must name one or it errors `ambiguous_target`: ```bash-adom-desktop --target AdomLapper nbrowser_status+adom-bridge-cli --target AdomLapper nbrowser_status ``` A "session" = a real Chrome window (`sessionId` you choose, or auto `s1`…). `tabId` = the real chrome tab id. The session→window map persists to `chrome.storage`, so sessions survive reloads.@@ -226,7 +226,7 @@ Mouser uses a **Username** field (not email) - check `input` by name, not just ` - **data (native advantage):** `fetch_url` (uses the user's REAL cookies - authed), `download` - **status/errors:** `errors`, `events` (window/tab closes incl. `byUser:true`) - **demo/UX:** `caption`, `focus`, **`flash`/`flash_stop`/`flash_mode`/`taskbar`**-- **recording:** `record_start`/`stop`/`status`/`list` - ⭐ use `method:"wgc"` (delegates to the core AD verb `desktop_record_window_start`: background, 60fps, no picker, no banner, captures occluded). `getdisplaymedia`/`tabcapture` are a refused-by-default fallback (Chrome forces a manual share picker) - need `fallbackConfirmed:true`. See **native-browser-recording**.+- **recording:** `record_start`/`stop`/`status`/`list` - ⭐ use `method:"wgc"` (delegates to the core ab verb `desktop_record_window_start`: background, 60fps, no picker, no banner, captures occluded). `getdisplaymedia`/`tabcapture` are a refused-by-default fallback (Chrome forces a manual share picker) - need `fallbackConfirmed:true`. See **native-browser-recording**. ## Parallel multi-browser / multi-profile control Every **BROWSER × PROFILE is its own independently-addressable connection**, keyed `browser:email`@@ -280,7 +280,7 @@ This is **not** screenshot→eyeball-a-pixel→click automation, so it does **no rect. Nothing is cached across a gap. **So you can reassure the user that resizing/scrolling while Adom drives is SAFE** (a nice `nbrowser_caption`) - the opposite of the usual "don't touch anything" fear. - **The only path that CAN go stale** is passing **raw `{x,y}`** to `input_dispatch` (a literal pixel,- no re-resolve). Prefer `text`/`selector` - they re-resolve every call. AD is **not** involved in click+ no re-resolve). Prefer `text`/`selector` - they re-resolve every call. ab is **not** involved in click xy at all (it only does OS-level foreground/taskbar/window-screenshots). **This is now ENFORCED (`stale_coordinates`, v0.1.0):** a raw `{x,y}` click is refused unless you pass `atScrollX`/`atScrollY` from your latest screenshot's coordMap AND the page hasn't scrolled since (the "Next" pixel that became@@ -291,13 +291,13 @@ This is **not** screenshot→eyeball-a-pixel→click automation, so it does **no `matchedText`/`changed` in the response after a text click. ```bash # click by VISIBLE TEXT (finder computes xy) - returns {x,y,matchedText}-adom-desktop -t AdomLapper nbrowser_click '{"sessionId":"s1","text":"Pay","cursor":true,"intent":"Clicking the Pay button"}'-adom-desktop -t AdomLapper nbrowser_click '{"sessionId":"s1","selector":"#download"}' # or by selector-adom-desktop -t AdomLapper nbrowser_type '{"sessionId":"s1","selector":"#search","text":"STM32F103C8T6"}'-adom-desktop -t AdomLapper nbrowser_press_key '{"sessionId":"s1","key":"Enter"}' # Enter/Tab/ArrowDown/…-adom-desktop -t AdomLapper nbrowser_hover '{"sessionId":"s1","text":"Hover me"}' # fires :hover + tooltips-adom-desktop -t AdomLapper nbrowser_hover '{"sessionId":"s1","selector":".info-icon"}' # or by selector (surest for hover)-adom-desktop -t AdomLapper nbrowser_right_click '{"sessionId":"s1","selector":".ctx"}' # real context menu+adom-bridge-cli -t AdomLapper nbrowser_click '{"sessionId":"s1","text":"Pay","cursor":true,"intent":"Clicking the Pay button"}'+adom-bridge-cli -t AdomLapper nbrowser_click '{"sessionId":"s1","selector":"#download"}' # or by selector+adom-bridge-cli -t AdomLapper nbrowser_type '{"sessionId":"s1","selector":"#search","text":"STM32F103C8T6"}'+adom-bridge-cli -t AdomLapper nbrowser_press_key '{"sessionId":"s1","key":"Enter"}' # Enter/Tab/ArrowDown/…+adom-bridge-cli -t AdomLapper nbrowser_hover '{"sessionId":"s1","text":"Hover me"}' # fires :hover + tooltips+adom-bridge-cli -t AdomLapper nbrowser_hover '{"sessionId":"s1","selector":".info-icon"}' # or by selector (surest for hover)+adom-bridge-cli -t AdomLapper nbrowser_right_click '{"sessionId":"s1","selector":".ctx"}' # real context menu ``` **Hover targets are often NON-interactive** (info icons, cards, tooltip triggers, plain spans). The finder first scans clickable elements (button/a/input/…); if `text` matches none, it **falls back to the@@ -314,7 +314,7 @@ see what the AI is doing and why. Mimics Hydrogen's cursor. A native `alert()`/`confirm()`/`prompt()`/`beforeunload` is **not an OS window (no hwnd)** and does **not appear in a screenshot** (Chrome renders it as browser chrome), but it **FREEZES the page's main thread**. Symptom: your clicks/typing/eval suddenly do nothing and you "can't fill the form" (real-incident: an Intuit "questionnaire saved" alert sat over the form while the AI kept trying). ABE now+incident: an Intuit "questionnaire saved" alert sat over the form while the AI kept trying). nbe now **auto-detects** it: the blocked verb returns `modal_open` (with the dialog text) and a screenshot carries a `_modal` note. **Dismiss it:** `nbrowser_handle_dialog {sessionId, accept:true}` (OK/Yes/Leave), `accept:false` (Cancel/Stay), or `text:"…"` to answer a prompt - then re-screenshot and continue. If an@@ -325,7 +325,7 @@ Three ways to "see", and the choice matters: - **The PAGE → `nbrowser_screenshot {sessionId}`** (CDP). Your DEFAULT. Fast, background-safe, captures the page bitmap. But it shows **page content ONLY** - no browser chrome (URL/tabs) and **no native popups/ dialogs** (Chrome's saved-password dropdown, print dialog, `alert`/`confirm`) which render OUTSIDE the page.-- **The WINDOW + chrome + native popups → `desktop_screenshot_window {hwnd | titleContains}`** (AD, WGC).+- **The WINDOW + chrome + native popups → `desktop_screenshot_window {hwnd | titleContains}`** (ab, WGC). Use this when you need the URL bar, a `chrome://` page, or a native popup/dialog. It captures the window **plus its OWNED CHILD windows and returns the full parent/child hwnd array** - the same technique that makes KiCad/Fusion error dialogs visible. Background-safe (no foregrounding). `nbrowser_open_window`@@ -334,9 +334,9 @@ Three ways to "see", and the choice matters: other work, is imprecise, and usually implies foregrounding. There is always a targeted option above. (`nbrowser_screenshot_full_res` is full-**PAGE**, not full-monitor - it still only sees the page.) ```bash-adom-desktop -t AdomLapper nbrowser_screenshot '{"sessionId":"s1","toFile":true}' # PAGE bitmap → laptop PNG path-adom-desktop -t AdomLapper desktop_screenshot_window '{"hwnd":<from open_window>}' # WINDOW + chrome + child popups-# then: adom-desktop -t AdomLapper pull_file '{"filePaths":["<path>"],"saveTo":"/tmp"}'+adom-bridge-cli -t AdomLapper nbrowser_screenshot '{"sessionId":"s1","toFile":true}' # PAGE bitmap → laptop PNG path+adom-bridge-cli -t AdomLapper desktop_screenshot_window '{"hwnd":<from open_window>}' # WINDOW + chrome + child popups+# then: adom-bridge-cli -t AdomLapper pull_file '{"filePaths":["<path>"],"saveTo":"/tmp"}' ``` Screenshots are large base64 - over the relay they can time out, so **prefer `toFile:true` + `pull_file`** (binary, chunked). `screenshot_full_res` is unresized; default resizes ≤1568 for the model.@@ -352,7 +352,7 @@ at different content (that's the drift you were hitting). For a `fullPage:true` page, so those coords are page-absolute. > **🚫 NEVER foreground the window to take a screenshot.** `nbrowser_screenshot` (CDP) captures the-> page bitmap with **zero OS-window interaction**, and AD `desktop_screenshot_window` (WGC) captures a+> page bitmap with **zero OS-window interaction**, and ab `desktop_screenshot_window` (WGC) captures a > **background / occluded** window fine. The user is usually working *in* their real browser - yanking > it forward to grab a shot disrupts them. See [Do NOT foreground the window](#do-not-foreground-the-window-to-look-at-it--capture-in-place). @@ -360,7 +360,7 @@ page, so those coords are page-absolute. Paint glanceable status on the **real browser window's Windows taskbar button** (the hwnd is captured at `open_window`; an **Adom overlay badge** goes on automatically while driving - a trust tell). It lands on **opened windows at open, and on adopted SPAWNED windows** when you call `nbrowser_owned` /-`nbrowser_list_windows` (that is when the bridge learns their hwnd). Driven via AD's `desktop_taskbar`.+`nbrowser_list_windows` (that is when the bridge learns their hwnd). Driven via ab's `desktop_taskbar`. **TWO badges, two meanings (v0.1.45+).** The user must be able to tell "an AI is reading/clicking this window" apart from "Adom merely opened it":@@ -433,10 +433,10 @@ USER closes it themselves, the bridge notices within ~30s, drops the window and ## OS-level window inventory (`nbrowser_os_windows`) + extension-state diagnosis `nbrowser_os_windows {}` returns every Chrome/Edge/pup window as the LAPTOP sees it (hwnd, title, rect,-z), annotated with `abeSession` (the live ABE session that owns it - NEVER force-close those) and-`kind` (`pup/chrome-for-testing` = browser_open/pup litter). It is measured bridge-side, so it works-when your own `desktop_list_windows` returns 0/null - usually AD mid-self-update-restart, or a wrong-`--target` when several ADs are connected. `ok:false` means "AD link down, data UNKNOWN", never "no+z), annotated with `nbeSession` (the live nbe session that owns it - NEVER force-close those) and+`kind` (`pup/chrome-for-testing` = pup_open/pup litter). It is measured bridge-side, so it works+when your own `desktop_list_windows` returns 0/null - usually ab mid-self-update-restart, or a wrong+`--target` when several abs are connected. `ok:false` means "ab link down, data UNKNOWN", never "no windows". Use it to find litter for the `nbrowser_force_close {hwnd}` gate, and to disambiguate "separate windows vs tabs" without asking the user. @@ -453,7 +453,7 @@ fields tell you exactly what to do. ## Activity log - "who drove what, when" (`nbrowser_activity`) Internal trails you can query when the trust signals misbehave: `{verb:"_taskbar"}` = every badge paint and flash (hwnd, which op, ok, HTTP status on failure); `{verb:"_open_announce"}` = every caption shown-to the user and its text; `{verb:"_ad_link"}` = AD-link outages/recoveries (if these show failures, the+to the user and its text; `{verb:"_ab_link"}` = ab-link outages/recoveries (if these show failures, the badge/flash are degraded and it is NOT your call's fault); `{verb:"_window_reaped"}` = windows the USER closed that we cleaned up. @@ -493,7 +493,7 @@ windows in the background. So: To say "come look" without forcing the window forward, use the taskbar **flash / overlay** above. - **Only** raise a window when the user EXPLICITLY asks to see it live ("show me", "let me watch"): ```bash-adom-desktop -t AdomLapper desktop_bring_to_front '{"titleContains":"<page title>","state":"restore"}' # or {hwnd}+adom-bridge-cli -t AdomLapper desktop_bring_to_front '{"titleContains":"<page title>","state":"restore"}' # or {hwnd} ``` ## 🎥 Recording doctrine - full-framerate background recording of the NATIVE browser@@ -504,7 +504,7 @@ PAINT-THROTTLING, not any codec.** Chrome correctly stops painting occluded / ba drops to ~5 fps ("frozen" / sped-up video). Every technique exists to keep frames flowing. *(The full cross-surface reference - Hydrogen vs pup, the foreground-rebump trick, page-side animation, the keyframe re-encode - lives in the **demo-recording** skill's `recording-mechanics.md`. CDP screencast is NOT garbage --it's pup's `browser_record_start` engine and does 20-30 fps when the page paints.)*+it's pup's `pup_record_start` engine and does 20-30 fps when the page paints.)* **When to use the NATIVE browser (this extension) to record:** only when the demo needs the **user's real session / cookies / logins** (a logged-in tool). If no login is needed, record via **Hydrogen or pup**@@ -540,7 +540,7 @@ keyframe re-encode (see recording-mechanics.md). ## Captions (two systems) - **In-page** `nbrowser_caption {text, style, position}` - `style:"subtitle"` (calm white-on-dark, the HD/web-hydrogen look) or `"alert"` (red **pulsing** banner). Tied to a page (cleared on navigation).-- **Fullscreen** AD `desktop_caption {text, position:"bottom", id}` - Win32 always-on-top over every app,+- **Fullscreen** ab `desktop_caption {text, position:"bottom", id}` - Win32 always-on-top over every app, truly unmissable; auto-expires. (`position:"bottom"` - top covers the URL bar + the debugger banner.) ## ⭐ The killer use case: chip-fetcher through the native browser@@ -548,11 +548,11 @@ EE CAD sources (**SnapEDA/SnapMagic, Ultralibrarian, Mouser, DigiKey**) gate the a **login** and often a **captcha**. `pup`'s empty controlled-Chromium hits the wall. The native browser uses the engineer's **real logged-in session**, so the STEP / KiCad symbol+footprint / Altium / Fusion `.lbr` bundles just download. Drive the chip-fetcher sourcing ladder (manufacturer-first → SnapMagic →-Mouser → DigiKey → … → LCSC last) with `nbrowser_*` instead of `browser_*` - same flow, real session, no+Mouser → DigiKey → … → LCSC last) with `nbrowser_*` instead of `pup_*` - same flow, real session, no wall. This is what changes the EE workflow. ## Pitfalls + best practices-- **Confirm connected first** (`nbrowser_status`). After `dev_reload` OR an **AD restart**, the native+- **Confirm connected first** (`nbrowser_status`). After `dev_reload` OR an **ab restart**, the native host re-launches and re-dials the bridge - expect a reconnect delay (see RECONNECT below). - **The extension dies if its profile has NO open windows** (MV3 SW sleeps) → `"not connected"`. Open a window in that profile; never close your LAST window there.@@ -562,15 +562,15 @@ wall. This is what changes the EE workflow. - **`nbrowser_eval` runs in the PAGE world** - `chrome.*` is NOT available there (only DOM/window). - **`fetch_url` is the authed shortcut** - real cookies; grabs a file behind a login without clicking. - **Run `nbrowser_help`** anytime - the bridge self-documents every verb + the flash/taskbar playbook.-- **Driving AD itself:** prefer the first-class file verbs (`read_file`/`write_file`/`list_dir`/+- **Driving ab itself:** prefer the first-class file verbs (`read_file`/`write_file`/`list_dir`/ `delete_file`, `process_list`, `net_stats`, `registry_*`) over `shell_execute` - cleaner, no quoting.- (AD 1.8.165 fixed `shell_execute`'s quote-mangling, so quoted Windows paths with spaces now work too -+ (ab 1.8.165 fixed `shell_execute`'s quote-mangling, so quoted Windows paths with spaces now work too - the old `ADOMDE~1` 8.3-short-name workaround is no longer needed.) ## RECONNECT (the one operational gotcha)-Each bridge restart (deploying new bridge code) or AD restart drops the extension; the native host then-re-dials and reconnects (currently 30-60s - being fixed AD-side with a stable host port). If-`nbrowser_status` says "not connected" but you didn't change anything, AD probably restarted to deploy a+Each bridge restart (deploying new bridge code) or ab restart drops the extension; the native host then+re-dials and reconnects (currently 30-60s - being fixed ab-side with a stable host port). If+`nbrowser_status` says "not connected" but you didn't change anything, ab probably restarted to deploy a fix - wait, then retry. If it persists, check `process_list` for **stray bridge instances** (kill all but one) and that `bridge_list` runtimePort matches the live bridge's port. @@ -579,8 +579,8 @@ The `extension/` and `bridge/` here are **source** - they run from the *laptop*, only after you push it there and reload: - **Extension** (`src/*.js`, `manifest.json`) loads unpacked from- `C:\Users\john\adom-browser-extension\extension`:- 1. `write_file` each changed file there (real quoted Windows paths work as of AD 1.8.165).+ `C:\Users\john\adom-native-browser\extension`:+ 1. `write_file` each changed file there (real quoted Windows paths work as of ab 1.8.165). 2. `nbrowser_dev_reload` - **self-completing 2-pass**: it does a soft reload, then arms a one-shot flag so the SW does a second *hard* reload on restart that re-reads disk. A single call now reliably picks up your changes (older builds needed two manual calls because the soft reload served cached files).@@ -596,11 +596,11 @@ only after you push it there and reload: ## Managing the extension's lifecycle / driving chrome:// pages → see **driving-the-desktop** CDP/`nbrowser_*` **cannot** reach `chrome://`/`edge://` pages, native OS dialogs, browser chrome, or an extension that **isn't loaded yet** - so the extension's own **install ("Load unpacked") / uninstall-("Remove") / reload**, plus anything on `edge://extensions`, is driven through AD's `desktop_*` verbs+("Remove") / reload**, plus anything on `edge://extensions`, is driven through ab's `desktop_*` verbs instead. That whole playbook - the **zero-math image-space click** path (`coordMap` + `space:"image"`), **background UI Automation** (`desktop_find_control` → `desktop_ui_click`/`desktop_ui_set`, no foreground), **full-screen capture that SEES popup bubbles**, modifier chords, the ghost-cursor TTL, and the-proven uninstall→reinstall recipe (validated on John's Edge 2026-06-23, AD 1.8.175) - lives in the+proven uninstall→reinstall recipe (validated on John's Edge 2026-06-23, ab 1.8.175) - lives in the **[driving-the-desktop](../driving-the-desktop/SKILL.md)** sub-skill. Reach for it whenever the user wants to install/uninstall/reload the extension or you need to touch a surface CDP can't. @@ -609,4 +609,4 @@ Extension is loaded unpacked into the user's profile (Chrome 137+ removed `--loa one-time `chrome://extensions → Developer mode → Load unpacked`; production = Chrome Web Store force-install). Bridge: `bridge_install '{"manifestUrl":"…native-browser-bridge-manifest.json"}'` → `bridges-cache`; native-messaging host registers in HKCU (Chrome + Edge); `persistent:true` keeps one-bridge alive on a stable port. See `install/`, `AD-REQUESTS.md`, `TASKBAR-FEATURE.md`.+bridge alive on a stable port. See `install/`, `ab-REQUESTS.md`, `TASKBAR-FEATURE.md`.
skills/installing-the-extension/SKILL.md+18−18@@ -1,15 +1,15 @@ --- name: installing-the-extension-description: "How WE (this repo) install the Adom browser extension into the user's real Chrome/Edge in DEVELOPER MODE (\"Load unpacked\"), driving adom-desktop's GENERIC OS verbs. This is OUR orchestration, not an adom-desktop feature - AD only exposes primitives (desktop_navigate / desktop_find_control / desktop_ui_click / desktop_ui_set / desktop_screenshot); the recipe, hardening, and recovery live here. This is the DAY-ONE install path: Chrome Web Store / Edge Add-ons review is ~2 months out, so until then dev-mode load-unpacked is how a real user gets the extension. Use when the user says \"install the extension\", \"install it for me\", \"load the extension into my browser\", \"set up the browser extension\", \"the extension isn't loaded yet\". For the low-level click mechanics (popups, the PWA guard, uninstall/reload), see driving-the-desktop. Part of the adom-native-browser."+description: "How WE (this repo) install the Adom Native Browser Extension into the user's real Chrome/Edge in DEVELOPER MODE (\"Load unpacked\"), driving Bridge's GENERIC OS verbs. This is OUR orchestration, not a Bridge feature - ab only exposes primitives (desktop_navigate / desktop_find_control / desktop_ui_click / desktop_ui_set / desktop_screenshot); the recipe, hardening, and recovery live here. This is the DAY-ONE install path: Chrome Web Store / Edge Add-ons review is ~2 months out, so until then dev-mode load-unpacked is how a real user gets the extension. Use when the user says \"install the extension\", \"install it for me\", \"load the extension into my browser\", \"set up the browser extension\", \"the extension isn't loaded yet\". For the low-level click mechanics (popups, the PWA guard, uninstall/reload), see driving-the-desktop. Part of the adom-native-browser." --- Part of **adom-native-browser**. Low-level OS-driving mechanics: **driving-the-desktop**. Safety rules: **native-browser-safety**. # Installing the extension (dev-mode Load unpacked) - the orchestration WE own -> **Whose code is this?** Ours. adom-desktop stays **generic** - it only gives us OS primitives+> **Whose code is this?** Ours. Bridge stays **generic** - it only gives us OS primitives > (`desktop_navigate`, `desktop_find_control`, `desktop_ui_click`, `desktop_ui_set`, > `desktop_screenshot`). The sequence that turns those into "the Adom extension is now loaded and-> connected" is **this skill** (plus the repo's `install/` scripts). AD never grows a "dev-mode+> connected" is **this skill** (plus the repo's `install/` scripts). ab never grows a "dev-mode > install" verb; we own and harden the recipe. **This is the day-one path** until the Chrome Web Store / > Edge Add-ons listings clear review (budget ~2 months for the broad-permission set), so it has to be > clean and repeatable, and we keep iterating it.@@ -22,32 +22,32 @@ The extension's `manifest.json` pins a `key`, so its ID is the **same on every m getting "Load unpacked" to pick the right folder; everything else is deterministic. ## Preconditions (do these once, before driving any UI)-1. **The `extension/` folder is on the laptop** at a known path (e.g. `C:\Users\<user>\adom-browser-extension\extension`). Get it there from the package/repo (`push_file` the folder, or the user clones it). Load-unpacked reads the **on-disk** build, so this path is what the picker must select.+1. **The `extension/` folder is on the laptop** at a known path (e.g. `C:\Users\<user>\adom-native-browser\extension`). Get it there from the package/repo (`push_file` the folder, or the user clones it). Load-unpacked reads the **on-disk** build, so this path is what the picker must select. 2. **The native-messaging host is registered** (`install/install.ps1`, or its steps): host manifest written with `allowed_origins` = the pinned ID, and `HKCU\...\NativeMessagingHosts\inc.adom.native_browser` pointing at it (Chrome and/or Edge). No UAC.-3. **The bridge is installed** in AD's cache (`bridge_install`, once). See the install-flow doc.+3. **The bridge is installed** in ab's cache (`bridge_install`, once). See the install-flow doc. ### ⚠ The host's Node - do NOT assume system Node exists (SDK runtime contract)-The bridge (`server.js`) is spawned by adom-desktop, so **AD guarantees its Node** (v1.9.63+ runtime-contract: system Node if on PATH, else a portable no-UAC copy AD provisions). **But the-native-messaging host is launched by CHROME, not AD**, so it does NOT inherit AD's managed-runtime+The bridge (`server.js`) is spawned by Bridge, so **ab guarantees its Node** (the SDK runtime+contract: system Node if on PATH, else a portable no-UAC copy ab provisions). **But the+native-messaging host is launched by CHROME, not ab**, so it does NOT inherit ab's managed-runtime PATH. A machine with **no system Node** would then fail to start the host (`'node' is not recognized`) even though the bridge is healthy - the extension would connect and immediately drop. This is handled automatically, and you usually do nothing:-- **The bridge publishes the exact Node AD gave it** (`process.execPath`) to+- **The bridge publishes the exact Node ab gave it** (`process.execPath`) to `~/.adom/bridges/native-browser/node-path.txt` every time it starts (bridge >= 0.0.22). - **`native-host.bat` reads that file first**, falls back to `node` on PATH only if it's absent. Since the bridge always runs before the extension is installed, the file is already there by first host- launch. Self-healing: an AD Node upgrade rewrites it on the next bridge respawn.+ launch. Self-healing: a Bridge Node upgrade rewrites it on the next bridge respawn. **If you ever need to write it explicitly** (e.g. a repair, or the host somehow launched before the-bridge ever ran): ask AD for the managed Node path and drop it in yourself -+bridge ever ran): ask ab for the managed Node path and drop it in yourself - ```-adom-desktop runtimes # -> runtimes.node.exe (source: system|cache), e.g. C:/Program Files/nodejs/node.exe+adom-bridge-cli runtimes # -> runtimes.node.exe (source: system|cache), e.g. C:/Program Files/nodejs/node.exe # then write that absolute path (one line, no newline games) to: # %USERPROFILE%\.adom\bridges\native-browser\node-path.txt ```-Never `winget install node` / prompt the user to install Node - AD already has one; reuse it.+Never `winget install node` / prompt the user to install Node - ab already has one; reuse it. ## ⭐ Verify by SCREENSHOT, not by control state (the rule for the whole flow) `desktop_find_control` / UIA state is a fast **hint** and it lies often enough to burn you: a toggle can@@ -93,10 +93,10 @@ Rules, in order: the user to pause typing for a moment, do the single action, and return immediately. Never hold the foreground, never do it silently, and never do it while the user is actively typing if there is any other way.-- ✅ **`desktop_navigate` is now focus-neutral (AD 1.9.34+).** It restores your prior foreground window+- ✅ **`desktop_navigate` is now focus-neutral (ab 1.9.34+).** It restores your prior foreground window after committing and returns an honest `navigated` signal (+ `titleChanged`). Verified live: a `chrome://extensions` navigate committed (`navigated:true`, tab title "Extensions") while my window- stayed at `z:1` and the user's window kept `z:0`. (On OLDER AD it foregrounded - the general principle+ stayed at `z:1` and the user's window kept `z:0`. (On OLDER ab it foregrounded - the general principle still holds: judge foreground by the **outcome**, `desktop_list_windows` `z:0` = topmost = you surfaced it.) Still screenshot-verify the page actually rendered, not just the `navigated` flag. @@ -135,7 +135,7 @@ desktop_ui_click {hwnd:<W>, automationId:"loadUnpacked"} # op # ensure exactly one picker; grab its hwnd <PK> via desktop_list_windows (cancel extras) desktop_bring_to_front {hwnd:<PK>} # ⭐ THE KEY: picker opens at z:1/z:2; SendInput only lands once it is z:0 desktop_press_key {hwnd:<PK>, keys:"ctrl+l"} # focus the picker address bar-desktop_type {hwnd:<PK>, text:"C:\\Users\\<user>\\adom-browser-extension\\extension"} # agent types the path - fast+desktop_type {hwnd:<PK>, text:"C:\\Users\\<user>\\adom-native-browser\\extension"} # agent types the path - fast desktop_press_key {hwnd:<PK>, keys:"enter"} # navigates INTO the folder desktop_screenshot_window {hwnd:<PK>} # VERIFY breadcrumb shows ...\extension BEFORE committing desktop_ui_click {hwnd:<PK>, automationId:"1"} # commit (Select Folder = IDOK) -> page toasts "Extension loaded"@@ -167,12 +167,12 @@ Developer-mode toggle is a shadow-DOM switch not exposed as settable - front the editor) - they never reach the picker (learned the hard way: a stray paste/Enter into the user's VS Code). Front the picker (it goes `z:0`), and `ctrl+l` -> `type` -> `enter` -> `Select Folder` works every time. This is a brief (~2s) **necessary** foreground - caption a "don't type" warning, do it fast,-clear the caption. (Step 1's `desktop_navigate` is focus-neutral on AD 1.9.34+, so this picker fill is+clear the caption. (Step 1's `desktop_navigate` is focus-neutral on ab 1.9.34+, so this picker fill is the ONLY foreground in the whole install.) ⛔ **Do NOT** use the clipboard for this: `desktop_clipboard_set` did **not** stick on a real machine (`set` -> `get` returned `""`), so a "paste the path" handoff leaves the user with an empty clipboard.-Agent-types-it-after-fronting is the method. (Clipboard verbs exist - AD 1.9.35 - but are unreliable for+Agent-types-it-after-fronting is the method. (Clipboard verbs exist - ab 1.9.35 - but are unreliable for this; flagged.) **Consent / handoff with notify (use it for the ONE gate, not the path):** before driving,
skills/native-browser-recording/SKILL.md+5−5@@ -1,6 +1,6 @@ --- name: native-browser-recording-description: "How to record full-resolution, high-framerate demo videos of a browser window in the BACKGROUND so the user keeps working. THE BEST PATH is the core adom-desktop verb desktop_record_window_start (WGC) - or nbrowser_record_start {method:\"wgc\"}, which delegates to it: no source picker, no banner, captures occluded windows, no focus theft. The extension's own getDisplayMedia/tabcapture recording is a LAST-RESORT FALLBACK (Chrome forces a manual share-source picker a human must click) and is refused unless you pass fallbackConfirmed:true. Use the native browser ONLY when a demo needs the user's real session/cookies/logins; otherwise record via pup or Hydrogen (see the demo-recording skill). Covers WGC vs the heinous getDisplayMedia picker, the paint-throttling problem, the occlusion/throttle-disable launch flags, the restart-native-Chrome-into-background-recording-mode flow (ask + warn + full process-tree kill + relaunch with profile + flags), and the offer-to-revert-for-battery step. Match on: record a browser window, record the native browser, record a logged-in demo, background recording of Chrome, nbrowser_record, desktop_record_window_start, WGC, source picker, share picker, full-framerate native recording. Part of the adom-native-browser."+description: "How to record full-resolution, high-framerate demo videos of a browser window in the BACKGROUND so the user keeps working. THE BEST PATH is the core Bridge verb desktop_record_window_start (WGC) - or nbrowser_record_start {method:\"wgc\"}, which delegates to it: no source picker, no banner, captures occluded windows, no focus theft. The extension's own getDisplayMedia/tabcapture recording is a LAST-RESORT FALLBACK (Chrome forces a manual share-source picker a human must click) and is refused unless you pass fallbackConfirmed:true. Use the native browser ONLY when a demo needs the user's real session/cookies/logins; otherwise record via pup or Hydrogen (see the demo-recording skill). Covers WGC vs the heinous getDisplayMedia picker, the paint-throttling problem, the occlusion/throttle-disable launch flags, the restart-native-Chrome-into-background-recording-mode flow (ask + warn + full process-tree kill + relaunch with profile + flags), and the offer-to-revert-for-battery step. Match on: record a browser window, record the native browser, record a logged-in demo, background recording of Chrome, nbrowser_record, desktop_record_window_start, WGC, source picker, share picker, full-framerate native recording. Part of the adom-native-browser." --- Part of **adom-native-browser** (see **driving-the-extension** for the verb surface). The broad demo pipeline (Hydrogen vs pup, scripting, TTS, storyboard, mux, wiki upload) lives in the **demo-recording**@@ -13,8 +13,8 @@ skill - this page is ONLY the part that's unique to recording the user's **real, (demo-recording skill) - those are throwaway/managed surfaces you can drive freely without disturbing the user's real browser. -## ⭐ FIRST: record with WGC (the core adom-desktop window recorder), NOT the extension-**The single best way to record a browser window is the CORE adom-desktop verb+## ⭐ FIRST: record with WGC (the core Bridge window recorder), NOT the extension+**The single best way to record a browser window is the CORE Bridge verb `desktop_record_window_start` (Windows Graphics Capture).** You can call it directly, or call `nbrowser_record_start {method:"wgc", titleContains:"<title>"|hwnd:<n>}` which delegates straight to it. WGC records ONE window in the **background, up to 60 fps, with NO source picker, NO "sharing" banner**,@@ -57,7 +57,7 @@ rendering). So enabling them is a **temporary, opt-in mode** we enter for record ## The flow - record a logged-in demo in the background 1. **Confirm you actually need the real session.** If not, go use pup/Hydrogen. 2. **Check whether native Chrome already runs with the flags** - read its launch command line- (`adom-desktop process_list {nameContains:"chrome"}` → the main `chrome.exe` with no `--type=`). pup's+ (`adom-bridge-cli process_list {nameContains:"chrome"}` → the main `chrome.exe` with no `--type=`). pup's "Chrome for Testing" already has them; the user's real Chrome usually does not. 3. **If the flags are missing → ASK + WARN, then fully restart Chrome into background-recording mode:** - Tell the user plainly: *"To record at full framerate while you keep working, I need to fully restart@@ -72,7 +72,7 @@ rendering). So enabling them is a **temporary, opt-in mode** we enter for record with `--auto-select-desktop-capture-source`). Drive the app (CDP, background) + `nbrowser_caption`. The occluded window now paints, so the capture is full-framerate and not blank. **Never foreground it.** 5. **Stop + fetch** - `nbrowser_record_stop {method:"wgc", recordingId}` (wgc is bridge-side; the stop needs- the `recordingId`, not just a sessionId). The mp4 lands on the laptop under `%TEMP%\adom-desktop-recordings`;+ the `recordingId`, not just a sessionId). The mp4 lands on the laptop under `%TEMP%\adom-bridge-recordings`; pull it with `pull_file {filePaths:["C:\\...\\window-….mp4"]}` (it saves into the container `/tmp`). 6. **OFFER TO REVERT (battery):** when the recording session is done, ask *"Chrome's in full-background recording mode, which uses more battery - want me to put it back to normal?"* → if yes, **fully restart
skills/native-browser-safety/SKILL.md+5−5@@ -1,6 +1,6 @@ --- name: native-browser-safety-description: "SAFETY rules for any agent driving the Adom browser extension (nbrowser_* verbs). This bridge drives the user's REAL, signed-in Chrome/Edge - their live tabs, forms, sessions, saved logins, and other AI threads. Read this BEFORE driving. The cardinal rule: only ever act on a window/tab YOU opened (nbrowser_open_window); NEVER navigate, reload, or close a window/tab the user already had open (this includes the OS-level desktop_navigate) - doing so destroyed a user's other Claude threads once. Also: background by default (never foreground unless asked), the CDP \"debugging\" bar, credentials/autologin are gated and never expose passwords, recording is WGC-first, and the user blocklist is hands-off. Match on: browser safety, agent browser isolation, don't touch my tabs, navigated my window, wiped my threads, never reuse a window, background by default, real browser safety, nbrowser safety, drive the real browser safely. Part of the adom-native-browser."+description: "SAFETY rules for any agent driving the Adom Native Browser Extension (nbrowser_* verbs). This bridge drives the user's REAL, signed-in Chrome/Edge - their live tabs, forms, sessions, saved logins, and other AI threads. Read this BEFORE driving. The cardinal rule: only ever act on a window/tab YOU opened (nbrowser_open_window); NEVER navigate, reload, or close a window/tab the user already had open (this includes the OS-level desktop_navigate) - doing so destroyed a user's other Claude threads once. Also: background by default (never foreground unless asked), the CDP \"debugging\" bar, credentials/autologin are gated and never expose passwords, recording is WGC-first, and the user blocklist is hands-off. Match on: browser safety, agent browser isolation, don't touch my tabs, navigated my window, wiped my threads, never reuse a window, background by default, real browser safety, nbrowser safety, drive the real browser safely. Part of the adom-native-browser." --- Part of **adom-native-browser**. Verb surface: **driving-the-extension**. Recording: **native-browser-recording**. @@ -37,12 +37,12 @@ Now they are **auto-adopted** into your session. The moment one appears: Deal with it: `nbrowser_owned {sessionId}` lists everything you own (`opened` + `spawned`); then either keep what you need or **`nbrowser_cleanup {sessionId}`** to close the litter (it closes ONLY what you spawned, never the user's own windows). **End every task** by closing what you own:-`nbrowser_close_window {sessionId, includeSpawned:true}`. If you ever find a TRUE orphan ABE can't+`nbrowser_close_window {sessionId, includeSpawned:true}`. If you ever find a TRUE orphan nbe can't attribute, `nbrowser_force_close` is a deliberately hard, two-step last resort (it can never close another thread's window). It has TWO forms behind the SAME gate (ticket → attest with a written reason): **`{windowId}`** for windows the extension can see, and **`{hwnd}`** (v0.1.23+, from `desktop_list_windows`) for browser windows the extension CANNOT reach — e.g. Chrome/Edge litter that a-`browser_open`/pup call or an HD test left behind, or a window in a profile with no extension. The hwnd+`pup_open`/pup call or an HD test left behind, or a window in a profile with no extension. The hwnd form only targets real browser windows and sends a graceful WM_CLOSE. So cleaning up ANOTHER tool's browser litter is allowed — but only through this gate, with a real justification, and never a window a live thread owns.@@ -119,7 +119,7 @@ their keystrokes (real incident: it typed a user's chat message into a username Chrome's password dropdown, twice). So a bare `userRequestedForeground:true` is **no longer enough** - without a `foregroundReason` the window **stays background**. To genuinely foreground (some auth pages / browser features truly need it):-1. **Warn the user first** with an AD notify: `adom-desktop notify_user {message:"Adom is about to bring a browser window forward for <reason>"}`.+1. **Warn the user first** with a Bridge notify: `adom-bridge-cli notify_user {message:"Adom is about to bring a browser window forward for <reason>"}`. 2. Then pass **`userRequestedForeground:true` + `foregroundReason:"<why>"`**. 3. Do the ONE thing that needed the foreground, then return to the background so you stop capturing focus. Every justified raise is logged as a `foreground_raised` event. And **never** fall back to an OS-level@@ -155,7 +155,7 @@ guarantee and **cannot be hidden on consumer Chrome** (Adom-managed machines lau `nbrowser_profile_unblock`. That is the user telling you no. Never work around it. ## Recording: WGC first (not the extension's picker)-To record a browser window, use the core adom-desktop verb `desktop_record_window_start` (WGC), or+To record a browser window, use the core Bridge verb `desktop_record_window_start` (WGC), or `nbrowser_record_start {method:"wgc"}` which delegates to it: background, high-fps, no picker, no banner, captures occluded, no focus theft. The extension's own `getdisplaymedia`/`tabcapture` path is a refused-by-default fallback (Chrome forces a manual share picker). Full detail: **native-browser-recording**.
skills/uninstalling-the-extension/SKILL.md+4−4@@ -1,13 +1,13 @@ --- name: uninstalling-the-extension-description: "How WE (this repo) uninstall the Adom browser extension from the user's real Chrome/Edge, driving adom-desktop's GENERIC OS verbs - BACKGROUND-first, never stealing focus. Covers the critical safety guard (all \"Remove\" buttons share the same name, so filter the list to the Adom card first or you can remove the WRONG extension), the Remove-confirm bubble (a Chromium owned popup that needs ONE brief foreground beat because it dies on focus loss), verifying removal by screenshot AND by the nbrowser_ profile key dropping, and optional full cleanup (native-messaging host + bridge). Use when the user says \"uninstall the extension\", \"remove the Adom extension\", \"take it out of my browser\", \"reinstall it\" (uninstall then installing-the-extension). Low-level click mechanics: driving-the-desktop. Safety: native-browser-safety. Part of the adom-native-browser."+description: "How WE (this repo) uninstall the Adom Native Browser Extension from the user's real Chrome/Edge, driving Bridge's GENERIC OS verbs - BACKGROUND-first, never stealing focus. Covers the critical safety guard (all \"Remove\" buttons share the same name, so filter the list to the Adom card first or you can remove the WRONG extension), the Remove-confirm bubble (a Chromium owned popup that needs ONE brief foreground beat because it dies on focus loss), verifying removal by screenshot AND by the nbrowser_ profile key dropping, and optional full cleanup (native-messaging host + bridge). Use when the user says \"uninstall the extension\", \"remove the Adom extension\", \"take it out of my browser\", \"reinstall it\" (uninstall then installing-the-extension). Low-level click mechanics: driving-the-desktop. Safety: native-browser-safety. Part of the adom-native-browser." --- Part of **adom-native-browser**. Install: **installing-the-extension**. Low-level OS mechanics: **driving-the-desktop**. Safety: **native-browser-safety**. # Uninstalling the extension - the orchestration WE own (background-first) -Like install, this is **our** recipe driving adom-desktop's **generic** OS verbs (`desktop_navigate`,-`desktop_find_control`, `desktop_ui_click`, `desktop_ui_set`, `desktop_screenshot_window`). AD has no+Like install, this is **our** recipe driving Bridge's **generic** OS verbs (`desktop_navigate`,+`desktop_find_control`, `desktop_ui_click`, `desktop_ui_set`, `desktop_screenshot_window`). ab has no "uninstall extension" feature; we own the sequence and its safety. ## ⛔⛔ Two rules that make or break this@@ -75,7 +75,7 @@ desktop_caption {text:""} # clear - **Just unloading vs full removal:** removing the card unloads the extension and drops the SW. To fully decommission (not just unload), also: remove the HKCU native-messaging host (`HKCU\Software\Google\Chrome\NativeMessagingHosts\inc.adom.native_browser`, and the Edge key), and if- the user wants the bridge gone too, `adom-desktop bridge_uninstall {name:"native-browser"}`. For a plain+ the user wants the bridge gone too, `adom-bridge-cli bridge_uninstall {name:"native-browser"}`. For a plain "take it out of my browser," Steps 1-4 are enough. ## Recovery
store/PERMISSION-JUSTIFICATIONS.md+1−1@@ -37,7 +37,7 @@ A concrete, representative task (use this verbatim if a reviewer asks "why so mu | Permission | Justification (paste into store) | |---|---|-| **nativeMessaging** | Connects the Extension to the user's local Adom Desktop application over Chrome's native-messaging channel, so the user's AI session can send commands and receive results. This is the core transport; without it the Extension cannot function. |+| **nativeMessaging** | Connects the Extension to the user's local Bridge application over Chrome's native-messaging channel, so the user's AI session can send commands and receive results. This is the core transport; without it the Extension cannot function. | | **debugger** | Uses the Chrome DevTools Protocol on the active tab to dispatch trusted (isTrusted) input, capture full-page screenshots, evaluate and debug JavaScript, and read console/network logs — the exact capabilities a developer or AI coding assistant needs to build, drive, and test web applications. Attached only while an action runs and detached when idle. | | **scripting** | Injects helper scripts to read the DOM, locate and fill form fields, and report page state when carrying out a user-requested action. | | **tabs** | Opens, closes, switches, reloads, and lists tabs the user asks the AI to operate, and maps the user's session to the correct tab. |
store/STORE-LISTING.md+3−3@@ -44,7 +44,7 @@ on your machine. your own commands — it does not crawl your browsing, does not sell data, and never decrypts your saved passwords (your browser's own autofill does that). See our Privacy Policy. -**Requires:** the Adom Desktop application on your computer and an Adom account. Learn more at+**Requires:** the Bridge application on your computer and an Adom account. Learn more at https://adom.inc and wiki.adom.inc. ## Screenshots to capture (1280×800 or 640×400; need 1–5)@@ -55,6 +55,6 @@ https://adom.inc and wiki.adom.inc. 5. Tab/window management or a recorded walkthrough. ## Support / homepage URLs-- Homepage: https://wiki.adom.inc/adom/adom-browser-extension+- Homepage: https://wiki.adom.inc/adom/adom-native-browser - Support: [email protected]-- Privacy policy: https://wiki.adom.inc/adom/adom-browser-extension/files/privacy.html+- Privacy policy: https://wiki.adom.inc/adom/adom-native-browser/files/privacy.html
tools/check-guard-coverage.mjs+19−1@@ -38,5 +38,23 @@ for (const verb of Object.keys(policy)) { if (!dispatched.includes(verb)) errors.push(`STALE policy entry 'nbrowser_${verb}' — not dispatched in sw.js`); } +// MANIFEST PARITY. bridge.json's verbs[] is what ab (Bridge) reads to learn our surface: a verb we+// implement but never declare is INVISIBLE to every caller, and one we declare but never implement is a+// dead advertisement. Both drifted silently before (12 verbs, incl. nbrowser_events, were implemented+// and undeclared) because the manifest was hand-maintained. VERB_META is the source of truth; this+// asserts bridge.json mirrors it exactly, order included, so `verbs[]` can never be stale again.+const metaKeys = [...server.slice(server.indexOf('const VERB_META = {')).matchAll(/^ {2}(nbrowser_[a-z0-9_]+):\s*\{/gm)].map((m) => m[1]);+const manifest = JSON.parse(readFileSync(join(root, 'bridge/bridge.json'), 'utf8'));+const undeclared = metaKeys.filter((v) => !manifest.verbs.includes(v));+const undefinedVerbs = manifest.verbs.filter((v) => !metaKeys.includes(v));+if (undeclared.length) errors.push(`UNDECLARED in bridge/bridge.json verbs[] (implemented but ab cannot see them): ${undeclared.join(', ')}`);+if (undefinedVerbs.length) errors.push(`DEAD entries in bridge/bridge.json verbs[] (declared but no VERB_META): ${undefinedVerbs.join(', ')}`);+if (!undeclared.length && !undefinedVerbs.length && manifest.verbs.join() !== metaKeys.join()) {+ errors.push('bridge/bridge.json verbs[] is out of ORDER vs VERB_META — regenerate it from the server order');+}+if (manifest.version !== readFileSync(join(root, 'bridge/BRIDGE_VERSION'), 'utf8').trim()) {+ errors.push(`VERSION SKEW: bridge.json version ${manifest.version} != BRIDGE_VERSION — bump them in lockstep`);+}+ if (errors.length) { console.error('guard-coverage FAILED:\n ' + errors.join('\n ')); process.exit(1); }-console.log(`guard-coverage OK: ${dispatched.length} verbs classified, ${Object.values(policy).filter((c) => c === 'mutating').length} mutating verbs gated, ${dispatched.filter((v) => policy[v] !== 'alias').length} verbs with non-empty hint + pitfalls`);+console.log(`guard-coverage OK: ${dispatched.length} verbs classified, ${Object.values(policy).filter((c) => c === 'mutating').length} mutating verbs gated, ${dispatched.filter((v) => policy[v] !== 'alias').length} verbs with non-empty hint + pitfalls, ${manifest.verbs.length} verbs declared to ab in bridge.json`);
wiki/page.json+11−6@@ -1,10 +1,14 @@ {- "slug": "adom-browser-extension",+ "slug": "adom-native-browser", "org": "adom",- "title": "Adom Browser Extension (Chrome + Edge)",- "summary": "Drive your real, logged-in Chrome/Edge from the Adom ecosystem \u2014 the native-browser counterpart to pup. Same nbrowser_* surface as the puppeteer bridge, but inside the actual signed-in profile so cookies, SSO, saved logins, and captcha human-trust all work.",+ "title": "Adom Native Browser",+ "summary": "Drive your real, logged-in Chrome/Edge from the Adom ecosystem — the native-browser counterpart to pup. Same nbrowser_* surface as the puppeteer bridge, but inside the actual signed-in profile so cookies, SSO, saved logins, and captcha human-trust all work.", "category": "Browser Automation", "tags": [+ "ab",+ "native-browser",+ "nbe",+ "nb", "browser", "extension", "chrome",@@ -12,7 +16,7 @@ "nbrowser", "pup", "native-messaging",- "adom-desktop",+ "bridge", "bridge" ], "status": "live",@@ -35,5 +39,6 @@ "type": "image", "path": "screenshots/hero.png" },- "visibility": "public"-}+ "visibility": "public",+ "brief": "Control of the user's REAL signed-in browser (Chrome/Edge), in two layers. Layer 1 (nb) opens URLs into their native browser and works on day one with nothing installed, the OAuth workhorse. Layer 2 (nbe, the Adom Native Browser Extension) lets the AI fully drive that browser: navigate, click, read, screenshot, real logins."+}