app
Pup - the AI's own browser
Public Made by Adomby adom
pup gives your AI a real browser on your desktop that it drives completely, in the background, signed in as you: windows that never jump in front of your work, one shared profile that learns your logins, and every window labeled on the taskbar with its AI thread's name.
← Commit history
1.8.70: foreground gate — reason required for open-foreground and raise, recorded and auditable, abe-parity dual key
5 files changed
+68−12
CHANGELOG.md+17@@ -1,5 +1,22 @@ # Changelog — Adom Desktop Puppeteer Bridge +## 1.8.70 — 2026-07-15 — THE FOREGROUND GATE (abe parity): reason required, recorded, auditable++Real incident: a thread background-opened a window then raised it 2 minutes later; the raise verb+required no reason, recorded nothing, and didn't even log — forensics needed the focus-latch+timestamp. pup now gates foregrounding the way abe does (dual key, one call):++- `browser_open_window {foreground:true}` WITHOUT `foregroundReason` → the open succeeds but the+ window stays BACKGROUND, with `foregroundDenied:'foreground_reason_required'` + a hint (abe+ semantics: withhold the foregrounding, not the work). With a reason (min 10 chars): granted.+ Also accepts abe's arg name `userRequestedForeground`.+- `browser_raise_os_window`/`browser_focus_window` WITHOUT `foregroundReason` → REFUSED with+ `errorCode: foreground_reason_required` and the contract in the hint.+- Every granted foreground is logged (`[foreground] ... GRANTED — reason: ...`), recorded in+ `lastAgentUpdate` (`FOREGROUND: <reason>`), and surfaced as `lastForeground {ts, verb, reason}`+ in session status — "who put this window on my screen and why" is now one query.+- browser_describe teaches the whole contract, including return-to-background after the show-me.+ ## 1.8.69 — 2026-07-15 — Sized opens keep their x/y (multi-monitor placement works); window-management granted - The sized-open background re-assert was still the LEGACY PS park, which moves the window to 0,0
adom-bridge-puppeteer-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "1.8.69",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.8.69/adom-bridge-puppeteer-v1.8.69.zip",- "sha256": "a39875c6d23ad5630a4678096b2ea1912f849f7f7a4269ef6b89846889266d8b",- "size": 173714,+ "version": "1.8.70",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.8.70/adom-bridge-puppeteer-v1.8.70.zip",+ "sha256": "4c5551d6ac2a45d49c826f96a19a4dd65aeac77e8fa14aab5dcb51bd4253b0bd",+ "size": 175022, "verbPrefixes": [ "browser_", "desktop_recorder_",
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-1.8.69+1.8.70
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "1.8.69",+ "version": "1.8.70", "description": "Drives a browser ALREADY on the machine \u2014 installed Chrome, else Microsoft Edge (every Windows PC ships with Edge) \u2014 launched with a FRESH isolated profile, so the common case has NO ~150 MB Chrome-for-Testing download and it never touches the user's real tabs/logins. Binds its local server to LOOPBACK (127.0.0.1) only, so no Windows Firewall 'allow Node through the firewall' prompt ever pops. Chrome for Testing is fetched only as a LAST RESORT if the machine has no Chromium browser at all. SPAWN-VERIFIED fallback: pup tries each detected browser (Chrome \u2192 Edge \u2192 CfT) and actually launches it, falling through to the next if one won't start (and handling browsers like Edge that relaunch onto another PID), so a corrupt/partial install never hard-fails an open. The browser that launches is cached as the default (persisted across restarts) so it isn't re-probed each time. On spawn the bridge SELF-WARMS: detects browsers + disk, and only if the box has no Chrome/Edge and no cached CfT does it background-fetch CfT so the first open is instant. browser_use pins a specific browser (chrome | edge | cft | auto); {browser:'chrome',install:true} downloads & installs REAL Google Chrome (user-scoped, no admin) on an Edge-only box. browser_readiness (READ-ONLY) reports which browser will be used + the full candidate list + the default + free disk. Every failure returns a specific errorCode + _hint (chrome_install_no_disk, chrome_download_network, chrome_install_permission, chrome_stable_not_detected, \u2026). Launches with FULL browser permissions by default for full debug (strictPermissions:true opts out). Headful automation: open/close windows+tabs, navigate, screenshot, eval JS, record. pup is a GENERIC, non-profile browser only \u2014 every session is a fresh isolated Chrome/Edge process, never the user's real signed-in browser; profile defaults to sessionId. To drive the user's REAL Chrome/Edge (their profile, cookies, SSO), use the Adom browser EXTENSION's nbrowser_* verbs \u2014 a separate bridge, not pup's job.", "homepage": "https://wiki.adom.inc/adom/adom-desktop", "author": "Adom Inc.",
src/server.js+45−6@@ -3509,6 +3509,9 @@ async function getSessionInfo(sessionId, session) { // v1.8.26: did background-by-default actually take? 'backgrounded' (good), // 'still-foreground' (focus steal — a bug), 'no-window', or null (foreground open // or non-Windows). Self-verified by re-reading the OS foreground window post-open.+ // v1.8.70: who put this window on the user's screen, when, and their stated reason —+ // the audit answer to "why did this foreground?" in one status query.+ lastForeground: session._lastForeground || null, background: (() => { try { const bg = browsers.get(session.profileName)?.browser?._bgResult || null;@@ -3637,7 +3640,7 @@ const server = http.createServer(async (req, res) => { hint: m.hint || summary, related: m.related || [], pitfalls: m.pitfalls || [] }; }; sendJSON(res, { success: true, bridge: 'puppeteer', verbs: [- V('browser_open_window', 'Open a generic pup window — a fresh isolated Chrome/Edge process (never your real signed-in browser), fully drivable via CDP. ALWAYS opens in the BACKGROUND (does not steal focus; never disturbs the user\'s main work; still fully screenshottable/drivable) UNLESS you pass foreground:true — do that ONLY when the user explicitly wants to watch. Pass width+height (+ optional x,y, screen px) to place it at a specific size/position for screenshot framing — this NO LONGER foregrounds it (sizing stays in the background too; default unsized is maximized). profile defaults to sessionId. OWNERSHIP: sessionIds are shared across ALL AI threads — use a task-prefixed sessionId + owner:"<your-task>" so pup can refuse other threads stealing your window (reusing an existing id with a new url navigates it away; declared cross-owner reuse errors with session_owned_by_another_thread unless takeover:true). (For the user\'s REAL Chrome/Edge, use the Adom extension\'s nbrowser_* verbs.)', { sessionId: 'required str — task-prefix it (e.g. "mytask-web"); generic ids collide across threads', url: 'optional str', profile: 'optional str — defaults to sessionId', owner: 'optional str — your thread/task label; lets pup protect this window from other threads', takeover: 'optional bool — claim a window owned by another thread (only when the user explicitly asks)', foreground: 'optional bool — the ONLY way to show the window (default false = background, always, even when sized)', width: 'optional int px — sizes for screenshot framing, stays background', height: 'optional int px', x: 'optional int px', y: 'optional int px', updateNote: 'optional str — a short "what I did" note, surfaced back in list_tabs/list_windows as lastAgentUpdate so the user knows what changed', autoFlash: 'optional bool (default true) — mutating verbs auto-flash this window\'s taskbar (debounced, never steals focus) so the user notices the agent touched it; pass false to make this window silent by default', silent: 'optional bool — suppress the auto-flash for just this call' }, 60, { sessionId: 'mytask-web', owner: 'mytask', url: 'https://example.com', width: 960, height: 540 }),+ V('browser_open_window', 'Open a generic pup window — a fresh isolated Chrome/Edge process (never your real signed-in browser), fully drivable via CDP. ALWAYS opens in the BACKGROUND (does not steal focus; never disturbs the user\'s main work; still fully screenshottable/drivable) UNLESS you pass foreground:true — do that ONLY when the user explicitly wants to watch. Pass width+height (+ optional x,y, screen px) to place it at a specific size/position for screenshot framing — this NO LONGER foregrounds it (sizing stays in the background too; default unsized is maximized). profile defaults to sessionId. OWNERSHIP: sessionIds are shared across ALL AI threads — use a task-prefixed sessionId + owner:"<your-task>" so pup can refuse other threads stealing your window (reusing an existing id with a new url navigates it away; declared cross-owner reuse errors with session_owned_by_another_thread unless takeover:true). (For the user\'s REAL Chrome/Edge, use the Adom extension\'s nbrowser_* verbs.)', { sessionId: 'required str — task-prefix it (e.g. "mytask-web"); generic ids collide across threads', url: 'optional str', profile: 'optional str — defaults to sessionId', owner: 'optional str — your thread/task label; lets pup protect this window from other threads', takeover: 'optional bool — claim a window owned by another thread (only when the user explicitly asks)', foreground: 'optional bool — request showing the window on the user\'s screen. GATED (v1.8.70): requires foregroundReason too, else the open succeeds but stays BACKGROUND with foregroundDenied set. Only when the USER explicitly asked to watch.', foregroundReason: 'required WITH foreground:true — quote/paraphrase what the user said that justifies putting a window on their screen (min 10 chars; logged + recorded in lastAgentUpdate, auditable)', width: 'optional int px — sizes for screenshot framing, stays background', height: 'optional int px', x: 'optional int px', y: 'optional int px', updateNote: 'optional str — a short "what I did" note, surfaced back in list_tabs/list_windows as lastAgentUpdate so the user knows what changed', autoFlash: 'optional bool (default true) — mutating verbs auto-flash this window\'s taskbar (debounced, never steals focus) so the user notices the agent touched it; pass false to make this window silent by default', silent: 'optional bool — suppress the auto-flash for just this call' }, 60, { sessionId: 'mytask-web', owner: 'mytask', url: 'https://example.com', width: 960, height: 540 }), V('browser_open_tab', 'Open a new tab in an EXISTING session/window (returns {tabId, tabCount, activeTabId} — no need to diff list_tabs). This is the right verb for multi-URL work: ONE window with many tabs (one taskbar button), not many windows. The window stays in the background; the tab auto-flashes the taskbar (debounced) so the user notices. The USER can click the window\'s taskbar button at any time to view it — pup never pulls it back down.', { sessionId: 'required str', url: 'optional str', updateNote: 'optional str — short "what I did" note surfaced in list_tabs as lastAgentUpdate', silent: 'optional bool — suppress the auto-flash for this call' }, 30, { sessionId: 'work', url: 'https://example.com' }), V('browser_close_tab', 'Close a tab.', { sessionId: 'required str', tabId: 'optional str (default active)' }, 15, { sessionId: 'work' }), V('browser_close_window', 'Close a window/session (its tabs go too).', { sessionId: 'required str' }, 20, { sessionId: 'work' }),@@ -3666,7 +3669,7 @@ const server = http.createServer(async (req, res) => { V('browser_prewarm', 'Install Chrome for Testing WITHOUT opening a window, and pin it as the default. Use when you specifically want CfT rather than the machine\'s installed Chrome/Edge.', { wait: 'optional bool — block until installed', setDefault: 'optional bool (default true) — pin CfT as the default' }, 240, {}), V('browser_use', 'Pin (or clear) the browser pup drives by default: chrome | edge | cft | auto (or an explicit executablePath). "cft" fetches Chrome for Testing then pins it. With {browser:"chrome", install:true} it DOWNLOADS & INSTALLS real Google Chrome on an Edge-only box, then pins it — SILENT on a normal PC; on a locked-down box (VM/managed device) it needs a Windows approval, so the bridge pops a native "click YES on the UAC" notification and, if it expires, re-notifies with a one-tap "Approve now" (all autonomous — no AI turn). Poll browser_readiness.chromeStablePhase: installing → awaiting_uac → ready. Persists across restarts; launch still spawn-verifies + falls back.', { browser: '"chrome" | "edge" | "cft" | "auto"', install: 'optional bool — with browser:"chrome", install real Chrome if missing', elevate: 'optional "auto"(default)|true|false — auto: quiet install, prompt only if it needs admin; false: never prompt; true: go straight to the prompt', executablePath: 'optional — pin an exact browser binary' }, 240, {}), V('browser_lower_os_window', 'Send a pup window to the BACKGROUND (behind the user\'s windows) without stealing focus — still fully drivable. The default for open; use this to re-hide a window you foregrounded.', { sessionId: 'optional str' }, 15, {}),- V('browser_raise_os_window', 'Bring a pup window to the FOREGROUND (user\'s screen). Use SPARINGLY — only when the user EXPLICITLY must SEE it (a demo, recording, "show me"). It STEALS focus and can leak a mid-typed keystroke into the page — to just signal "I updated this window", use browser_alert_window (or rely on the auto-flash), NOT raise. pup blurs the page\'s focused element before raising to reduce the keystroke leak. Alias: browser_focus_window.', { sessionId: 'optional str' }, 15, {}),+ V('browser_raise_os_window', 'Bring a pup window to the FOREGROUND (user\'s screen). GATED (v1.8.70): REFUSED with foreground_reason_required unless you pass foregroundReason — a quote/paraphrase of what the user said that justifies it (min 10 chars; logged, recorded in lastAgentUpdate, auditable). Allowed ONLY when the user EXPLICITLY must SEE it (a demo, recording, "show me"). It STEALS focus — to just signal "I updated this window", use browser_alert_window (or the auto-flash), NOT raise. After the show-me: browser_lower_os_window to return it to the background. Alias: browser_focus_window.', { sessionId: 'optional str', foregroundReason: 'REQUIRED — why the user asked to see this window' }, 15, { foregroundReason: 'user said "show me the wiki page"' }), V('browser_alert_window', 'Flash the taskbar icon for a pup window — a non-intrusive "it\'s ready/DONE" nudge that does NOT steal focus. NOTE (v1.8.23): mutating verbs already AUTO-FLASH (debounced, one per ~5s burst per window) so the user always knows the agent touched a window — use this for an explicit end-of-task "it\'s ready" nudge on top of that. STRONGLY prefer this (or the auto-flash) over browser_raise_os_window, which steals focus and can leak a mid-typed keystroke into the page.', { sessionId: 'optional str', stop: 'optional bool — CLEAR the highlight instead of flashing' }, 15, {}), V('browser_render_html', 'Render a local HTML string in the tab via page.setContent — UTF-8-correct (a data: URL mojibakes accented chars without an explicit charset). Great for previewing generated markup without a temp file. Auto-flashes the window.', { sessionId: 'required str', html: 'required HTML string', waitUntil: 'optional (domcontentloaded default)', updateNote: 'optional note for lastAgentUpdate', silent: 'optional bool — suppress the auto-flash' }, 30, { sessionId: 'work', html: '<h1>Café</h1>' }), // Session/tab/window management + probes (were routed but undocumented).@@ -3755,6 +3758,17 @@ const server = http.createServer(async (req, res) => { const downloadPath = (args.downloadPath && typeof args.downloadPath === 'string') ? args.downloadPath : path.join(os.homedir(), 'Downloads');+ // v1.8.70: THE FOREGROUND GATE (open-time half; mirrors abe's dual key). The flag+ // alone is NOT enough — without foregroundReason the open still SUCCEEDS but the+ // window stays BACKGROUND (abe semantics: withhold the foregrounding, not the work),+ // and the response says exactly what was missing. A granted foreground is logged ++ // recorded in lastAgentUpdate so the user can always audit who put a window on their+ // screen and why. Also accepts abe's arg name userRequestedForeground.+ const fgRequested = !!(args.foreground || args.raise || args.show || args.userRequestedForeground);+ const fgReason = (typeof args.foregroundReason === 'string' && args.foregroundReason.trim().length >= 10) ? args.foregroundReason.trim() : null;+ const fgGranted = fgRequested && !!fgReason;+ const fgDenied = fgRequested && !fgReason;+ if (fgGranted) console.log(`[foreground] "${sessionId}" open GRANTED — reason: ${fgReason}`); let session; try { session = await launchSession(sessionId, args.url, {@@ -3767,8 +3781,8 @@ const server = http.createServer(async (req, res) => { takeover: !!args.takeover, // v1.8.24: background-by-default is enforced DURING launch (focus handed // back to the user's window the instant pup's window appears) unless the- // caller explicitly wants it foreground.- foreground: !!(args.foreground || args.raise || args.show),+ // caller explicitly wants it foreground (v1.8.70: AND supplied a reason).+ foreground: fgGranted, silent: !!args.silent || args.autoFlash === false, }); } catch (e) {@@ -3811,7 +3825,7 @@ const server = http.createServer(async (req, res) => { // is background even after the bounds call. const w = Number(args.width), h = Number(args.height), wx = Number(args.x), wy = Number(args.y); const sized = Number.isFinite(w) && Number.isFinite(h);- const foreground = !!(args.foreground || args.raise || args.show);+ const foreground = fgGranted; // v1.8.70: the gate's verdict, not the raw flag if (sized) { try { await setSessionWindowBounds(session, { x: wx, y: wy, width: w, height: h }); } catch (e) {} // v1.8.21: ALSO pin the page VIEWPORT (CDP device-metrics) to width×height.@@ -3834,6 +3848,10 @@ const server = http.createServer(async (req, res) => { // v1.8.26: record whether this window is meant to be on screen — gates every // later bringToFront (navigate/reload/switch_tab must not raise a bg window). if (session) session._foreground = foreground;+ if (session && fgGranted) {+ session._lastForeground = { ts: Date.now(), verb: 'browser_open_window', reason: fgReason };+ try { recordAgentUpdate(session, 'browser_open_window', { updateNote: `FOREGROUND: ${fgReason}` }, session.activeTabId); } catch (e) {}+ } // v1.8.23: session-level auto-flash default (autoFlash:false → this window never // auto-flashes, e.g. a chatty automation loop the user isn't watching). if (session && args.autoFlash === false) session._flashSilent = true;@@ -3851,6 +3869,7 @@ const server = http.createServer(async (req, res) => { } // Verbose browser report: which browser we actually drove, what else is available. const brep = browserPickReport();+ const fgDenyHint = fgDenied ? ' ⛔ FOREGROUND DENIED: you passed foreground:true WITHOUT a foregroundReason, so the window opened in the BACKGROUND (the open itself succeeded). Foregrounding steals the user\'s screen+keyboard and is allowed ONLY when the USER explicitly asked to SEE this window. If they did: re-call with foreground:true + foregroundReason:"<quote/paraphrase what the user said>" (min 10 chars — it is recorded and auditable). If they did not: leave it background; browser_alert_window is the correct "it\'s ready" signal.' : ''; const bgHint = foreground ? 'Opened in the FOREGROUND as EXPLICITLY requested (foreground/raise/show:true). Return it to the background with browser_lower_os_window once the user is done watching.' : `Opened in the BACKGROUND (behind the user's windows, did NOT steal focus)${sized ? ' — and placed at the requested size/position WITHOUT foregrounding, so framing it for a screenshot never disturbs the user' : ''}. This is the HARD default so pup never disrupts the user's main work. It is FULLY drivable here: browser_screenshot / browser_eval / browser_navigate all work while it stays hidden. ONLY bring it to the user's screen if they EXPLICITLY want to WATCH (a demo, a recording, a "show me") — pass foreground:true on open (or call browser_raise_os_window). Sizing alone does NOT foreground anymore.`;@@ -3893,7 +3912,8 @@ const server = http.createServer(async (req, res) => { // v1.8.16: reuse/ownership verdict (null = fresh window). reused: reuse ? { navigatedAway: reuse.navigatedAway, previousUrl: reuse.previousUrl, previousOwner: reuse.previousOwner, takeover: reuse.takeover } : null, owner: session.owner || null,- _hint: `${bgHint}${ownHint} ${brep._hint}${staleWindowsHint()}`,+ foregroundDenied: fgDenied ? 'foreground_reason_required' : undefined,+ _hint: `${fgDenyHint}${bgHint}${ownHint} ${brep._hint}${staleWindowsHint()}`, ...info, }), });@@ -4219,6 +4239,25 @@ const server = http.createServer(async (req, res) => { return; } const session = sessions.get(sessionId);+ // v1.8.70: THE FOREGROUND GATE (mirrors abe's userRequestedForeground+foregroundReason+ // dual key — same contract across Adom's browser surfaces). Raising steals the user's+ // screen and keyboard; it is allowed ONLY as a user-requested show-me, and the caller+ // must SAY SO in a recorded, auditable reason. Real incident: a thread raised a window+ // 2 minutes after a background open and the only forensics were a focus-latch+ // timestamp — never again.+ const fgReason = (typeof args.foregroundReason === 'string' && args.foregroundReason.trim().length >= 10) ? args.foregroundReason.trim() : null;+ if (!fgReason) {+ sendJSON(res, {+ success: false,+ errorCode: 'foreground_reason_required',+ error: 'Raise refused: foregroundReason is required.',+ _hint: 'Foregrounding steals the user\'s screen and keyboard focus. It is allowed ONLY when the USER explicitly asked to SEE this window (a "show me", a demo, a live recording). Call again with foregroundReason:"<quote or closely paraphrase what the user said that justifies this>" (min 10 chars) — the reason is logged, recorded in lastAgentUpdate, and auditable by the user. If the user did NOT ask to see it, do NOT raise: use browser_alert_window (taskbar nudge) or rely on the auto-flash. After a granted raise: do the one thing the user asked for, then browser_lower_os_window to return it to the background.',+ });+ return;+ }+ console.log(`[foreground] "${sessionId}" raise GRANTED — reason: ${fgReason}`);+ session._lastForeground = { ts: Date.now(), verb: command, reason: fgReason };+ try { recordAgentUpdate(session, command, { updateNote: `FOREGROUND: ${fgReason}` }, session.activeTabId); } catch (e) {} try { // v1.8.26: background windows launch OFF-SCREEN (-32000). To actually SHOW one, // move it back onto the primary monitor + maximize it (via CDP, no OS activation)