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 with its AI thread's name in your taskbar or dock.
← Commit history
v1.8.14: Chrome UAC notify live on AD 1.9.84 — app-less direct-API, bridge token, cross-AD peer ping, notify-first ordering
4 files changed
+74−33
adom-bridge-puppeteer-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "1.8.13",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.8.13/adom-bridge-puppeteer-v1.8.13.zip",- "sha256": "3b90ecd842bd2d8727c37eec14f4b4f68f853422a38e3e7e219eb9a0f9bcc0df",- "size": 138306,+ "version": "1.8.14",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.8.14/adom-bridge-puppeteer-v1.8.14.zip",+ "sha256": "4c0657b417050919306e81087d8ac161f6935c43f90a2a96595e9a1e2cbf793d",+ "size": 139201, "verbPrefixes": [ "browser_", "desktop_recorder_",
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-1.8.13+1.8.14
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "1.8.13",+ "version": "1.8.14", "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/chrome.js+68−27@@ -445,7 +445,8 @@ function readiness() { installingChrome: !!_chromeStableInFlight, // a real-Google-Chrome install is in flight chromeStablePhase: chromeStableState.phase, // idle|installing|awaiting_uac|ready|failed chromeStableAwaitingApproval: chromeStableState.phase === 'awaiting_uac', // a UAC is up on the machine running pup; the user must click YES there- chromeStableNotifyChannel: chromeStableState.notifyChannel === undefined ? null : !!chromeStableState.notifyChannel, // could the bridge actually notify the user? (false on AD builds that don't expose notify_user to the direct-API — the AI must relay the approval ask)+ chromeStableNotifyChannel: chromeStableState.notifyChannel === undefined ? null : !!chromeStableState.notifyChannel, // could the bridge actually notify the user? (false on AD <1.9.84 that doesn't expose notify_user to the direct-API — the AI must relay the approval ask)+ chromeStableNotifyPeers: chromeStableState.notifyPeers == null ? null : chromeStableState.notifyPeers, // # of peer ADs also pinged (cross-AD target:"all") so the user is told even if attending another device chromeStableError: chromeStableState.error, puppeteerLoadError: _puppeteerLoadError, };@@ -506,22 +507,37 @@ function _resolveAdApiBase() { _adApiBase = 'http://127.0.0.1:47200'; // documented default; harmless if unreachable return _adApiBase; }-// Fire an AD verb via the direct-API. Best-effort: resolves null on any failure so-// a missing/asleep AD never blocks the install (the UAC still appears regardless).-// `app` defaults to the probed namespace notify_user lives under (see _probeAdNotifyCap).-function adCommand(command, args, app = 'desktop', timeoutMs = 8000) {+// Call an AD verb via the direct-API. AD ≥1.9.84: `app` is inferred and the FULL verb+// surface is reachable (notify_user, notify_response, targets, …). Best-effort: resolves+// null on any failure so a missing/old AD never blocks the install (the UAC still appears).+// opts.target → route to a PEER AD: "<clientName>" | "all" | "self"/"local" (default).+// opts.noToken → internal: retry unattributed after a stale-token 403.+// Sends the spawn-time ADOM_BRIDGE_TOKEN as X-Adom-Bridge-Token for Activity-Log+// attribution (OPTIONAL — a stale token 403s, so we retry once without it).+function adCommand(command, args, opts = {}) {+ const { target, timeoutMs = 8000, noToken = false } = opts; return new Promise((resolve) => { let base; try { base = _resolveAdApiBase(); } catch { return resolve(null); } if (!base) return resolve(null); try { const http = require('http');- const body = JSON.stringify({ app, command, args: args || {} });+ const payload = { command, args: args || {} };+ if (target) payload.target = target;+ const body = JSON.stringify(payload); const u = new URL(base + '/command');+ const headers = { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) };+ const tok = process.env.ADOM_BRIDGE_TOKEN;+ if (tok && !noToken) headers['X-Adom-Bridge-Token'] = tok; const req = http.request(- { hostname: u.hostname, port: u.port, path: u.pathname, method: 'POST',- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } },- (resp) => { let d = ''; resp.on('data', c => d += c); resp.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve(null); } }); }+ { hostname: u.hostname, port: u.port, path: u.pathname, method: 'POST', headers },+ (resp) => {+ let d = ''; resp.on('data', c => d += c);+ resp.on('end', () => {+ if (resp.statusCode === 403 && tok && !noToken) return resolve(adCommand(command, args, { ...opts, noToken: true }));+ try { resolve(JSON.parse(d)); } catch { resolve(null); }+ });+ } ); req.on('error', () => resolve(null)); req.setTimeout(timeoutMs, () => { try { req.destroy(); } catch {}; resolve(null); });@@ -542,27 +558,36 @@ function _adGet(pathname, timeoutMs = 6000) { } catch { resolve(null); } }); }-// Probe whether this AD build actually exposes notify_user through the direct-API.-// As of AD 1.9.79 it does NOT — the `desktop` app only dispatches `desktop_*` verbs,-// and top-level verbs (notify_user/notify_response) return "Unknown desktop command".-// So a bridge cannot fire a toast until AD adds it (see HANDOFF/AD build prompt). We-// probe /commands and cache the answer: {app} if reachable, null if not. This is the-// gate that stops us from blindly raising a UAC the user is never told about.-let _adNotifyCap = undefined; // undefined=unprobed | null=unavailable | {app}+// Probe whether this AD exposes notify_user through the direct-API. AD ≥1.9.84 lists it+// in /commands.topLevel; AD ≤1.9.79 didn't expose it at all (top-level verbs returned+// "Unknown desktop command"), so a bridge couldn't notify. We cache the answer: {ok} if+// reachable, null if not. This gates the notify-first UAC flow vs. the honest degradation+// (raise once + relay hint) on an AD too old to notify.+let _adNotifyCap = undefined; // undefined=unprobed | null=unavailable | {ok:true} async function _probeAdNotifyCap() { if (_adNotifyCap !== undefined) return _adNotifyCap; _adNotifyCap = null; try { const cmds = await _adGet('/commands');- if (cmds && cmds.desktop) {- const list = cmds.desktop.commands || cmds.desktop || [];- if (Array.isArray(list) && (list.includes('notify_user') || list.includes('desktop_notify_user'))) _adNotifyCap = { app: 'desktop' };+ if (cmds) {+ const top = cmds.topLevel || [];+ const dlist = (cmds.desktop && (cmds.desktop.commands || cmds.desktop)) || [];+ const has = (a) => Array.isArray(a) && (a.includes('notify_user') || a.includes('desktop_notify_user'));+ if (has(top) || has(dlist)) _adNotifyCap = { ok: true }; } } catch {} return _adNotifyCap; }-const adNotify = (args) => adCommand('notify_user', args, (_adNotifyCap && _adNotifyCap.app) || 'desktop');-const adNotifyResponse = (id) => adCommand('notify_response', { id }, (_adNotifyCap && _adNotifyCap.app) || 'desktop');+const adNotify = (args, target) => adCommand('notify_user', args, { target });+const adNotifyResponse = (id) => adCommand('notify_response', { id });+// Peer ADs on the same relay (other machines the user might be sitting at). [] if none/old AD.+async function adPeerNames() {+ try {+ const r = await adCommand('targets', {});+ const list = (r && (r.targets || (r.data && r.data.targets))) || [];+ return list.map(t => t && (t.name || t.clientName)).filter(Boolean);+ } catch { return []; }+} const _sleep = (ms) => new Promise(r => setTimeout(r, ms)); // PowerShell single-quote escaping (double any embedded quote).@@ -622,7 +647,21 @@ async function _elevatedInstallWithNotify(installerPath) { return await pollChrome(150 * 1000); // ~2.5 min for the user to click the single prompt } - // (A) Notify-first with re-arm on tap.+ // (A) Notify-first with re-arm on tap. The UAC lives on THIS box (you can only approve+ // it here), so the actionable "Approve now" toast is LOCAL. If the user is attending a+ // different device (peer AD on the relay — e.g. pup on a VM, user on their laptop), we+ // ALSO ping every peer so they're told where to go. Cross-AD `target:"all"` routes the+ // ping via the relay. The local toast's tap re-raises the UAC here; peer toasts are+ // informational ("go to <host>"). Autonomous — zero AI turns.+ const host = (() => { try { return os.hostname(); } catch { return 'this PC'; } })();+ const peers = await adPeerNames();+ chromeStableState.notifyPeers = peers.length;+ const pingPeers = (title, body, level) => peers.length+ ? adNotify({ id: 'pup-chrome-peer', level: level || 'warning', scenario: 'reminder', buttons: [{ label: 'OK' }], title, body }, 'all')+ : Promise.resolve();+ const doneLocal = () => adNotify({ id, level: 'success', title: 'Chrome is installed ✓', body: 'Google Chrome is ready — pup will drive it from now on.' });+ const donePeers = () => pingPeers('Chrome installed ✓', `Chrome finished installing on ${host}.`, 'success');+ const OVERALL_MS = 12 * 60 * 1000; const t0 = Date.now(); let first = true;@@ -630,23 +669,25 @@ async function _elevatedInstallWithNotify(installerPath) { await adNotify({ id, level: 'warning', scenario: 'reminder', buttons: [{ label: 'Approve now' }], title: first ? 'Chrome needs your approval' : 'Chrome still needs your approval',- body: "Tap 'Approve now', then click YES on the Windows security prompt to finish installing Google Chrome. (Standard Google installer.)",+ body: `Tap 'Approve now', then click YES on the Windows security prompt to finish installing Google Chrome on ${host}.`, });+ await pingPeers(`Chrome needs approval on ${host}`,+ `pup is installing Chrome on ${host}. Go to that machine, tap 'Approve now', then click YES on the Windows prompt.`); first = false;- // WAIT for the tap before raising the UAC (so the toast isn't hidden by the secure desktop).+ // WAIT for the LOCAL tap before raising the UAC (so the toast isn't hidden by the secure desktop). let tapped = false; const waitEnd = Date.now() + 4 * 60 * 1000; // re-notify (sticky) if untouched for 4 min while (Date.now() < waitEnd) { await _sleep(2500); const c = listSystemBrowsers().find(x => x.kind === 'chrome');- if (c) { await adNotify({ id, level: 'success', title: 'Chrome is installed ✓', body: 'Google Chrome is ready — pup will drive it from now on.' }); return c; }+ if (c) { await doneLocal(); await donePeers(); return c; } const resp = await adNotifyResponse(id); if (resp && resp.pending === false && resp.action) { tapped = true; break; } }- if (!tapped) continue; // untouched → loop top re-fires the sticky toast+ if (!tapped) continue; // untouched → loop top re-fires the sticky toasts await _raiseElevatedInstall(installerPath); // they opted in → one UAC now const c = await pollChrome(130 * 1000);- if (c) { await adNotify({ id, level: 'success', title: 'Chrome is installed ✓', body: 'Google Chrome is ready — pup will drive it from now on.' }); return c; }+ if (c) { await doneLocal(); await donePeers(); return c; } // UAC expired/declined → loop top re-notifies and waits for another tap. } return null;