← Commit history
Update 5 file(s)
5 files changed
+51−9
bridge/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.1.22+0.1.23
bridge/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "native-browser", "displayName": "Adom Native Browser (Chrome + Edge)",- "version": "0.1.22",+ "version": "0.1.23", "expectedExtensionVersion": "0.1.2", "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",
bridge/package.json+1−1@@ -1,6 +1,6 @@ { "name": "adom-native-browser-bridge",- "version": "0.1.22",+ "version": "0.1.23", "private": true, "description": "adom-desktop bridge for the Adom native-browser extension (Chrome + Edge)", "main": "server.js",
bridge/server.js+44−2@@ -731,6 +731,7 @@ function extQuery(profileKey, verb, args, timeoutMs) { // re-badges adopted spawns. The extension query is exact (it knows its own windows), so this can never // hit another profile's window or a PWA. const _profCorrAt = new Map(); // profileKey -> last correlation attempt ts+const _osCloseTickets = new Map(); // OS-level force_close: ticket -> {hwnd, expires} function touchProfileBadge(profileKey) { if (!BADGE_ENABLED || !profileKey) return; let touched = false;@@ -1141,7 +1142,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. TWO-STEP: call once with {windowId} → get an overrideTicket + exactly what will be destroyed; call again with {windowId, overrideTicket, attestOrphan:true, reason} to actually close it. 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', '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'] },+ 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_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'] },@@ -1329,7 +1330,7 @@ function dispatch(verb, args) { 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. TWO-STEP: call 1 {windowId} returns overrideTicket + willDestroy; call 2 {windowId,overrideTicket,attestOrphan:true,reason} closes it. NEVER closes another live thread\'s window.', { windowId: 'required int', 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. 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_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' } }),@@ -1455,6 +1456,47 @@ function dispatch(verb, args) { }, 2500); 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+ // 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+ // 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) {+ const hwnd = Number(args.hwnd);+ (async () => {+ let ws = [];+ try {+ const r = await adCommand('desktop', 'desktop_list_windows', {});+ let out = r.body; if (out && typeof out.output === 'string') { try { out = JSON.parse(out.output); } catch {} }+ ws = ((out && out.data && out.data.windows) || (out && out.windows) || []).filter((w) => /Chrome_WidgetWin/.test(w.className || ''));+ } 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.' });+ 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.` }) });+ }+ 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}.' });+ if (Date.now() > t.expires) { _osCloseTickets.delete(args.overrideTicket); return resolve({ success: false, errorCode: 'ticket_expired', error: 'override ticket expired (60s)', _hint: 'Request a fresh one with just {hwnd}, then confirm promptly.' }); }+ if (args.attestOrphan !== true || !args.reason) return resolve({ success: false, errorCode: 'attestation_required', error: '⛔ you must attest {attestOrphan:true, reason:"…"} to proceed - this deliberate friction ensures force-close is never reflexive.', _hint: 'Include attestOrphan:true and a human-readable reason.' });+ _osCloseTickets.delete(args.overrideTicket);+ const psBody = [+ 'Add-Type @"',+ 'using System;using System.Runtime.InteropServices;',+ 'public class WMC { [DllImport("user32.dll")] public static extern bool PostMessage(IntPtr h, uint m, IntPtr w, IntPtr l); }',+ '"@',+ `[WMC]::PostMessage([IntPtr]${hwnd},0x0010,[IntPtr]::Zero,[IntPtr]::Zero)`,+ ].join('\r\n');+ await adCommand('desktop', 'run_script', { interpreter: 'powershell', scriptB64: Buffer.from(psBody, 'utf8').toString('base64'), timeoutSeconds: 20 });+ logActivity({ verb: 'nbrowser_force_close', sessionId: (args && args.sessionId) || null, thread: (args && args.thread) || null, profile: 'os-hwnd', ok: true });+ 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;+ } if (verb === 'nbrowser_wake_profile') { const want = String((args && (args.profile || args.email)) || ''); if (!want) return resolve({ success: false, error: 'profile required', _hint: 'Pass profile:"chrome:[email protected]" (or just the email, optionally browser:"edge"). nbrowser_profiles lists asleep profiles.' });
native-browser-bridge-manifest.json+4−4@@ -1,8 +1,8 @@ { "manifest_version": 1,- "version": "0.1.22",- "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.22/native-browser-bridge-v0.1.22.bin",- "sha256": "2122a36dc4a028a34eb3756d78101aae0c762780c67c8e6551114f6a23045366",- "size": 75205,+ "version": "0.1.23",+ "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.23/native-browser-bridge-v0.1.23.bin",+ "sha256": "355a10953b9ad63683da9a2d6e299f2b67fe85a9f63261c0926e29d771c5a454",+ "size": 77021, "released_at": "2026-07-18T00:00:00Z" }