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
v1.7.0: remove all native-browser drive (631+ lines) — pup is generic non-profile CDP browser only; native real-browser drive is the Adom extension's nbrowser_ verbs; profile defaults to sessionId
6 files changed
+32−723
SKILL.md+9−15@@ -1,7 +1,7 @@ --- name: pup user-invocable: true-description: "Drive Chrome browser windows on the user's desktop via the adom-desktop CLI (the Puppeteer bridge): open/close windows + tabs, navigate, screenshot, eval JS, record, drive the user's REAL signed-in Chrome/Edge/Brave. Cold-start: pup drives the browser already on the machine (installed Chrome, else Edge — every Windows PC has Edge) with a fresh isolated profile, so usually NO download; Chrome for Testing only as a last resort. For 'is it ready' use browser_readiness (not browser_status). Trigger words: pup, puppeteer, browser window, open in pup, browser screenshot, browser reload, browser eval, visual debug, browser_open_window, browser_screenshot, browser_readiness, browser_prewarm, chrome for testing, open in browser."+description: "Drive a generic browser window on the user's desktop via the adom-desktop CLI (the Puppeteer 'pup' bridge): open/close windows + tabs, navigate, screenshot, eval JS, record. pup opens a FRESH isolated Chrome/Edge process (never the user's real signed-in profile — that's the Adom browser extension's nbrowser_* verbs). Cold-start: pup drives the browser already on the machine (installed Chrome, else Edge — every Windows PC has Edge) with a fresh isolated profile, so usually NO download; Chrome for Testing only as a last resort. For 'is it ready' use browser_readiness (not browser_status). Trigger words: pup, puppeteer, browser window, open in pup, browser screenshot, browser reload, browser eval, visual debug, browser_open_window, browser_screenshot, browser_readiness, browser_prewarm, chrome for testing, open in browser." --- # Pup — Puppeteer Browser Control (with cold-start self-heal)@@ -29,7 +29,7 @@ When you run `browser_open_window`, one of these happens — handle each: | `errorCode: "node_not_found"` / `could not run npm` / runtime not ready | The bridge's Node runtime isn't ready yet. On **AD ≥ 1.9.63 you do nothing** — AD provisions Node automatically (portable, no UAC) and prewarms it in the background right after first launch. | Check `adom-desktop runtimes '{}'`; if `state:"installing"`, wait a few seconds and **retry** the same `browser_*` call — it's ready once `state:"ready"`. No user action, no prompt. (Only on AD older than 1.9.63: `desktop_install_node` — see "The Node step".) | | `errorCode: "chrome_for_testing_installing"` (`installing:true`) | **RARE** — the machine has NO Chrome AND no Edge, so pup is fetching Chrome for Testing as a fallback (~150 MB, in the background; it did NOT block). | Poll **`browser_readiness`** until `{ready:true}`, then re-issue the same `browser_open_window`. **Faster fix:** ask the user to install Chrome or Edge (Edge ships with Windows) — pup then uses it instantly, no download. Don't report failure. | | `errorCode: "chrome_install_no_disk"` | No Chrome/Edge and too little free disk to install Chrome for Testing (`readiness.diskFreeMb` shown). | Tell the user how much is free and that ~800 MB is needed; ask them to free space OR install Edge/Chrome (Edge needs ~nothing). Do NOT retry the download — it fails until disk is freed. |-| `errorCode: "chrome_install_failed"` / `chrome_download_network` / `chrome_install_permission` | No Chrome/Edge AND the CfT fallback download failed (network/proxy, permission, or other). | Read `readiness.lastError`. Ask the user to install Chrome/Edge, or drive their real browser via native mode (below). |+| `errorCode: "chrome_install_failed"` / `chrome_download_network` / `chrome_install_permission` | No Chrome/Edge AND the CfT fallback download failed (network/proxy, permission, or other). | Read `readiness.lastError`. Ask the user to install Chrome or Edge (Edge ships with Windows). | **Want real Google Chrome on an Edge-only box?** If the user asks for Chrome specifically, run `browser_use '{"browser":"chrome","install":true}'` — pup downloads Google's official installer and installs Chrome **user-scoped (no admin prompt in most cases)**, then pins it. Poll `browser_readiness` (`installingChrome` / `chromeStablePhase`) until `browserKind:"chrome"`. If it stalls, a UAC prompt may be waiting on the user's desktop — ask them to accept it. @@ -135,7 +135,7 @@ adom-desktop browser_alert_window '{"sessionId":"job"}' # non-intrusive "do | `browser_readiness` | Which browser pup will drive + candidate list + default (READ-ONLY) | none | | `browser_prewarm` | Install Chrome for Testing WITHOUT opening a window, and pin it | `wait` (optional bool) | | `browser_use` | Pin the browser pup drives: `chrome`/`edge`/`cft`/`auto` | `browser`, `executablePath?` |-| `browser_open_window` | Spawn a new session/window (installed Chrome/Edge or CfT; native via `nativeBrowser`+`nativeProfile`) | `sessionId`, `profile`, `url` |+| `browser_open_window` | Spawn a new generic pup session/window (fresh isolated Chrome/Edge process; `profile` defaults to `sessionId`) | `sessionId`, `url`, `profile?` | | `browser_open_tab` | Add a tab to an existing session | `sessionId`, `url` | | `browser_navigate` | Drive an existing tab to a URL (reuses — no new window) | `sessionId`, `url`, `tabId?` | | `browser_screenshot` | Capture the page (lossless PNG) | `sessionId`, `fullPage?` |@@ -207,19 +207,13 @@ months). Sanity-check: `browser_eval '{"sessionId":"...","expr":"document.title"}'` (fastest — 404s, login walls, and anti-bot blocks all change the title), or a `browser_screenshot` when the title isn't discriminating. -## Driving the user's REAL Chrome/Edge/Brave (native mode)+## Need the user's REAL signed-in browser? That's the Adom extension, not pup. -The default path drives Chrome for Testing (a clean, isolated, fully-controllable Chromium). To drive-the user's **actual signed-in browser** (their cookies, org SSO, device enrollment), pass-`nativeBrowser`+`nativeProfile` (run `desktop_list_browsers` first):--```bash-adom-desktop browser_open_window '{"sessionId":"gcp","nativeBrowser":"chrome","nativeProfile":"Default","url":"https://console.cloud.google.com"}'-```--Native mode is the fallback when Chrome for Testing can't be installed (no network for the CfT-download) but the user's real browser is already on the machine. Never auto-escalate to native for a-plain "open in pup" — that's CfT.+pup is **generic only** — every session is a fresh isolated Chrome/Edge process, never the user's real+profile. If a task needs the user's **actual signed-in browser** (their cookies, org SSO, saved logins),+that's the **Adom browser extension** (`nbrowser_*` verbs) — a separate bridge that drives the real+running Chrome/Edge without closing it. Reach for the `adom-browser-extension` skill for that; pup does+not do native-browser drive. ## Key rules
adom-bridge-puppeteer-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "1.6.3",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.6.3/adom-bridge-puppeteer-v1.6.3.zip",- "sha256": "70327d8abcfd589b7bcd56aaf601a8ca31d5b0c24dfa699b9b8ddd2c98f05f3a",- "size": 138996,+ "version": "1.7.0",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.7.0/adom-bridge-puppeteer-v1.7.0.zip",+ "sha256": "f30de8c514fe1bde4f82b000234968f24d294199051dcee03c89b743d36f1406",+ "size": 128274, "verbPrefixes": [ "browser_", "desktop_recorder_",
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-desktop-puppeteer-bridge",- "version": "1.6.3",+ "version": "1.7.0", "type": "app", "description": "CONTAINER-SIDE install \u2014 the everyday `pup` user skill only. This pkg drops the `pup` skill (drive the browser bridge: open/close/tabs/screenshot/record, background-by-default) into ~/.claude/skills/ AND ~/.codex/skills/. It does NOT install the developer/publishing skills (pup-bridge-dev, pup-bridge-publish) \u2014 those are source-only; clone the repo (adom-wiki repo clone adom/adom-desktop-puppeteer-bridge) to get them from dev-skills/. It also does NOT contain the bridge runtime: the bridge (Node + Chrome for Testing) lives on the DESKTOP \u2014 bundled inside Adom Desktop and published here on the Releases tab as adom-bridge-puppeteer-v<ver>.zip, which Adom Desktop streams into its bridges-cache. TL;DR: `pkg install` = the pup user skill; dev skills = clone the source; the bridge = the Release zip + bundled in Adom Desktop.", "keywords": [
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-1.6.3+1.7.0
src/bridge.json+2−2@@ -2,8 +2,8 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "1.6.3",- "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. v1.0.6+ optional native-drive: pass nativeBrowser+nativeProfile (from desktop_list_browsers) to browser_open_window to drive the user's REAL signed-in Chrome/Edge/Brave (their actual profile, org policies, device enrollment) \u2014 with the smart relaunch/consent flow (native_relaunch_required / native_cdp_blocked). The generic path (no nativeBrowser) always works.",+ "version": "1.7.0",+ "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.", "license": "MIT",
src/server.js+15−700@@ -1360,7 +1360,7 @@ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPer // Prefer an ALREADY-INSTALLED browser (Chrome → Edge; every Windows box has Edge) // so the common case needs NO ~150 MB Chrome-for-Testing download. Falls back to a // cached CfT. A fresh --user-data-dir (below) keeps it CLEAN — it never touches the- // user's real tabs/logins (that's native-mode's job).+ // user's real tabs/logins (driving the real browser is the Adom extension's job). // // launchCandidates() returns the FULL ordered fallback list ([pinned default] → // Chrome → Edge → cached CfT). The launch loop below tries each in turn and@@ -1876,637 +1876,6 @@ async function launchSession(sessionId, url, { freshProfile = false, profile, st return session; } -// ─────────────────────────────────────────────────────────────────────────────-// Native browser drive (v1.0.6+) — drive the user's REAL Chrome/Edge/Brave.-//-// v1.0.5 first attempted a "clone the user-data-dir" strategy so we wouldn't-// touch the user's live Chrome. It had three fatal-for-real-use problems:-// 1. Chrome's *managed* profile state (CBCM device enrollment, group-// policies bound to the Local State + device identity) doesn't follow-// a copy — the cloned profile prompts "this profile will be managed"-// and re-runs the enrollment flow, defeating the entire point.-// 2. SQLite files (Cookies, Login Data) take brief exclusive locks during-// writes. Robocopy skipped them mid-write, so the clone landed without-// cookies and the cloned Chrome saw a logged-out claude.ai/GCP/etc.-// 3. The clone drifts from the live profile — fine for a brief test, bad-// for a workflow.-//-// v1.0.6 trades clone-clever for honest: when the user explicitly wants-// native drive, we ASK to close + relaunch their real Chrome with-// `--remote-debugging-port` + `--restore-last-session`. Their open windows-// and tabs come back, every signed-in cookie + org policy + device-bound-// credential stays intact, and CDP gives us full control over the SAME-// profile they were using. The price: the user momentarily sees Chrome-// close and reopen with their tabs restored.-//-// The opt-in shape:-// browser_open_window {nativeBrowser:"chrome", nativeProfile:"Default", ...}-// → bridge checks if Chrome is already running with a debug port-// → if yes (e.g. user always launches Chrome with it), connect.-// → if no, return errorCode:"native_relaunch_required" with a clear-// message. Caller surfaces it to the user.-// browser_open_window {..., nativeRelaunch:true}-// → user confirmed. Bridge taskkills Chrome cleanly (WM_CLOSE so-// Sessions save), waits for exit, relaunches with --user-data-dir +-// --profile-directory + --remote-debugging-port + --restore-last-session,-// connects via puppeteer.connect.-//-// Subsequent calls with the same (browser, profile) reuse that Chrome —-// one process, every tab/window/eval goes through it. If the user later-// quits Chrome normally, a future native call returns native_relaunch_required-// again, same dance.-//-// Power-user workflow tip we should surface in docs: if you live in this-// flow, just launch Chrome at startup with --remote-debugging-port=<n>-// permanently. Then AD reconnects every session without ever asking.-// ─────────────────────────────────────────────────────────────────────────────--function whichBin(candidates) {- for (const c of candidates) {- try {- const out = execSync(`which ${c}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();- if (out && fs.existsSync(out)) return out;- } catch {}- }- return null;-}--// Per-browser metadata: how to find the exe + the real User Data root that-// holds <profileId> + Local State. Keys match desktop_list_browsers names.-function nativeBrowserMeta(name) {- const isWin = process.platform === 'win32';- const isLinux = process.platform === 'linux';- const isMac = process.platform === 'darwin';- const local = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');- const programFiles = process.env['ProgramFiles'] || 'C:\\Program Files';- const programFiles86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';- const home = os.homedir();-- const firstExisting = (paths) => paths.find(p => fs.existsSync(p));-- switch (name) {- case 'chrome': {- if (isWin) {- const exe = firstExisting([- path.join(programFiles, 'Google', 'Chrome', 'Application', 'chrome.exe'),- path.join(programFiles86, 'Google', 'Chrome', 'Application', 'chrome.exe'),- path.join(local, 'Google', 'Chrome', 'Application', 'chrome.exe'),- path.join(local, 'Programs', 'Google', 'Chrome', 'Application', 'chrome.exe'),- ]);- return exe ? { displayName: 'Chrome', exe, realUserDataDir: path.join(local, 'Google', 'Chrome', 'User Data') } : null;- }- if (isLinux) {- const exe = whichBin(['google-chrome-stable', 'google-chrome', 'chromium']);- return exe ? { displayName: 'Chrome', exe, realUserDataDir: path.join(home, '.config', 'google-chrome') } : null;- }- if (isMac) {- const exe = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';- return fs.existsSync(exe) ? { displayName: 'Chrome', exe, realUserDataDir: path.join(home, 'Library', 'Application Support', 'Google', 'Chrome') } : null;- }- return null;- }- case 'edge': {- if (isWin) {- const exe = firstExisting([- path.join(programFiles86, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),- path.join(programFiles, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),- path.join(local, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),- ]);- return exe ? { displayName: 'Edge', exe, realUserDataDir: path.join(local, 'Microsoft', 'Edge', 'User Data') } : null;- }- if (isLinux) {- const exe = whichBin(['microsoft-edge-stable', 'microsoft-edge']);- return exe ? { displayName: 'Edge', exe, realUserDataDir: path.join(home, '.config', 'microsoft-edge') } : null;- }- if (isMac) {- const exe = '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge';- return fs.existsSync(exe) ? { displayName: 'Edge', exe, realUserDataDir: path.join(home, 'Library', 'Application Support', 'Microsoft Edge') } : null;- }- return null;- }- case 'brave': {- if (isWin) {- const exe = firstExisting([- path.join(programFiles, 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),- path.join(programFiles86, 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),- path.join(local, 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),- path.join(local, 'Programs', 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),- ]);- return exe ? { displayName: 'Brave', exe, realUserDataDir: path.join(local, 'BraveSoftware', 'Brave-Browser', 'User Data') } : null;- }- if (isLinux) {- const exe = whichBin(['brave-browser', 'brave']);- return exe ? { displayName: 'Brave', exe, realUserDataDir: path.join(home, '.config', 'BraveSoftware', 'Brave-Browser') } : null;- }- if (isMac) {- const exe = '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser';- return fs.existsSync(exe) ? { displayName: 'Brave', exe, realUserDataDir: path.join(home, 'Library', 'Application Support', 'BraveSoftware', 'Brave-Browser') } : null;- }- return null;- }- default:- return null;- }-}--function nativeKey(browserName, profileId) {- return `native:${browserName}:${profileId}`;-}--function makeNativeError(code, message, extra = {}) {- const e = new Error(message);- e.errorCode = code;- Object.assign(e, extra);- return e;-}--// --- Native-drive CDP blocklist (v1.0.10) ----------------------------------// Some real-browser profiles never expose CDP no matter how we relaunch them —-// most commonly enterprise-managed Chrome where the RemoteDebuggingAllowed cloud-// policy is disabled browser-wide (e.g. a Google Workspace account). Closing +-// relaunching such a browser is futile AND disruptive (the user's browser blinks-// for nothing). We remember, per user-data-dir, that a relaunch failed to bring-// up CDP, and fail the *consent* step fast on the next attempt so we never close-// the browser for a doomed relaunch. Entries self-heal after CDP_BLOCK_TTL_MS (in-// case an admin enables the policy), and a successful connect clears them.-const CDP_BLOCK_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days-function cdpBlocklistPath() {- return path.join(os.homedir(), '.adom', 'native-cdp-blocked.json');-}-function readCdpBlocklist() {- try { return JSON.parse(fs.readFileSync(cdpBlocklistPath(), 'utf8')) || {}; }- catch { return {}; }-}-function cdpBlockKey(userDataDir) {- return (userDataDir || '').replace(/[\\/]+$/, '').replace(/\\/g, '/').toLowerCase();-}-function isCdpBlocked(userDataDir) {- const e = readCdpBlocklist()[cdpBlockKey(userDataDir)];- if (!e) return false;- // A numeric, expired timestamp self-heals (retry allowed). A missing/non-numeric- // timestamp is treated as a hard block until a successful connect clears it.- if (typeof e.blockedAt === 'number' && Date.now() - e.blockedAt > CDP_BLOCK_TTL_MS) return false;- return true;-}-function markCdpBlocked(userDataDir, displayName) {- try {- const bl = readCdpBlocklist();- bl[cdpBlockKey(userDataDir)] = { blockedAt: Date.now(), displayName: displayName || '', userDataDir };- fs.mkdirSync(path.dirname(cdpBlocklistPath()), { recursive: true });- fs.writeFileSync(cdpBlocklistPath(), JSON.stringify(bl, null, 2));- } catch (e) { console.log(`[native] could not persist CDP blocklist: ${e.message}`); }-}-function clearCdpBlocked(userDataDir) {- try {- const bl = readCdpBlocklist();- if (bl[cdpBlockKey(userDataDir)]) {- delete bl[cdpBlockKey(userDataDir)];- fs.writeFileSync(cdpBlocklistPath(), JSON.stringify(bl, null, 2));- }- } catch { /* best effort */ }-}-// Best-effort diagnosis of WHY a freshly-launched browser refused to open its-// debug port. The overwhelmingly common cause is an enterprise policy-// (RemoteDebuggingAllowed disabled) on a managed Chromium browser. We probe the-// local registry for a machine/user policy to upgrade "likely" → "confirmed";-// absent that, a managed profile + a refused port still points squarely at a-// cloud-pushed policy (Chrome Browser Cloud Management / Google Workspace). (v1.0.11)-function diagnoseCdpFailure(meta, browserName) {- if (process.platform === 'win32') {- const polKey = browserName === 'edge' ? 'Microsoft\\Edge'- : browserName === 'brave' ? 'BraveSoftware\\Brave'- : 'Google\\Chrome';- for (const hive of ['HKLM', 'HKCU']) {- try {- const out = execSync(- `reg query "${hive}\\SOFTWARE\\Policies\\${polKey}" /v RemoteDebuggingAllowed`,- { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });- const m = out.match(/RemoteDebuggingAllowed\s+REG_DWORD\s+0x([0-9a-fA-F]+)/);- if (m && parseInt(m[1], 16) === 0) {- return {- cause: 'policy_remote_debugging_disabled',- scope: 'local_machine_policy',- confidence: 'confirmed',- detail: `a local policy (${hive}\\SOFTWARE\\Policies\\${polKey}\\RemoteDebuggingAllowed = 0) disables remote debugging browser-wide — an admin/MDM set this.`,- };- }- } catch { /* key/value absent — keep looking */ }- }- }- return {- cause: 'policy_remote_debugging_disabled',- scope: 'cloud_managed_policy',- confidence: 'likely',- detail: `no local registry policy was found, but the debug port refused to bind. On a managed profile this is almost always a cloud-pushed policy (Chrome Browser Cloud Management or a Google Workspace RemoteDebuggingAllowed policy).`,- };-}--// What ELSE could the caller drive? Other installed Chromium browsers (with-// their current block status) so the AI can offer the user a real alternative.-function nativeAlternatives(attemptedName) {- const out = [];- for (const name of ['chrome', 'edge', 'brave']) {- if (name === attemptedName) continue;- let m; try { m = nativeBrowserMeta(name); } catch { m = null; }- if (!m) continue; // not installed- out.push({ nativeBrowser: name, displayName: m.displayName, cdpBlocked: isCdpBlocked(m.realUserDataDir) });- }- return out;-}-function genericPupHint() {- return `You can ALWAYS fall back to generic pup: call browser_open_window with NO nativeBrowser/nativeProfile for a clean Chrome-for-Testing instance — no user session, no managed policy, fully controllable. If the task needs the user's logged-in session, sign in once in that CfT window.`;-}-function altsBlurb(attemptedName) {- const alternatives = nativeAlternatives(attemptedName);- const open = alternatives.filter(a => !a.cdpBlocked);- const blocked = alternatives.filter(a => a.cdpBlocked);- let blurb = '';- if (open.length) {- blurb += `Other native browsers are installed and NOT known to be policy-blocked: ${open.map(a => `${a.displayName} (nativeBrowser:"${a.nativeBrowser}")`).join(', ')}. `;- if (open.some(a => a.nativeBrowser === 'edge')) {- blurb += `Edge in particular is usually outside Google Workspace management, so it often drives natively when Chrome cannot. `;- }- blurb += `Ask the user which they'd prefer. `;- }- if (blocked.length) {- blurb += `(Also installed but already known policy-blocked: ${blocked.map(a => a.displayName).join(', ')}.) `;- }- return { blurb, alternatives };-}-function cdpBlockedError(meta, browserName) {- const diagnosis = diagnoseCdpFailure(meta, browserName);- const { blurb, alternatives } = altsBlurb(browserName);- return makeNativeError(- 'native_cdp_blocked',- `${meta.displayName}'s debug port never bound after launch — ${diagnosis.detail} Native drive of ${meta.displayName} is not possible here, and AD will not close/relaunch it again for this. ` +- blurb + genericPupHint() +- ` (If an admin later enables RemoteDebuggingAllowed, call again with "nativeRelaunch": true to force a retry.)`,- { displayName: meta.displayName, realUserDataDir: meta.realUserDataDir, cdpBlocked: true, diagnosis, alternatives, genericPupAvailable: true }- );-}-function relaunchRequiredError(meta, browserName, profileId, runningCount) {- const { blurb, alternatives } = altsBlurb(browserName);- return makeNativeError(- 'native_relaunch_required',- `Native ${meta.displayName} drive needs the browser running with --remote-debugging-port. ${meta.displayName} is open right now (${runningCount} process(es)) without it, so AD must CLOSE and reopen it (with --restore-last-session, so every tab/window comes back exactly as it was). Ask the user if that's OK, then call browser_open_window again with "nativeRelaunch": true. ` +- blurb + genericPupHint() +- ` (Power-user tip: launch ${meta.displayName} with --remote-debugging-port=<N> at startup and AD reconnects every time with no prompt.)`,- { displayName: meta.displayName, runningProcesses: runningCount, mustCloseBrowser: true, confirmArgs: { nativeBrowser: browserName, nativeProfile: profileId, nativeRelaunch: true }, alternatives, genericPupAvailable: true }- );-}--// Find the user's real Chrome/Edge/Brave processes whose --user-data-dir is-// (a) the real one for this browser, or (b) absent (which means Chrome is-// using its default = the real one). Returns [{pid, port, cmdline}] where-// port is the --remote-debugging-port value (0 if absent).-function findRealBrowserProcesses(meta) {- if (process.platform !== 'win32') {- // POSIX fallback via `ps` — same parsing logic.- try {- const exeName = path.basename(meta.exe);- const out = execSync(`ps -e -o pid=,args=`, { encoding: 'utf8' });- return out.split('\n')- .filter(l => l.includes(exeName))- .map(l => {- const m = l.match(/^\s*(\d+)\s+(.*)$/);- if (!m) return null;- return parseBrowserCmdline(Number(m[1]), m[2], meta.realUserDataDir);- })- .filter(Boolean);- } catch { return []; }- }- // Windows: Win32_Process via PowerShell. ConvertTo-Json gives us each- // process's ProcessId + full CommandLine.- const exeName = path.basename(meta.exe);- try {- const ps = `Get-CimInstance Win32_Process -Filter \"Name='${exeName}'\" | Select-Object ProcessId, CommandLine | ConvertTo-Json -Compress`;- const out = execSync(`powershell -NoProfile -Command "${ps.replace(/"/g, '\\"')}"`, {- encoding: 'utf8',- stdio: ['ignore', 'pipe', 'ignore'],- windowsHide: true,- }).trim();- if (!out) return [];- let arr;- try { arr = JSON.parse(out); } catch { return []; }- if (!Array.isArray(arr)) arr = [arr];- return arr- .map(p => parseBrowserCmdline(Number(p.ProcessId), p.CommandLine || '', meta.realUserDataDir))- .filter(Boolean);- } catch (e) {- console.log(`[native] findRealBrowserProcesses failed: ${e.message}`);- return [];- }-}--function parseBrowserCmdline(pid, cmdline, realUserDataDir) {- // Child processes (renderer, GPU, utility) inherit --user-data-dir but carry- // --type=. Only the browser process owns top-level windows + the debug port.- // Exclude children so callers act on real browser processes only (v1.0.8).- if (/--type=/.test(cmdline)) return null;- // Determine whether this process uses the REAL user-data-dir. Match the- // KNOWN realUserDataDir as a substring rather than greedily capturing the- // flag value: Windows reports the cmdline UNQUOTED, and Chrome's default dir- // ("...\User Data") contains a space, so a \S+ capture truncates at "User"- // and never matches the real profile. (v1.0.9: this was the bug that made- // the native-drive RECONNECT path silently fail against the real profile —- // it only ever matched no-space side profiles like .adom\native-profiles.)- const nc = cmdline.replace(/\\/g, '/').toLowerCase();- const realDir = realUserDataDir.replace(/[\\/]+$/, '').replace(/\\/g, '/').toLowerCase();- let usesReal;- if (/--user-data-dir[=\s]/i.test(cmdline)) {- const esc = realDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');- usesReal = new RegExp(`--user-data-dir[=\\s]"?${esc}("|\\s|$)`, 'i').test(nc);- } else {- // No --user-data-dir flag → Chrome uses its default (= real user-data-dir).- usesReal = true;- }- if (!usesReal) return null;- const portMatch = cmdline.match(/--remote-debugging-port=(\d+)/);- return {- pid,- port: portMatch ? Number(portMatch[1]) : 0,- cmdline,- };-}--// Send WM_CLOSE to every browser process (graceful — Chrome flushes Sessions-// and Cookies cleanly), wait for them to exit, force-kill stragglers. The-// graceful path is what lets --restore-last-session bring tabs back; a-// straight /F kill leaves Chrome's "Restore pages?" interstitial which the-// user has to click manually.-async function closeRealBrowserGracefully(meta, processes) {- if (processes.length === 0) return;- const exeName = path.basename(meta.exe);- console.log(`[native] graceful WM_CLOSE on ${processes.length} ${exeName} process(es)...`);- if (process.platform === 'win32') {- // taskkill WITHOUT /F sends WM_CLOSE to the PID's top-level windows. Chrome- // catches it, runs its shutdown path (writes Sessions, flushes Cookies,- // closes child processes), then exits. v1.0.8: target the SPECIFIC browser- // PIDs — NOT /IM chrome.exe, which would also close Chrome-for-Testing- // bridge sessions and any other Chromium sharing the exe name.- for (const p of processes) {- try {- execSync(`taskkill /PID ${p.pid}`, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });- } catch {- // Non-zero exit is common (some refuse; we'll wait then escalate).- }- }- } else {- for (const p of processes) {- try { process.kill(p.pid, 'SIGTERM'); } catch {}- }- }- // Wait up to 15s for the exe to actually disappear.- for (let i = 0; i < 30; i++) {- await new Promise(r => setTimeout(r, 500));- if (findRealBrowserProcesses(meta).length === 0) {- console.log(`[native] all ${exeName} processes exited cleanly after ${(i + 1) * 0.5}s`);- return;- }- }- // Force-kill anything still alive. Chrome's session file is already on- // disk by now; --restore-last-session will still bring tabs back, the- // user just sees the "Chrome didn't shut down correctly" toast briefly.- console.log(`[native] graceful close took too long — force-killing remaining ${exeName}`);- try {- if (process.platform === 'win32') {- // Per-PID /F /T (kill the process tree) on the specific browser PIDs- // still alive — never /IM, to spare CfT sessions + other Chromium (v1.0.8).- for (const p of findRealBrowserProcesses(meta)) {- try { execSync(`taskkill /F /T /PID ${p.pid}`, { stdio: 'ignore', windowsHide: true }); } catch {}- }- } else {- for (const p of findRealBrowserProcesses(meta)) {- try { process.kill(p.pid, 'SIGKILL'); } catch {}- }- }- } catch {}- await new Promise(r => setTimeout(r, 2000));-}--// Spawn the user's REAL browser detached on its REAL user-data-dir with-// a fresh debug port + session restore, return a connected puppeteer.Browser.-async function spawnRealBrowserWithDebugPort(meta, profileId, debugPort) {- const { spawn } = require('child_process');- const args = [- `--user-data-dir=${meta.realUserDataDir}`,- `--profile-directory=${profileId}`,- `--remote-debugging-port=${debugPort}`,- // The whole reason this UX is acceptable: tabs come back exactly as- // they were before the close. Chrome reads its Sessions/ dir and- // restores windows/tabs across all profiles that had them open.- '--restore-last-session',- // Silence first-run prompts.- '--no-first-run',- '--no-default-browser-check',- // NOTE: deliberately NOT passing --disable-blink-features=AutomationControlled- // (v1.0.12). (1) It's redundant: we puppeteer.connect() to a browser spawned- // WITHOUT --enable-automation, so navigator.webdriver is already false and- // there's no "controlled by automated software" infobar to suppress.- // (2) Edge treats it as an "unsupported command-line flag" and shows a yellow- // "stability and security risks" banner for the entire session.- ];- console.log(`[native] relaunching ${meta.displayName} on real ${meta.realUserDataDir} with --remote-debugging-port=${debugPort} + --restore-last-session`);- const child = spawn(meta.exe, args, { detached: true, stdio: 'ignore', windowsHide: false });- child.unref();-- // Wait up to 30s for CDP to come up. Real Chrome with managed-profile- // enrollment + session restore takes longer than a fresh CfT launch.- for (let i = 0; i < 60; i++) {- await new Promise(r => setTimeout(r, 500));- try {- const resp = await fetch(`http://127.0.0.1:${debugPort}/json/version`);- if (resp.ok) {- const browser = await puppeteer.connect({- browserURL: `http://127.0.0.1:${debugPort}`,- defaultViewport: null,- });- browser._cdpPort = debugPort;- browser._detachedPid = child.pid;- console.log(`[native] connected to real ${meta.displayName} on CDP :${debugPort} (spawned PID ${child.pid})`);- // --restore-last-session faithfully reopens the prior session — including- // any blank / new-tab windows, which pile up as "a bunch of empty windows"- // before the target URL opens. Prune the blanks (keep every REAL restored- // tab); the target tab is created afterwards by openTabInNativeBrowser, so- // closing blanks here can't touch it. Settle first — restore reopens tabs- // asynchronously, so a brief wait lets them all register before we prune.- await new Promise(r => setTimeout(r, 1500));- try {- const isBlank = (u) => !u || u === 'about:blank' ||- /^(chrome|edge|brave):\/\/(newtab|new-tab-page)\b/.test(u) ||- u.startsWith('chrome-search://local-ntp');- const pages = await browser.pages();- const blanks = pages.filter(p => isBlank(p.url()));- const reals = pages.filter(p => !isBlank(p.url()));- // Close every blank when real tabs exist; if the whole restored session- // was blank, keep ONE so the browser doesn't quit before the target opens.- const toClose = reals.length > 0 ? blanks : blanks.slice(1);- for (const p of toClose) { try { await p.close({ runBeforeUnload: false }); } catch {} }- if (toClose.length) {- console.log(`[native] pruned ${toClose.length} empty restored window(s)/tab(s); kept ${pages.length - toClose.length} (${reals.length} real)`);- }- } catch (e) { console.log(`[native] empty-window prune skipped: ${e.message}`); }- return browser;- }- } catch { /* CDP not ready yet — keep polling */ }- }- throw new Error(`Real ${meta.displayName} did not expose CDP on :${debugPort} within 30s after relaunch`);-}--// Public: launch a session driving the user's REAL browser.-// Behavior:-// - If real browser is already running with --remote-debugging-port → connect.-// - Else if nativeRelaunch:true → close gracefully, relaunch with the port,-// --restore-last-session brings tabs back.-// - Else → throw errorCode:"native_relaunch_required" so the caller can-// surface the consent prompt to the user and re-call with the flag.-async function launchNativeSession(sessionId, url, opts = {}) {- const { browser: browserName, profileId, nativeRelaunch = false } = opts;-- if (!browserName) throw makeNativeError('native_no_browser', 'nativeBrowser is required');- if (browserName === 'firefox') {- throw makeNativeError('firefox_no_cdp',- 'Firefox does not support CDP — cannot drive natively. Use a Chromium-family browser (chrome / edge / brave). Firefox is included in desktop_list_browsers for visibility but rejected here.');- }- if (!profileId) throw makeNativeError('native_no_profile', 'nativeProfile is required (e.g. "Default", "Profile 1")');-- const meta = nativeBrowserMeta(browserName);- if (!meta) {- throw makeNativeError('native_browser_not_installed',- `Browser "${browserName}" not detected on this machine. Run desktop_list_browsers to see what's installed.`);- }- if (!fs.existsSync(meta.realUserDataDir)) {- throw makeNativeError('native_user_data_missing',- `Real User Data dir not found at ${meta.realUserDataDir} — launch ${meta.displayName} once manually so it creates its profile, then retry.`);- }- const realProfileDir = path.join(meta.realUserDataDir, profileId);- if (!fs.existsSync(realProfileDir)) {- throw makeNativeError('native_profile_not_found',- `Profile "${profileId}" does not exist under ${meta.realUserDataDir}. Run desktop_list_browsers to list valid profile ids.`);- }-- const key = nativeKey(browserName, profileId);-- // 1. In-memory hit: this bridge already drives this (browser, profile).- const existingEntry = browsers.get(key);- if (existingEntry && existingEntry.browser.isConnected()) {- const memSession = await openTabInNativeBrowser(existingEntry.browser, sessionId, url, key);- memSession._narrative = `Already driving ${meta.displayName} (${profileId}) — reused the live connection, nothing relaunched.`;- return memSession;- }- if (existingEntry) browsers.delete(key);-- // 2. Probe the real browser's running processes. If one of them ALREADY- // has --remote-debugging-port, the user has either set that up- // themselves (the power-user path) or our own previous-session- // relaunch is still alive. Either way, just connect.- const realProcs = findRealBrowserProcesses(meta);- const procWithPort = realProcs.find(p => p.port > 0);- if (procWithPort) {- console.log(`[native] found real ${meta.displayName} PID ${procWithPort.pid} already exposing CDP on :${procWithPort.port} — connecting without relaunch`);- try {- const browser = await puppeteer.connect({- browserURL: `http://127.0.0.1:${procWithPort.port}`,- defaultViewport: null,- });- browser._cdpPort = procWithPort.port;- browser._detachedPid = procWithPort.pid;- browsers.set(key, { browser, refCount: 0 });- browser.on('disconnected', () => browsers.delete(key));- clearCdpBlocked(meta.realUserDataDir); // it CAN do CDP — forget any prior block- const reconnSession = await openTabInNativeBrowser(browser, sessionId, url, key);- reconnSession._narrative = `${meta.displayName} was already running with a debug port — connected without relaunching anything.`;- return reconnSession;- } catch (e) {- console.log(`[native] connect to existing port :${procWithPort.port} failed (${e.message}); falling through to relaunch path`);- }- }-- // 3. No usable CDP port on the real browser. First: if we've already learned- // this profile can't expose CDP, tell the caller now (diagnosis +- // alternatives) — never close/relaunch a browser for a doomed attempt.- if (isCdpBlocked(meta.realUserDataDir)) throw cdpBlockedError(meta, browserName);-- // 3a. Browser is NOT running → nothing to disrupt → launch it fresh with the- // debug port WITHOUT asking the user (no windows to close). If the port- // refuses to bind (policy), close the throwaway window + report it.- if (realProcs.length === 0) {- console.log(`[native] ${meta.displayName} not running — launching fresh with debug port (no disruption, no consent needed)`);- const freshPort = 9300 + Math.floor(Math.random() * 700);- let freshBrowser;- try {- freshBrowser = await spawnRealBrowserWithDebugPort(meta, profileId, freshPort);- } catch (e) {- markCdpBlocked(meta.realUserDataDir, meta.displayName);- try { await closeRealBrowserGracefully(meta, findRealBrowserProcesses(meta)); } catch {}- throw cdpBlockedError(meta, browserName);- }- clearCdpBlocked(meta.realUserDataDir);- browsers.set(key, { browser: freshBrowser, refCount: 0 });- freshBrowser.on('disconnected', () => browsers.delete(key));- const freshSession = await openTabInNativeBrowser(freshBrowser, sessionId, url, key);- freshSession._launchedFresh = true;- freshSession._narrative = `${meta.displayName} wasn't open, so AD launched it fresh with the debug port — no tabs closed, nothing disrupted. Connected and ready to drive.`;- return freshSession;- }-- // 3b. Browser IS running without a debug port. Closing it is disruptive, so- // require explicit consent (nativeRelaunch:true) — surfaced with the- // close-and-reopen prompt + alternatives the AI can offer the user.- if (!nativeRelaunch) {- throw relaunchRequiredError(meta, browserName, profileId, realProcs.length);- }-- // 4. Caller confirmed. Close real browser cleanly, relaunch with debug port.- await closeRealBrowserGracefully(meta, realProcs);- const debugPort = 9300 + Math.floor(Math.random() * 700);- let browser;- try {- browser = await spawnRealBrowserWithDebugPort(meta, profileId, debugPort);- } catch (e) {- // Relaunched but never exposed CDP (debug port never bound). Remember this- // profile so the next consent probe fails fast instead of closing the browser- // again — most commonly enterprise RemoteDebuggingAllowed (v1.0.10).- markCdpBlocked(meta.realUserDataDir, meta.displayName);- throw cdpBlockedError(meta, browserName);- }- clearCdpBlocked(meta.realUserDataDir); // success — forget any prior block- browsers.set(key, { browser, refCount: 0 });- browser.on('disconnected', () => browsers.delete(key));- const relaunchedSession = await openTabInNativeBrowser(browser, sessionId, url, key);- relaunchedSession._narrative = `Closed and reopened ${meta.displayName} with the debug port (tabs restored via --restore-last-session). Connected and ready to drive.`;- return relaunchedSession;-}--async function openTabInNativeBrowser(browser, sessionId, url, key) {- // v1.0.7: ALWAYS open a NEW tab for native drive — never hijack one of the- // user's existing tabs. The old `pages[0]` reuse had two problems: (1) it- // navigated whatever the user had open in their first tab (data loss), and- // (2) when that page redirected (e.g. github.com/login/device → /login),- // the captured frame DETACHED, leaving the session's tab pointing at a dead- // frame — every subsequent eval / input_dispatch failed with "detached- // Frame" and browser_rescan could not re-acquire it. A fresh page navigates- // cleanly through redirects, which is exactly what the device-code / OAuth- // consent flows need.- const page = await browser.newPage();- if (url) {- try { await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 }); }- catch (e) { console.log(`[native] initial navigate failed (continuing): ${e.message}`); }- }- let session = sessions.get(sessionId);- if (!session) {- session = createSession(key);- sessions.set(sessionId, session);- }- session.profileName = key;- session.tabs = [{ tabId: `${sessionId}-tab-1`, page, errors: [] }];- session.activeTabId = session.tabs[0].tabId;- session._native = true;- const be = browsers.get(key);- if (be) be.refCount++;- activeSessionId = sessionId;- return session;-} // Append a tab to an existing session. Returns the new tabId. async function addTabToSession(sessionId, url) {@@ -2883,7 +2252,7 @@ setInterval(async () => { // asked to adopt: a calling AI should never have to guess and never have to // read docs first. { verb: { hint, related[], pitfalls[] } }. const VERB_META = {- browser_open_window: { hint: 'Opens a session you then drive by sessionId. Pass nativeBrowser+nativeProfile to drive the user\'s REAL signed-in Chrome/Edge/Brave; omit both for a clean CfT window.', related: ['browser_navigate', 'browser_list_windows', 'browser_close_window'], pitfalls: ['sessionId is required and is your handle for every later call', 'nativeBrowser and nativeProfile must be passed together', 'run desktop_list_browsers first to see installed native profiles'] },+ browser_open_window: { hint: 'Opens a generic, fully-controllable pup window (a fresh isolated Chrome/Edge process — never your real signed-in browser) that you then drive by sessionId. To drive the user\'s REAL Chrome/Edge, use the Adom browser extension\'s nbrowser_* verbs instead.', related: ['browser_navigate', 'browser_list_windows', 'browser_close_window'], pitfalls: ['sessionId is required and is your handle for every later call', 'profile defaults to sessionId — omit it for a plain "open in pup"'] }, browser_open_tab: { hint: 'Adds a tab to an existing session; returns the new tabId.', related: ['browser_switch_tab', 'browser_list_tabs', 'browser_close_tab'], pitfalls: ['the session must already exist (browser_open_window first)'] }, browser_close_tab: { hint: 'Closes one tab; the session stays open.', related: ['browser_close_window', 'browser_list_tabs'], pitfalls: ['omit tabId to close the active tab — closing the last tab may close the window'] }, browser_close_window: { hint: 'Closes the whole session and all its tabs. Destructive.', related: ['browser_list_windows'], pitfalls: ['this ends the session — its sessionId is no longer valid afterward'] },@@ -3024,7 +2393,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 browser window/session (CfT, or the user\'s real Chrome/Edge/Brave via nativeBrowser).', { sessionId: 'required str', url: 'optional str', profile: 'optional str', nativeBrowser: 'optional chrome|edge|brave', nativeProfile: 'optional str' }, 60, { sessionId: 'work', url: 'https://example.com' }),+ 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. profile defaults to sessionId. (For the user\'s REAL Chrome/Edge, use the Adom extension\'s nbrowser_* verbs.)', { sessionId: 'required str', url: 'optional str', profile: 'optional str — defaults to sessionId' }, 60, { sessionId: 'work', url: 'https://example.com' }), V('browser_open_tab', 'Open a new tab in a session.', { sessionId: 'required str', url: 'optional str' }, 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' }),@@ -3059,21 +2428,14 @@ const server = http.createServer(async (req, res) => { sendJSON(res, { success: false, error: 'sessionId is required. Example: browser_open_window { "sessionId": "dart2", "profile": "dart2", "url": "https://..." }' }); return; }- // v1.0.5+: native drive — nativeBrowser+nativeProfile route to the- // user's REAL Chrome/Edge/Brave profile via a cloned user-data-dir.- // Both args required together; mutually exclusive with the pup- // `profile` arg. Default (no native args) is byte-for-byte the same- // CfT path as v1.0.4 — strictly additive, no regressions.- const wantsNative = !!(args.nativeBrowser || args.nativeProfile);- // ── Cold-start gate (CfT path only) — READ-ONLY detect; never blocks ──+ // ── Cold-start gate — READ-ONLY detect; never blocks ── // pup drives an ALREADY-INSTALLED browser (Chrome → Edge; EVERY Windows box // has Edge) with a fresh isolated profile — so readiness is almost always TRUE // instantly with NO download, and this gate just falls through. Only if the // machine has NO Chromium browser at all (no Chrome, no Edge, no cached CfT) —- // rare — do we fall back to fetching Chrome for Testing, kicked off in the- // BACKGROUND here (non-blocking, pollable {installing:true}). Native drive uses- // the user's REAL browser, so it skips this.- if (!wantsNative) {+ // rare — do we fetch Chrome for Testing as a fallback, kicked off in the+ // BACKGROUND here (non-blocking, pollable {installing:true}).+ { const rd = chrome.readiness(); // detect-only; never downloads if (!rd.ready) { if (rd.installPhase === 'failed') {@@ -3082,7 +2444,7 @@ const server = http.createServer(async (req, res) => { errorCode: rd.lastErrorCode || 'chrome_install_failed', error: `No Chrome/Edge installed and the Chrome-for-Testing fallback failed to install: ${rd.lastError}`, readiness: rd,- _hint: 'This machine has no Chrome or Edge, and the Chrome-for-Testing fallback download failed (usually no network / a proxy blocking storage.googleapis.com, or low disk). Fastest fix: have the user install Chrome or Edge (Edge ships with Windows) — pup will then use it instantly with no download. Or retry browser_prewarm. Fallback: drive their real browser via browser_open_window with nativeBrowser+nativeProfile.',+ _hint: 'This machine has no Chrome or Edge, and the Chrome-for-Testing fallback download failed (usually no network / a proxy blocking storage.googleapis.com, or low disk). Fastest fix: have the user install Chrome or Edge (Edge ships with Windows) — pup will then use it instantly with no download. Or retry browser_prewarm.', }); return; }@@ -3112,60 +2474,13 @@ const server = http.createServer(async (req, res) => { return; } }- if (wantsNative) {- if (!args.nativeBrowser || !args.nativeProfile) {- sendJSON(res, { success: false, error: 'nativeBrowser and nativeProfile must be passed together. Example: browser_open_window { "sessionId": "gcp", "nativeBrowser": "chrome", "nativeProfile": "Default", "url": "https://console.cloud.google.com" }. Run desktop_list_browsers first to see what is installed.' });- return;- }- if (args.profile) {- sendJSON(res, { success: false, error: 'nativeBrowser/nativeProfile are mutually exclusive with `profile` (pup-managed CfT). Drop `profile` to drive the real browser, OR drop nativeBrowser/nativeProfile to use CfT.' });- return;- }- const sessionId = args.sessionId;- try {- const session = await launchNativeSession(sessionId, args.url, {- browser: args.nativeBrowser,- profileId: args.nativeProfile,- nativeRelaunch: !!args.nativeRelaunch,- });- const info = await getSessionInfo(sessionId, session);- sendJSON(res, {- success: true,- output: JSON.stringify({- ok: true,- native: true,- nativeBrowser: args.nativeBrowser,- nativeProfile: args.nativeProfile,- launchedFresh: !!session._launchedFresh,- narrative: session._narrative || null,- ...info,- }),- });- } catch (err) {- // Surface every structured field the caller needs: the consent- // prompt (confirmArgs/mustCloseBrowser), the policy diagnosis, the- // alternative browsers it can offer the user, and the always-- // available generic-pup fallback. (v1.0.11)- sendJSON(res, {- success: false,- errorCode: err.errorCode || 'native_launch_failed',- error: err.message,- displayName: err.displayName,- runningProcesses: err.runningProcesses,- mustCloseBrowser: err.mustCloseBrowser,- confirmArgs: err.confirmArgs,- diagnosis: err.diagnosis,- alternatives: err.alternatives,- genericPupAvailable: err.genericPupAvailable,- cdpBlocked: err.cdpBlocked,- });- }- return;- }- if (!args.profile) {- sendJSON(res, { success: false, error: 'profile is required. Example: browser_open_window { "sessionId": "dart2", "profile": "dart2", "url": "https://..." }. Use same value as sessionId for a dedicated profile, or share another session\'s profile to share IndexedDB/cookies. (For driving your real Chrome instead of CfT, pass nativeBrowser + nativeProfile — see desktop_list_browsers.)' });- return;- }+ // pup is a generic, non-profile browser: each session is a fresh isolated+ // profile (its own Chrome/Edge process), never the user's real signed-in+ // browser. `profile` names that isolated user-data-dir; default it to the+ // sessionId so a bare "open in pup" (just sessionId + url) works cleanly.+ // (To drive the user's REAL Chrome/Edge, that's the Adom browser EXTENSION's+ // nbrowser_* verbs — a separate bridge, not pup.)+ if (!args.profile) args.profile = args.sessionId; const sessionId = args.sessionId; // v1.6.3+: optional downloadPath override. Defaults to the user's // home Downloads dir, which is what desktop_watch_files polls by