← Commit history

Leaked isolated-profile reaper: iso-* throwaway profile directories from isolated:true / wikiView:public opens (which never got cleaned up and piled up into the hundreds per machine) are now reaped automatically every 15 minutes and at boot. Removes an iso-* dir only when no live session or session file owns it, no Chrome process is holding it, and it is older than 15 minutes; durable jars (adom-you and named profiles) can never match the iso- prefix so are never touched. New pup_reap_profiles verb triggers it on demand with dryRun preview.

John Lauer ·b944b1dcd3 ·2mo ago ·parent 8f92c29
4 files changed +79−6
adom-bridge-puppeteer-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "puppeteer",-  "version": "2.0.128",-  "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.128/adom-bridge-puppeteer-v2.0.128.zip",-  "sha256": "0e04953162cadbf22033ddd2567acb8974cc8383befaf8b84ebebf4ce2046d2a",-  "size": 873349,+  "version": "2.0.129",+  "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.129/adom-bridge-puppeteer-v2.0.129.zip",+  "sha256": "9b6c9ab88752195a1982ea80f8863252250065025c1e2051d3f63df5f6628660",+  "size": 875277,   "verbPrefixes": [     "browser_",     "desktop_recorder_",
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-2.0.128+2.0.129
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "puppeteer",   "displayName": "Puppeteer (Chrome for Testing + Native)",-  "version": "2.0.128",+  "version": "2.0.129",   "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. pup_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. pup_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. macOS (v1.9.74+): full parity with mac-native mechanics \u2014 background-by-default is the app-level hide (windows stay drivable/screenshottable while hidden), raise/lower map to unhide+activate/hide, pup_alert_window posts a native toast whose click focuses the window, pup_use {browser:'chrome',install:true} installs Chrome from the official dmg, and taskbar identity/badging remains Windows-only.",   "homepage": "https://wiki.adom.inc/adom/adom-bridge",   "author": "Adom Inc.",
src/server.js+73
@@ -2485,6 +2485,56 @@ async function reapOrphanPupChromes() {   return { killed, remaining: remaining.length }; } +// v2.0.129: reap LEAKED isolated profile DIRECTORIES on disk. Every isolated:true / wikiView:public+// open mints a throwaway `iso-*` profile under PROFILES_DIR; they are never reused and never meant to+// persist, but nothing deleted the directory when the window closed — so they piled up (387 found on+// one machine, each a full Chrome profile). This removes an iso-* dir only when ALL hold true:+//   (a) no LIVE session owns it, and no on-disk session FILE references it (recovery-pending safety);+//   (b) no live Chrome process is holding it (never delete an in-use dir);+//   (c) it hasn't been modified in the last 15 min (never race an in-flight isolation launch).+// Durable jars (adom-you, named profiles, _recorder) never carry the iso- prefix, so they can NEVER be+// hit. Same empty-map guard as reapOrphanPupChromes. Pass {dryRun:true} to preview without deleting.+async function reapLeakedIsolatedProfiles(opts) {+  const dryRun = !!(opts && opts.dryRun);+  const MIN_AGE_MS = 15 * 60 * 1000;+  const out = { removed: 0, scanned: 0, spared: 0, held: 0, tooFresh: 0, removedNames: [], dryRun };+  let entries = [];+  try { entries = fs.readdirSync(PROFILES_DIR, { withFileTypes: true }).filter(d => d.isDirectory() && /^iso-/.test(d.name)).map(d => d.name); }+  catch (e) { return out; }+  out.scanned = entries.length;+  if (!entries.length) return out;+  // Spare anything a live session OR a session file references.+  const spared = new Set();+  for (const [, s] of sessions) { if (s && s.profileName) spared.add(s.profileName); }+  let sessFiles = [];+  try { sessFiles = listSessionFiles(); } catch (e) {}+  for (const sf of sessFiles) { if (sf && sf.profile) spared.add(sf.profile); }+  // Empty-map + pending-files guard: no live sessions but session files exist → recovery is pending;+  // an iso session could still be re-adopted, so do NOT reap this cycle (mirrors the process reaper).+  if (!sessions.size && sessFiles.length) { out.skipped = 'recovery_pending'; return out; }+  // Which iso profiles have a live Chrome process holding them (never delete an in-use dir).+  const heldDirs = new Set();+  if (_isWin) {+    try {+      const procs = await _psProcs(`$_.CommandLine -like '*${_psLike(PROFILES_DIR)}*'`);+      for (const p of procs) { const c = (p.cmd || '').toLowerCase(); for (const n of entries) { if (c.includes(path.join(PROFILES_DIR, n).toLowerCase())) heldDirs.add(n); } }+    } catch (e) {}+  }+  const now = Date.now();+  for (const name of entries) {+    if (spared.has(name)) { out.spared++; continue; }+    if (heldDirs.has(name)) { out.held++; continue; }+    const dir = path.join(PROFILES_DIR, name);+    let st; try { st = fs.statSync(dir); } catch (e) { continue; }+    if (now - st.mtimeMs < MIN_AGE_MS) { out.tooFresh++; continue; }+    if (dryRun) { out.removed++; if (out.removedNames.length < 50) out.removedNames.push(name); continue; }+    try { fs.rmSync(dir, { recursive: true, force: true }); out.removed++; if (out.removedNames.length < 50) out.removedNames.push(name); }+    catch (e) { /* locked / in-use — leave it for a later cycle */ }+  }+  if (out.removed && !dryRun) recordCleanup(`reaped ${out.removed} leaked isolated profile(s) with no live session (${out.spared} spared, ${out.held} in-use, ${out.tooFresh} too fresh) — durable jars untouched`);+  return out;+}+ // Tear down the browser for a profile and VERIFY it. Returns {killed, remaining}; remaining MUST be 0. // Falls back to browser.close() if PID isn't available. `browser` may be null when we're just cleaning // up an orphaned lock file and have no live puppeteer handle.@@ -11601,6 +11651,7 @@ const server = http.createServer(async (req, res) => {             V('pup_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 pup_alert_window (or the auto-flash), NOT raise. After the show-me: pup_lower_os_window to return it to the background. Alias: pup_focus_window. IMPORTANT: if you are foregrounding in order to CLICK a native dialog or a cross-origin FedCM/Google account chooser, do NOT raise-then-click yourself — use pup_assisted_click, which waits for the user to actually be on the window (alt-tab aware) and narrates via captions, so you never act while they are not ready.', { sessionId: 'optional str', foregroundReason: 'REQUIRED — why the user asked to see this window' }, 15, { foregroundReason: 'user said "show me the wiki page"' }),             V('pup_login', 'Fill (and optionally submit) an HTML login form in the CURRENT tab, in the BACKGROUND — no Chrome autofill dropdown, no foregrounding, React/Vue-safe. Creds from the keychain vault (matched on the tab URL; captured automatically as you log in, or stored via credential_set / save:true) or passed explicitly. Passwords are NEVER echoed back. Vault-matched pages AUTO-FILL on load (_autoLogin on the response). SUBMIT IS REASON-GATED: submit:true requires submitReason AND makes you confirm the live domain (the refusal regurgitates the exact host so a lookalike/phishing site is caught before the secret is sent). pup_configure {autoLogin:"off"} disables auto-fill; {captureLogins:"off"} stops auto-capture.', { sessionId: 'optional str — omit to target the window your own thread owns', username: 'optional str', password: 'optional str — with username, fills these instead of the vault', submit: 'optional bool — submit after filling; REQUIRES submitReason', submitReason: 'required WITH submit:true — why you are confident the CURRENT domain is the correct, intended site to send these credentials to (min 10 chars; the refusal shows you the live host to confirm; logged, auditable)', save: 'optional bool — with explicit creds, store them (OS keychain) so next time is automatic', host: 'optional str — override the vault host key when saving', realKeystrokes: 'optional bool — type with REAL key events instead of a background .value-set. Needed for React-controlled MASKED fields (Gusto/Keycloak SSO) that ignore .value and would submit an empty password. It briefly FOREGROUNDS the window (real keystrokes only register on-screen). Use when a normal fill returned filledPassword:false / reactMaskedFieldSuspected, or when the login-recipe for the site says so.' }, 20, { sessionId: 'work', submit: true, submitReason: 'confirmed the tab is on the real vendor domain I intended to sign into' }),             V('pup_credentials', 'List saved logins (host + username only; passwords are in the OS keychain and NEVER returned). pup captures these automatically as you sign into sites and auto-fills them in the background on return visits.', {}, 10, {}),+            V('pup_reap_profiles', 'Delete LEAKED isolated profile directories — the iso-* throwaway jars minted by isolated:true / wikiView:public opens whose window is long gone (they used to pile up, 100s per machine). Only removes an iso-* dir that no live session or session file owns, that no Chrome is holding, and that is >15 min old; durable jars (adom-you, named profiles) are NEVER touched. Runs automatically every 15 min too. dryRun:true previews the list without deleting.', { dryRun: 'optional bool — preview which iso-* profiles would be removed, delete nothing' }, 20, { dryRun: true }),             V('pup_uninstall', 'DESTRUCTIVE — cleanly remove pup\'s WHOLE footprint from this machine: the HKCU AppUserModelId Adom.Pup registry keys, all staged extensions, ALL pup profiles + sessions, the OS-keychain credential vault, pup-settings.json, the badge-debug temp dir, and the jump-list launcher. ALWAYS preview first with dryRun:true (removes nothing, returns the plan). To actually remove, pass confirm:"uninstall". keepCredentials:true preserves the saved-login vault; keepProfiles:true preserves profiles/sessions. Chrome-for-Testing + firewall rules are SHARED and left alone unless removeChrome:true / removeFirewallRules:true. Closes every window + quits every browser first to release profile locks. (The bridge itself is managed by Adom Bridge — remove pup from Bridge to stop it re-seeding.)', { dryRun: 'optional bool — preview the removal plan without removing anything (do this first)', confirm: 'required str "uninstall" to actually remove', keepCredentials: 'optional bool — keep the saved-login vault', keepProfiles: 'optional bool — keep pup profiles + sessions', removeChrome: 'optional bool — also delete the Chrome-for-Testing install (shared; default keep)', removeFirewallRules: 'optional bool — also remove the CfT firewall rules (needs admin)' }, 60, { dryRun: true }),             V('pup_alert_window', 'Nudge the user about a pup window WITHOUT stealing focus — on Windows it flashes the taskbar button; on macOS (no taskbar, and no API to bounce another app\'s Dock tile) it unhides the window at its z-position and posts an AD toast whose click focuses it. Same contract on both, so call it identically; just never tell a Mac user to look for a flashing icon. 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 pup_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('pup_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>' }),@@ -12376,6 +12427,22 @@ const server = http.createServer(async (req, res) => {           sendJSON(res, { success: true, output: JSON.stringify({ ok: true, closed, skipped, protectedByPages, _hint: closed.length ? `Closed the idle browser process for: ${closed.join(', ')}. The next pup_open_window on these profiles launches a FRESH Chrome, which re-reads staged extensions.` : `No idle browsers to close.${protectedByPages.length ? ` PROTECTED from closing (still hold open windows, likely mid-re-adoption after a restart): ${protectedByPages.join(', ')}.` : ' Every known browser still carries live sessions (listed in skipped).'}` }) });           return;         }+        case 'pup_reap_profiles': {+          // v2.0.129: manual trigger for the leaked-isolated-profile reaper (also runs periodically).+          // dryRun:true previews which iso-* throwaway dirs WOULD be removed without deleting anything.+          const dryRun = args.dryRun === true;+          const r = await reapLeakedIsolatedProfiles({ dryRun });+          sendJSON(res, { success: true, output: JSON.stringify({+            ok: true, dryRun, scannedIsoProfiles: r.scanned, wouldRemoveOrRemoved: r.removed, removed: dryRun ? 0 : r.removed,+            spared: r.spared, inUse: r.held, tooFresh: r.tooFresh, skipped: r.skipped || null, sampleNames: r.removedNames,+            _hint: r.skipped === 'recovery_pending'+              ? 'Skipped this cycle: session recovery is pending (no live sessions but session files exist on disk) — an isolated session could still be re-adopted. Retry after recovery settles.'+              : (dryRun+                  ? `${r.removed} leaked iso-* profile(s) would be removed (${r.spared} spared by a live session/file, ${r.held} in-use, ${r.tooFresh} too fresh to touch). Re-run without dryRun to delete them.`+                  : `Removed ${r.removed} leaked iso-* profile(s) (${r.spared} spared, ${r.held} in-use, ${r.tooFresh} too fresh). Durable jars (adom-you, named profiles) are never touched — only iso-* throwaways.`)+          }) });+          return;+        }         case 'pup_uninstall': {           // v2.0.126 (John): cleanly remove pup's WHOLE machine footprint. Destructive — REASON-           // GATED behind confirm:"uninstall". dryRun:true reports what WOULD be removed. Keep-flags@@ -16177,6 +16244,12 @@ setInterval(async () => {   setTimeout(() => { reapOrphanPupChromes().catch(() => {}); }, 15000);   setInterval(() => { reapOrphanPupChromes().catch(() => {}); }, 180000); +  // v2.0.129: reap LEAKED isolated profile DIRECTORIES (iso-* throwaways whose window is long gone).+  // Boot pass waits 60s so session recovery has fully settled first (never delete a profile an iso+  // session is mid-recovery on); then a slow 15-min net drains the backlog a few at a time.+  setTimeout(() => { reapLeakedIsolatedProfiles().catch(() => {}); }, 60000);+  setInterval(() => { reapLeakedIsolatedProfiles().catch(() => {}); }, 15 * 60 * 1000);+   // Periodic cleanup: every 5 minutes, remove stale session files with dead PIDs   setInterval(async () => {     const files = listSessionFiles();