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
pup-native credential manager (John authorized), so the AI manages logins without Chromes foreground picker. Auto-capture: as any login is submitted in a pup window, the username and password are recorded into the OS keychain vault (encrypted at rest, never in the index, never logged, never echoed) and the owning thread is hinted that its adom-you identity just grew. Auto-fill on return: matching pages fill in the background over CDP, no Chrome dropdown, no foregrounding. Submit is REASON-GATED and domain-confirmed per John: pup never auto-submits; the AI must call pup_login submit true with a submitReason, and the refusal regurgitates the exact live host so a lookalike or phishing domain is caught before the secret is sent. New pup_credentials verb lists saved logins host and username only; captureLogins and the richer autoLogin modes are dashboard settings and pup_configure keys
4 files changed
+112−16
adom-bridge-puppeteer-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "2.0.109",- "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.109/adom-bridge-puppeteer-v2.0.109.zip",- "sha256": "06f27e04bbab15d1f15e77bb7dd196a0b33af002caff61793dbbe2c0aa5f7454",- "size": 857354,+ "version": "2.0.110",+ "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.110/adom-bridge-puppeteer-v2.0.110.zip",+ "sha256": "e6325ee8c4b17d412ce2c61376eb450912c7b817e8d581ebd0b36c1139c8107b",+ "size": 860685, "verbPrefixes": [ "browser_", "desktop_recorder_",
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-2.0.109+2.0.110
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "2.0.109",+ "version": "2.0.110", "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+106−10@@ -327,6 +327,40 @@ function threadNextBlock() { 'pup_screenshot {} captures the PAGE (CDP, pixel-perfect, works in background — the shot you want 99% of the time). Only when you need the browser CHROME in frame (tab strip, toolbar icons): pup_window_info {} hands you this window\'s hwnd for desktop_screenshot_window — no searching, ever.', ]; }+// v2.0.110 (John authorized: pup runs its OWN login store so the AI manages credentials without+// Chrome's foreground picker). This rail records a login as it is submitted: on submit of a form+// holding a filled secret field, it stashes {host, username, secret} on window.__pupCapturedLogin.+// The 10s sweep harvests it into the OS-keychain vault (never the index, never a log) and hints+// the caller. Read-and-cleared atomically by the sweep to minimize how long it sits in the page.+const CAPTURE_LOGIN_JS = `(() => {+ if (window !== window.top) return;+ if (window.__pupCapV === '${BRIDGE_VERSION}') return;+ window.__pupCapV = '${BRIDGE_VERSION}';+ function vis(el){ return !!el && !el.disabled && el.offsetParent !== null && el.getClientRects().length > 0; }+ function grab(form){+ try {+ var pwd = [].slice.call((form || document).querySelectorAll('input[type=password]')).filter(vis)[0];+ if (!pwd || !pwd.value) return;+ var all = [].slice.call((form || document).querySelectorAll('input'));+ var pi = all.indexOf(pwd);+ var user = [].slice.call((form || document).querySelectorAll('input[autocomplete=username], input[type=email]')).filter(vis)[0];+ if (!user) {+ var cands = [].slice.call((form || document).querySelectorAll('input[type=text], input[type=tel], input:not([type])')).filter(vis);+ user = cands.slice().reverse().filter(function(c){ return all.indexOf(c) < pi; })[0] || cands[0] || null;+ }+ window.__pupCapturedLogin = { host: location.hostname, u: user ? user.value : '', p: pwd.value, ts: Date.now() };+ } catch (e) {}+ }+ addEventListener('submit', function(e){ grab(e.target && e.target.tagName === 'FORM' ? e.target : null); }, true);+ addEventListener('click', function(e){+ try {+ var t = e.target && e.target.closest && e.target.closest('button, input[type=submit], [role=button]');+ if (!t) return;+ var f = t.form || (t.closest && t.closest('form'));+ if (f && f.querySelector('input[type=password]')) grab(f);+ } catch (e2) {}+ }, true);+})()`; let pupshotUrl = null; // v1.9.4: profileName -> in-flight launch Promise. A fresh profile isn't in `browsers` until its // Chrome launch FINISHES (~10-15s), so two near-simultaneous opens for the same profile used to@@ -721,6 +755,9 @@ function attachTab(session, sessionId, page, { makeActive = true, opener = 'user // registers after them, so the strip wins). const tjClean = titleJs(''); try { page.evaluateOnNewDocument(tjClean).catch(() => {}); page.evaluate(tjClean).catch(() => {}); } catch (e) {}+ // v2.0.110: login-capture rail — records credentials as they are submitted so the keychain+ // vault grows the adom-you identity automatically (John authorized). Self-guarded, page-local.+ try { page.evaluateOnNewDocument(CAPTURE_LOGIN_JS).catch(() => {}); page.evaluate(CAPTURE_LOGIN_JS).catch(() => {}); } catch (e) {} } try { page.evaluateOnNewDocument(PUP_DEEP_HELPERS).catch(() => {}); } catch (e) {} // v1.9.48: re-evaluate the window CATEGORY when this tab finishes navigating.@@ -3100,6 +3137,7 @@ function pupSettings() { D.jlAnnotate = 'on'; // v2.0.49 // v2.0.50 (plan: interactive surface): caption bar baked under captures; toolbar extension family D.annCaptionBar = 'on'; D.toolbarExtension = 'on'; D.tbOwnerBadge = 'on'; D.tbSnipHotkey = 'on'; D.tbFloatyPencil = 'off'; D.floatyHitPx = 24;+ D.captureLogins = 'on'; // v2.0.110 (John authorized): auto-capture form logins into the keychain vault // v1.9.293 (John): the 'NOT foregrounded by Adom' caption is now OFF by default. He is right that // input-timing cannot reliably tell his own raises (click / alt+tab / window click) from a rare // JS raise - and Windows blocks most background foreground-steals anyway, so the case it exists@@ -9539,17 +9577,24 @@ function wikiUrlLint(u) { async function autoLoginOnLoad(res, page) { try { if (!res || !page) return;- let mode = 'on';- try { mode = (pupSettings().autoLogin === 'off') ? 'off' : 'on'; } catch (e) {}+ let mode = 'submit'; // v2.0.110: default now fill+submit (confidence-gated below); 'fill'/'off' downgrade+ try { const av = pupSettings().autoLogin; mode = (av === 'off') ? 'off' : (av === 'fill' ? 'fill' : 'submit'); } catch (e) {} if (mode === 'off') return; let url = ''; try { url = page.url(); } catch (e) { return; } if (!url || !/^https?:/i.test(url)) return; const creds = await credentialVault.getCredentialForUrl(url); if (!creds) return;+ // v2.0.110 (John): pup FILLS in the background but NEVER auto-submits. Submitting a stored+ // credential is reason-gated (like foregrounding): the AI must call pup_login {submit:true,+ // submitReason} and, in doing so, confirm the LIVE domain it is handing the password to — the+ // hint regurgitates that exact domain so a lookalike/phishing host is caught before the secret+ // is sent. mode 'fill' is identical here (fill-only); 'off' skips entirely.+ let liveHost = '';+ try { liveHost = new URL(url).hostname; } catch (e) {} const r = await credentialVault.fillLoginForm(page, { username: creds.username, password: creds.password, submit: false }); if (r && r.ok && r.filledPassword) {- res._autoLoginNote = `pup AUTO-FILLED this login form for "${creds.host}" from your saved credential (username ${creds.username}) — in the BACKGROUND, NOT submitted, no Chrome dropdown, no foregrounding. To sign in: pup_login {sessionId,submit:true} or click the submit button yourself. Wrong form? pup_configure {autoLogin:"off"} turns this off.`;+ res._autoLoginNote = `pup AUTO-FILLED this login for saved host "${creds.host}" (username ${creds.username}) in the BACKGROUND, NOT submitted — no Chrome dropdown, no foregrounding. ⛔ BEFORE SUBMITTING, CONFIRM THE DOMAIN: you are about to send this credential to "${liveHost}". Is that the domain you intended, and does it match where this login belongs? A lookalike/typo/redirected host here is how credentials get phished. If you are sure, submit with pup_login {submit:true, submitReason:"<why you are confident this is the right site to send these creds to>"} (the reason is required; it forces you to actually verify the domain). pup_configure {autoLogin:"off"} disables auto-fill.`; } } catch (e) { /* autofill is best-effort; never disturb the navigation */ } }@@ -9735,6 +9780,14 @@ function sendJSON(res, obj, status = 200) { if (_rc2 && inner.renderCheck === undefined) inner.renderCheck = _rc2; // v1.9.125: surface any background auto-login note. if (res && res._autoLoginNote && inner._autoLogin === undefined) { inner._autoLogin = res._autoLoginNote; touched = true; }+ // v2.0.110: drain a pending login-capture hint for this window (set async by the+ // sweep when a login was captured), so the owning thread learns it on its next call.+ try {+ const _cs = sessions.get(inner.sessionId || (obj && obj.sessionId));+ if (_cs && _cs._pendingCredHint && inner._credentialCaptured === undefined) {+ inner._credentialCaptured = _cs._pendingCredHint; _cs._pendingCredHint = null; touched = true;+ }+ } catch (e) {} // v1.9.118: CALL-TIME reprimand — fires on the URL itself, pass or fail. try { if (inner._urlLint === undefined) {@@ -10458,7 +10511,8 @@ const SETTINGS_DEF=[ {k:'autoFlash',l:'Taskbar flash on agent activity',d:'Flash a window taskbar button when an AI changes that window (navigate, click, type). One flash per burst.'}, {k:'agentActivity',l:'Taskbar progress bar',d:'Show a progress strip on a window taskbar button while an AI is actively driving it.'}, {k:'sleepRecovery',l:'Sleep and crash recovery',d:'After the laptop wakes or Chrome dies, automatically bring every pup window back at its last page, signed in. Never restarts a live Chrome.'},- {k:'autoLogin',l:'Auto-fill saved logins',d:'Fill (never submit) a matching saved login when a login page loads in a pup window.'},+ {k:'autoLogin',l:'Auto-fill saved logins',d:'Fill a matching saved login when a login page loads in a pup window (background, no Chrome dropdown). pup never submits on its own: submitting a stored password is reason-gated and confirms the live domain first, to guard against lookalike/phishing sites.'},+ {k:'captureLogins',l:'Capture logins to the vault',d:'As you sign into sites in pup windows, store the username and password in the OS keychain (encrypted at rest, never logged) so return visits auto-fill. This is how the shared adom-you identity grows over time.'}, {k:'reportIssues',l:'Issue-report hints',d:'Let pup invite AIs to file a wiki issue when they hit something broken.'}, {k:'titleTag',l:'Session tag in the window title',d:'Append "(ai-thread: <name>)" to each window title so pup (and you) can tell windows apart from the title bar. Off by default because it can look like the page mangled its own title; pup still resolves windows without it (transiently, while off-screen).'}, {k:'titlePrefix',l:'Thread name prefixed to the title',d:'Prepend the owning thread (and wiki logged-in/out state) to the start of each window title. Off by default.'},@@ -10772,7 +10826,7 @@ const server = http.createServer(async (req, res) => { aumidIcons: ['on', 'off'], jumpLists: ['on', 'off'], fgWatch: ['on', 'off'], fgCaption: ['on', 'off'], overlayBadges: ['on', 'off'], autoFlash: ['on', 'off'], openFlash: ['on', 'off'], sleepRecovery: ['on', 'off'], agentActivity: ['on', 'off'],- reportIssues: ['on', 'off'], autoLogin: ['on', 'off'], minimalTouch: ['on', 'off'],+ reportIssues: ['on', 'off'], autoLogin: ['on', 'off', 'submit', 'fill'], captureLogins: ['on', 'off'], minimalTouch: ['on', 'off'], taskbarGrouping: ['split', 'grouped'], aumidIconStyle: ['standard', 'category'], jlViewTasks: ['on', 'off'], jlCloseAll: ['on', 'off'], jlDashboard: ['on', 'off'], jlAnnotate: ['on', 'off'], annCaptionBar: ['on', 'off'], toolbarExtension: ['on', 'off'], tbOwnerBadge: ['on', 'off'], tbSnipHotkey: ['on', 'off'], tbFloatyPencil: ['on', 'off'],@@ -11392,7 +11446,8 @@ const server = http.createServer(async (req, res) => { V('pup_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 pup_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('pup_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('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 vault (matched on the tab URL; store via credential_set or save:true) or passed explicitly. Passwords are NEVER echoed back. Vault-matched login pages also AUTO-FILL on load (_autoLogin on the response; pup_configure {autoLogin:"off"} disables). Pairs with the durable shared profile: sign in once, every window stays signed in.', { sessionId: 'required str', username: 'optional str', password: 'optional str — with username, fills these instead of the vault', submit: 'optional bool — click the submit button after filling (default false)', 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' }, 20, { sessionId: 'work', submit: true }),+ 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' }, 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_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>' }), // Session/tab/window management + probes (were routed but undocumented).@@ -12038,12 +12093,21 @@ const server = http.createServer(async (req, res) => { sendJSON(res, { success: false, error: `reportIssues must be one of on|off` }); return; }- // v1.9.125: autoLogin on|off — pup auto-filling a vault-matched login form on page load.+ // v1.9.125 / v2.0.110: autoLogin — pup auto-filling a vault-matched login form on page+ // load. 'on'|'submit' = fill (submit stays reason-gated via pup_login); 'fill' = same+ // fill-only; 'off' = do not auto-fill. const al = args.autoLogin;- if (al !== undefined && !['on', 'off'].includes(al)) {- sendJSON(res, { success: false, error: `autoLogin must be one of on|off` });+ if (al !== undefined && !['on', 'off', 'submit', 'fill'].includes(al)) {+ sendJSON(res, { success: false, error: `autoLogin must be one of on|off|submit|fill` });+ return;+ }+ // v2.0.110: captureLogins on|off — record form logins into the keychain vault as they submit.+ const cl = args.captureLogins;+ if (cl !== undefined && !['on', 'off'].includes(cl)) {+ sendJSON(res, { success: false, error: `captureLogins must be one of on|off` }); return; }+ if (cl) { savePupSettings({ captureLogins: cl }); console.log(`[configure] captureLogins -> ${cl}`); } // v1.9.185: minimalTouch on|off — see the minimalTouch() note. 'on' strips pup down to // open-a-window + drive-it-over-CDP, disabling every cosmetic/janitor action pup performs // ON Chrome (taskbar identity/badges/jump lists/progress/flash/toasts, the in-page title@@ -14878,10 +14942,11 @@ const server = http.createServer(async (req, res) => { } return; }+ case 'pup_credentials': // v2.0.110: friendlier alias case 'credential_list': { try { const credentials = credentialVault.listCredentials();- sendJSON(res, { success: true, output: JSON.stringify({ ok: true, credentials }) });+ sendJSON(res, { success: true, output: JSON.stringify({ ok: true, credentials, _hint: credentials.length ? `${credentials.length} saved login(s) in the OS keychain (passwords never listed). pup auto-fills these in the background; submit is reason-gated via pup_login. Remove one: credential_delete {host}.` : 'No saved logins yet. pup captures them automatically as you sign into sites (captureLogins:"on"), or store one explicitly with credential_set {host, username, password}.' }) }); } catch (e) { sendJSON(res, { success: false, error: e.message }); }@@ -14926,6 +14991,16 @@ const server = http.createServer(async (req, res) => { _hint: `No credential to fill. Either pass {username, password} (add {save:true} to remember it for this host), or store one first with credential_set {host:"${(() => { try { return new URL(curUrl).hostname; } catch (e) { return '<host>'; } })()}", username, password} and call pup_login again. pup then fills the form in the BACKGROUND — no Chrome dropdown, no foregrounding.` }); return; }+ // v2.0.110 (John): SUBMIT is reason-gated + domain-confirmed. Sending a stored secret+ // is the irreversible act — refuse submit:true without a submitReason, and regurgitate+ // the LIVE domain so the AI must confirm it is not a lookalike/phishing host first.+ if (args.submit && (!args.submitReason || String(args.submitReason).trim().length < 10)) {+ let liveHost = '<unknown>'; try { liveHost = new URL(curUrl).hostname; } catch (e) {}+ sendJSON(res, { success: false, errorCode: 'submit_reason_required', output: JSON.stringify({ ok: false, url: curUrl, host: liveHost }),+ _hint: `⛔ CONFIRM THE DOMAIN BEFORE SENDING CREDENTIALS. You asked to submit a stored login, but this is the point of no return: the password goes to "${liveHost}" the instant you submit. Is "${liveHost}" truly the site these credentials belong to — not a lookalike, typo-squat, or a host you were redirected to? If you are sure, re-call pup_login {submit:true, submitReason:"<why you are confident ${liveHost} is the correct, intended domain for this login>"} (min 10 chars). Not sure? Do NOT submit — pup already filled the fields, so a human can finish, or you can navigate away.` });+ return;+ }+ if (args.submit) { let lh = ''; try { lh = new URL(curUrl).hostname; } catch (e) {} console.log(`[cred] pup_login submit to ${lh} — reason: ${String(args.submitReason).slice(0, 200)}`); } const r = await credentialVault.fillLoginForm(tab.page, { username, password, submit: !!args.submit }); if (!r || !r.ok) { sendJSON(res, { success: false, errorCode: r && r.reason === 'no_password_field' ? 'no_login_form' : 'login_fill_failed',@@ -15544,6 +15619,27 @@ setInterval(async () => { } } } catch (e) {}+ // v2.0.110: login-capture harvest — read-and-clear any credential the capture rail recorded on+ // submit, store it in the OS keychain vault, and stage a hint for the owning thread's next+ // response. The password is handed straight to keytar and never logged (we log host+user only).+ try {+ if (pupSettings().captureLogins === 'off') throw 0;+ for (const [sidC, sC] of [...sessions]) {+ if (sC._lostBrowser || !(sC.tabs || []).length) continue;+ for (const tC of (sC.tabs || [])) {+ if (!tC || !tC.page) continue;+ let cap = null;+ try { cap = await Promise.race([tC.page.evaluate(() => { const c = window.__pupCapturedLogin; window.__pupCapturedLogin = null; return c || null; }), new Promise(r => setTimeout(() => r(null), 2000))]); } catch (e) {}+ if (cap && cap.host && cap.p) {+ try {+ await credentialVault.setCredential({ host: cap.host, username: cap.u || '', password: cap.p });+ console.log(`[cred] captured login for ${cap.host}${cap.u ? ` (${cap.u})` : ''} on "${sidC}" → keychain vault`);+ sC._pendingCredHint = `pup CAPTURED this login for "${cap.host}"${cap.u ? ` (username ${cap.u})` : ''} into the OS keychain (encrypted at rest; never logged, never echoed). Next visit auto-fills and signs in with no Chrome dropdown and no foregrounding. This is how the shared adom-you identity grows — every login you do makes future ones automatic. Manage: pup_credentials {} to list, credential_delete {host} to remove, pup_configure {captureLogins:"off"} to stop capturing.`;+ } catch (e) { console.log(`[cred] capture store failed for ${cap.host}: ${e.message}`); }+ }+ }+ }+ } catch (e) {} // v2.0.102: title CLEANUP sweep (the 2.0.98 owner re-assert inverted). Strips the prefix any // elder keeper still applies on pages the attach-time cleanup missed. Idempotent via guard. try {