← Commit history
v0.0.19: rich _hint on every verb + hint/related/pitfalls in describe (issue #16); bundle native-browser-safety skill that installs (issue #17)
9 files changed
+161−15
bridge/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.0.18+0.0.19
bridge/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "native-browser", "displayName": "Adom Native Browser (Chrome + Edge)",- "version": "0.0.18",+ "version": "0.0.19", "description": "Drives the user's REAL signed-in Chrome/Edge via an installed extension (native-messaging transport). Mirrors the puppeteer bridge's verb surface as nbrowser_* but runs inside the actual logged-in profile, so cookies/SSO/saved-logins/captcha human-trust all work. The bridge binds an ephemeral loopback port (PORTS.md compliant) and writes a discovery file; the native-messaging host pumps stdio <-> this bridge; the extension never touches a TCP port.", "homepage": "https://wiki.adom.inc/adom/adom-browser-extension", "author": "Adom Inc.",
bridge/package.json+1−1@@ -1,6 +1,6 @@ { "name": "adom-native-browser-bridge",- "version": "0.0.18",+ "version": "0.0.19", "private": true, "description": "adom-desktop bridge for the Adom native-browser extension (Chrome + Edge)", "main": "server.js",
bridge/server.js+75−3@@ -394,8 +394,78 @@ function flashHint() { return `${FLASH_HINT}${flashMode}. Get the user's attention: nbrowser_flash {sessionId}. The USER may prefer more/less noise - nbrowser_flash_mode {mode:"quiet"|"chatty"|"off"} (quiet=come-look moments only [default]; chatty=every action, for users who want to watch; off=silent). On a human-in-the-loop gate (captcha/2FA/login), nbrowser_flash so they come help. Never leave it strobing - it auto-clears on focus, or nbrowser_flash_stop.`; } +// ============================================================================================+// 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+// 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+// every navigate/reload/close/window verb: only ever act on a window/tab YOU opened (nbrowser_open_window).+// Never touch one the user already has open - that can wipe their live work and other AI threads' tabs.+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.';+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_ping: { hint: 'Round-trip liveness through to the extension.', related: ['nbrowser_status'], pitfalls: [] },+ nbrowser_open_window: { hint: 'Open a NEW background window YOU own and get a sessionId; do ALL your work through that sessionId. This is how you get a surface to drive WITHOUT touching the user\'s own windows.', related: ['nbrowser_navigate', 'nbrowser_screenshot', 'nbrowser_close_window'], next: 'nbrowser_navigate', pitfalls: ['opens UNFOCUSED - do NOT pass focused:true unless the user asked to watch', 'this is the safe way to work: always drive a window you opened, never the user\'s existing ones'] },+ nbrowser_close_window: { hint: 'Close a window you opened. Clean up your own sessions when done.', related: ['nbrowser_list_windows'], pitfalls: ['⛔ only close a sessionId YOU opened - closing the user\'s window can wipe their live work and other AI threads'] },+ nbrowser_switch_window: { hint: 'Target a window for later verbs. Does NOT raise it.', related: ['nbrowser_list_windows'], pitfalls: ['does not foreground (by design); pass focused:true ONLY on explicit user request'] },+ nbrowser_list_windows: { hint: 'List live windows/sessions with bounds+state.', related: ['nbrowser_switch_window', 'nbrowser_rescan'], pitfalls: [] },+ nbrowser_window_state: { hint: 'Maximize/fullscreen/normal/minimize or move+resize.', related: ['nbrowser_fullscreen'], pitfalls: ['resizing a window the USER is in disrupts them; act on your own session windows'] },+ nbrowser_fullscreen: { hint: 'Fullscreen the window (hides tabs/URL bar) - great for clean recordings.', related: ['nbrowser_window_state'], pitfalls: ['F11-via-key does NOT work; use this verb'] },+ nbrowser_login_state: { hint: 'Is the profile signed in to a URL right now (via chrome.cookies)?', related: ['nbrowser_credentials'], pitfalls: [] },+ nbrowser_credentials: { hint: 'Read-only QUERY of the browser\'s own saved-login store (which vendors the user can auto-login to). Never returns passwords.', related: ['nbrowser_login_state', 'nbrowser_navigate'], pitfalls: ['if a vendor is not listed, the user has no saved login - ask them to save one once', 'never exposes the password; Chrome autofills it when you focus the field'] },+ nbrowser_profiles: { hint: 'List every BROWSER x PROFILE you can drive + which is active. Drive several in parallel with profile:"<key>".', related: ['nbrowser_use_profile', 'nbrowser_profile_block'], pitfalls: ['active is only the default when no profile is passed'] },+ nbrowser_use_profile: { hint: 'Set the default profile that verbs target.', related: ['nbrowser_profiles'], pitfalls: ['you can still override per-call with profile:"<email>"'] },+ nbrowser_profile_block: { hint: 'Blocklist a profile so it is NEVER driven (user safety whitelist; persists).', related: ['nbrowser_profile_unblock', 'nbrowser_profiles'], pitfalls: ['respect the blocklist - it is the user telling you hands-off'] },+ nbrowser_profile_unblock: { hint: 'Remove a profile from the blocklist.', related: ['nbrowser_profile_block'], pitfalls: [] },+ nbrowser_rescan: { hint: 'Reconcile the session map against live windows (drop closed).', related: ['nbrowser_list_windows'], pitfalls: [] },+ nbrowser_open_tab: { hint: 'Open a background tab in one of YOUR session windows.', related: ['nbrowser_close_tab', 'nbrowser_list_tabs'], pitfalls: ['opens active:false (background); open tabs in a window you created, not the user\'s'] },+ nbrowser_close_tab: { hint: 'Close a tab you opened.', related: ['nbrowser_list_tabs'], pitfalls: ['⛔ only close a tab YOU opened - closing the user\'s tab destroys their live page/session'] },+ nbrowser_switch_tab: { hint: 'Make a tab active within its window.', related: ['nbrowser_list_tabs'], pitfalls: [] },+ nbrowser_list_tabs: { hint: 'List tabs (tabId/url/title/active).', related: ['nbrowser_switch_tab'], pitfalls: [] },+ nbrowser_navigate: { hint: 'Navigate a tab to a URL. ' + OWN_ONLY, related: ['nbrowser_open_window', 'nbrowser_screenshot', 'nbrowser_wait'], next: 'nbrowser_screenshot', pitfalls: [OWN_ONLY, 'navigation can race - verify with nbrowser_screenshot before asserting success'] },+ nbrowser_reload: { hint: 'Reload a tab. ' + OWN_ONLY, related: ['nbrowser_navigate'], pitfalls: ['⛔ never reload a window the user is in - you lose their unsaved state; reload only your own session'] },+ nbrowser_back: { hint: 'History back on your own session tab.', related: ['nbrowser_forward'], pitfalls: ['only on a window you opened'] },+ nbrowser_forward: { hint: 'History forward on your own session tab.', related: ['nbrowser_back'], pitfalls: ['only on a window you opened'] },+ nbrowser_wait: { hint: 'Sleep server-side (let a page settle) before the next verb.', related: ['nbrowser_screenshot'], pitfalls: [] },+ nbrowser_screenshot: { hint: 'CDP screenshot - captures a BACKGROUND/occluded window WITHOUT foregrounding it. This is how you "see" a page; never pull a window forward to grab a shot.', related: ['nbrowser_eval', 'nbrowser_click'], pitfalls: ['resized <=1568 for the model; pass toFile:true to save full-res to a laptop file (pull_file it)'] },+ nbrowser_screenshot_full_res: { hint: 'Full-resolution / full-page screenshot via CDP.', related: ['nbrowser_screenshot'], pitfalls: ['large base64 can be slow over the relay - prefer toFile then pull_file'] },+ 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'] },+ nbrowser_click: { hint: 'Click by visible text / selector / xy (isTrusted). Optional gliding cursor + intent label.', related: ['nbrowser_screenshot', 'nbrowser_type'], next: 'nbrowser_screenshot', pitfalls: ['attaches the debugger (yellow "debugging" bar; idle-detaches ~8s)', 'verify it landed with a screenshot - SPAs can swallow synthetic-looking events'] },+ 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.).', related: ['nbrowser_type'], pitfalls: [] },+ 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_errors: { hint: 'Buffered page/console errors for a tab.', related: ['nbrowser_eval'], pitfalls: [] },+ nbrowser_close: { hint: 'Not an extension verb - close a specific window/tab instead.', related: ['nbrowser_close_window', 'nbrowser_close_tab'], pitfalls: ['there is no global detach-all; the debugger idle-detaches on its own'] },+ 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: [] },+ nbrowser_record_list: { hint: 'List recent recordings.', related: ['nbrowser_record_status'], pitfalls: [] },+ 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: [] },+ nbrowser_flash_mode: { hint: 'Set auto-flash verbosity (quiet/chatty/off).', related: ['nbrowser_flash'], pitfalls: [] },+ nbrowser_taskbar: { hint: 'Paint progress/overlay/flash on the window taskbar button (e.g. paused=yellow for a human gate).', related: ['nbrowser_flash'], pitfalls: ['clear everything at task end: {progress:{state:"none"},overlay:{badge:"none"},flash:"stop"}'] },+ nbrowser_describe: { hint: 'Returns per-verb schemas + hint/related/pitfalls for AD\'s Verbs tab.', related: ['nbrowser_help'], pitfalls: [] },+};+// Attach a rich `_hint` (+ related/_next) to EVERY response from VERB_META, so an AI calling cold is+// always told what it got, what to do next, and what to avoid - without ever reading docs first.+function decorateHint(verb, r) {+ if (!r || typeof r !== 'object') return r;+ const m = VERB_META[verb];+ if (!m) return r;+ if (r._hint == null && m.hint) r._hint = m.hint; // never clobber a more-specific hint+ if (r.success !== false) { // chaining aids only on success+ if (r.related == null && m.related && m.related.length) r.related = m.related;+ if (r._next == null && m.next) r._next = m.next;+ }+ return r;+}+ function dispatch(verb, args) {- return new Promise((resolve) => {+ return new Promise((rawResolve) => {+ const resolve = (r) => rawResolve(decorateHint(verb, r)); if (verb === 'nbrowser_help') { return resolve({ success: true, output: JSON.stringify({ ok: true, bridge: BRIDGE, version: VERSION, extConnected: anyConnected(), flashMode, ...HELP }) }); }@@ -405,8 +475,10 @@ function dispatch(verb, args) { if (verb === 'nbrowser_describe') { const S = 'optional sessionId of a window opened via nbrowser_open_window'; const P = 'optional profile key "chrome:<email>" / "edge:<email>" (default = active)';- const d = (name, summary, input, output, timeoutSeconds, opts = {}) =>- ({ name, summary, input, output, timeoutSeconds, statusVerb: opts.statusVerb || null, longRunning: !!opts.longRunning, example: opts.example || {} });+ const d = (name, summary, input, output, timeoutSeconds, opts = {}) => {+ const m = VERB_META[name] || {};+ return { name, summary, input, output, timeoutSeconds, statusVerb: opts.statusVerb || null, longRunning: !!opts.longRunning, example: opts.example || {}, hint: m.hint || summary, related: m.related || [], pitfalls: m.pitfalls || [] };+ }; const verbs = [ d('nbrowser_status', 'Is the extension connected? Reports ext id/version + active browser.', {}, { ok: 'bool', ext: 'str', version: 'str', engine: 'str', browser: '"chrome"|"edge"' }, 15), d('nbrowser_ping', 'Round-trip liveness check through to the extension.', {}, { ok: 'bool', pong: 'bool' }, 15),
install.sh+1−1@@ -3,7 +3,7 @@ # (The extension itself loads into Chrome/Edge via "Load unpacked" from extension/, or the Web Store.) set -e DEST="${HOME}/.claude/skills"-for name in cross-platform-dev driving-the-extension native-browser-recording driving-the-extension-demo driving-the-desktop driving-sites driving-gusto reading-google-messages; do+for name in cross-platform-dev driving-the-extension native-browser-safety native-browser-recording driving-the-extension-demo driving-the-desktop driving-sites driving-gusto reading-google-messages; do mkdir -p "$DEST/$name" cp "skills/$name/SKILL.md" "$DEST/$name/SKILL.md" echo "installed skill: $name"
native-browser-bridge-manifest.json+5−5@@ -1,8 +1,8 @@ { "manifest_version": 1,- "version": "0.0.18",- "url": "native-browser-bridge-v0.0.18.bin",- "sha256": "b6af866e5f4b1967b24f264a9d669389743770964574b933ed9aaf5261bb9a6d",- "size": 24297,- "released_at": "2026-06-27T19:49:25Z"+ "version": "0.0.19",+ "url": "native-browser-bridge-v0.0.19.bin",+ "sha256": "4cef2a4e27dc73c461c819457a603aeaada86d0bb8251e249a4bdaa63219680d",+ "size": 27226,+ "released_at": "2026-06-27T20:47:59Z" }
package.json+3−2@@ -1,9 +1,9 @@ { "slug": "adom-browser-extension",- "version": "0.3.8",+ "version": "0.3.9", "type": "app", "title": "Adom Browser Extension (Chrome + Edge)",- "description": "Drive your real, logged-in Chrome/Edge from the Adom ecosystem \u2014 the nbrowser_* native-browser bridge (counterpart to pup). Multi-profile driving, a read-only credential-library query over the browser's own saved logins (Chrome + Edge), and hands-free vendor login automation, so chip CAD / datasheets download past every login wall in your real signed-in session.",+ "description": "Drive your real, logged-in Chrome/Edge from the Adom ecosystem: the nbrowser_* native-browser bridge (counterpart to pup). Multi-profile driving, a read-only credential-library query over the browser's own saved logins (Chrome + Edge), and hands-free vendor login automation, so chip CAD / datasheets download past every login wall in your real signed-in session.", "install": { "binary_name": "adom-browser-extension" },@@ -15,6 +15,7 @@ "README.md", "skills/cross-platform-dev/SKILL.md", "skills/driving-the-extension/SKILL.md",+ "skills/native-browser-safety/SKILL.md", "skills/native-browser-recording/SKILL.md", "skills/driving-the-extension-demo/SKILL.md", "skills/driving-the-desktop/SKILL.md",
skills/native-browser-safety/SKILL.mdadded+73@@ -0,0 +1,73 @@+---+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-browser-extension.+---+Part of **adom-browser-extension**. Verb surface: **driving-the-extension**. Recording: **native-browser-recording**.++# Driving the user's REAL browser safely++`nbrowser_*` is not pup. It runs **inside the user's actual, signed-in Chrome/Edge**, so every action+lands on their live browser: open tabs they are reading, forms they are filling, authenticated+sessions, saved logins, and **other AI threads' tabs**. That power is the whole point (it sails+through logins and captchas pup cannot), and it is exactly why a careless move is destructive. These+rules are not optional polish; one of them exists because an agent wiped a user's other Claude threads.++## ⛔ Cardinal rule: only ever drive a window/tab YOU opened+**Open your own surface and work only in it.** `nbrowser_open_window {url}` gives you a background+window + a `sessionId`; do ALL your driving through that `sessionId`.++**NEVER** navigate, reload, close, or repurpose a window or tab the user already had open:+- not with `nbrowser_navigate` / `nbrowser_reload` / `nbrowser_close_tab` / `nbrowser_close_window`,+- and **not with the OS-level `desktop_navigate` either** (same destruction, different door).++Why it is this strict: navigating a user's active tab yanks their live page out from under them and+**wipes anything unsaved - including other running AI threads**. That actually happened. There is no+"probably fine" version of this. If you need a page, open a new window and use that.++The only thing you may do to one of the user's existing windows is `desktop_bring_to_front` to surface+it for them - and only when they explicitly ask to see it.++## ⛔ Background by default - never foreground unless asked+The user is working in other windows. The verbs are built to stay out of their way:+`nbrowser_open_window` opens **unfocused**, `nbrowser_open_tab` opens `active:false`,+`nbrowser_switch_window` does **not** raise. **Leave those defaults alone.** You can fully see and+drive a window without foregrounding it: `nbrowser_screenshot` (CDP) and `desktop_screenshot_window`+(WGC) both capture a **background/occluded** window, and WGC records one too. So there is never a+reason to pull a window forward. Foregrounding is the **user's** choice (`desktop_bring_to_front`),+made by them, only when they want to watch.++## The "debugging this browser" bar is expected, not a bug+CDP verbs (`click`, `type`, `screenshot`, `eval`, ...) attach `chrome.debugger`, which makes Chrome+show the yellow `"Adom for Chrome & Edge" started debugging this browser` bar. The bridge+lazy-attaches only for those verbs and idle-detaches ~8s later, so it appears while driving and+clears when idle. It is a Chrome security guarantee and **cannot be hidden on consumer Chrome**. If+you only need to read/navigate your own window, prefer the native verbs (no bar).++## Credentials and auto-login are gated - and you never see secrets+- `nbrowser_credentials` is a **read-only query** of which vendors the user has a saved login for. It+ **never returns passwords**; Chrome autofills those when you focus the field.+- Auto-login is **opt-in per call** (`confirm:true` + an explicit `host:`) so it can never fire for an+ arbitrary site. The password is decrypted on the laptop and injected by the extension - the agent+ never sees it.+- Treat everything you read from the user's logged-in sessions (`fetch_url`, page content, `eval`+ results) as **sensitive**. Do not exfiltrate it; use it for the task at hand.++## Respect the user's blocklist+`nbrowser_profile_block` marks a profile **hands-off**; the bridge refuses to drive it until+`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+`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**.++## Every verb tells you how to behave - read the hints+Every `nbrowser_*` response carries a `_hint` (what you got, what to do next, the trap to avoid), and+`nbrowser_describe` lists each verb's `hint` / `related` / `pitfalls`. When unsure, call+`nbrowser_describe` once and read the `pitfalls` - they encode these safety rules per verb.++## The one-line version+Open your own window, work in the background, leave the user's tabs and focus untouched, keep their+secrets secret, honor the blocklist, and record with WGC. If a move would change something the user+did not ask you to change, don't.
uninstall.sh+1−1@@ -1,5 +1,5 @@ #!/usr/bin/env bash DEST="${HOME}/.claude/skills"-for name in cross-platform-dev driving-the-extension native-browser-recording driving-the-extension-demo driving-the-desktop driving-sites driving-gusto reading-google-messages; do+for name in cross-platform-dev driving-the-extension native-browser-safety native-browser-recording driving-the-extension-demo driving-the-desktop driving-sites driving-gusto reading-google-messages; do rm -rf "$DEST/$name" && echo "removed skill: $name" done