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
v2.0.733 (insiders): pup tells an AI about importing the user's own logins before anyone is stuck at a sign-in page (John, 2026-10-20: 'i'd rather him auto discover it'). The first pup window each AI thread opens on Windows carries a one-time _importTip while pup has fewer than 5 saved logins (setting importTipBelow) and no import has ever run; it never repeats after the first import. Also: the extension lane, every window's default, now handles sign-in pages after each open and navigate (fills a saved login in the background, never submitted; otherwise _loginTip tells the AI to offer pup_import_browser_logins, one Adom Bridge approval plus one Windows prompt, and on a Mac says what works instead), the new pup-import-logins skill, and login phrases in the wiki page's discovery triggers and pitch. Tested on ConfRoomROG: the tip shows on a thread's first window and not its second; a sign-in page with no saved login carries _loginTip; a saved login is filled (username landed, not submitted); an ordinary page carries neither. Gates on arav-rog and ConfRoomROG: smoke all pass, foreground pass, jump list at baseline, isolated-close replay kept every window, parity 48/56 (pup-bridge#131). Includes 2.0.730.
6 files changed
+65−17
rust/Cargo.lock+12−12@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-pup-bridge"-version = "2.0.730"+version = "2.0.733" dependencies = [ "pup-browser", "pup-cdp",@@ -781,7 +781,7 @@ dependencies = [ [[package]] name = "pup-browser"-version = "2.0.730"+version = "2.0.733" dependencies = [ "pup-core", "pup-host",@@ -793,7 +793,7 @@ dependencies = [ [[package]] name = "pup-cdp"-version = "2.0.730"+version = "2.0.733" dependencies = [ "serde_json", "thiserror",@@ -803,7 +803,7 @@ dependencies = [ [[package]] name = "pup-core"-version = "2.0.730"+version = "2.0.733" dependencies = [ "serde", "serde_json",@@ -813,7 +813,7 @@ dependencies = [ [[package]] name = "pup-dashboard"-version = "2.0.730"+version = "2.0.733" dependencies = [ "pup-cdp", "pup-core",@@ -826,7 +826,7 @@ dependencies = [ [[package]] name = "pup-host"-version = "2.0.730"+version = "2.0.733" dependencies = [ "serde", "serde_json",@@ -835,7 +835,7 @@ dependencies = [ [[package]] name = "pup-host-mac"-version = "2.0.730"+version = "2.0.733" dependencies = [ "core-foundation", "core-graphics",@@ -851,7 +851,7 @@ dependencies = [ [[package]] name = "pup-host-win"-version = "2.0.730"+version = "2.0.733" dependencies = [ "pup-core", "pup-host",@@ -861,7 +861,7 @@ dependencies = [ [[package]] name = "pup-identity"-version = "2.0.730"+version = "2.0.733" dependencies = [ "fontdue", "image",@@ -871,7 +871,7 @@ dependencies = [ [[package]] name = "pup-session"-version = "2.0.730"+version = "2.0.733" dependencies = [ "pup-core", "serde",@@ -881,7 +881,7 @@ dependencies = [ [[package]] name = "pup-vault"-version = "2.0.730"+version = "2.0.733" dependencies = [ "pup-core", "pup-host",@@ -892,7 +892,7 @@ dependencies = [ [[package]] name = "pup-verbs"-version = "2.0.730"+version = "2.0.733" dependencies = [ "base64", "png 0.17.16",
rust/Cargo.toml+1−1@@ -4,7 +4,7 @@ members = ["core", "host", "host-win", "host-mac", "cdp", "browser", "session", # One version for the whole bridge, in lockstep with src/BRIDGE_VERSION (tools/ship.sh bumps both). [workspace.package]-version = "2.0.730"+version = "2.0.733" edition = "2021" license = "MIT"
rust/verbs/src/import.rs+2@@ -100,6 +100,8 @@ fn import_now(ctx: &Ctx, caller: &Caller, args: &Value) -> Value { let filters: Vec<String> = match &args["hostFilter"] { Value::String(s) => vec![s.to_lowercase()], Value::Array(a) => a.iter().filter_map(|v| v.as_str().map(|s| s.to_lowercase())).collect(), _ => vec![] }; let filtered: Vec<Value> = if filters.is_empty() { rows.clone() } else { rows.iter().filter(|r| { let h = r["host"].as_str().or(r["url"].as_str()).or(r["origin"].as_str()).unwrap_or("").to_lowercase(); filters.iter().any(|f| h.contains(f)) }).cloned().collect() }; let (added, updated, skipped) = ctx.vault.merge_imported(&*ctx.host, &filtered);+ // 2.0.733: remembered so the first-window import tip (vault::import_tip_once) stops for good after an import.+ let _ = std::fs::write(ctx.host.state_dir().join("pup-logins-imported.json"), json!({ "at": pup_core::now_ms(), "added": added, "updated": updated }).to_string()); let st = &payload["stats"]; pup_core::log(format!("[import] merge done: +{added} new, {updated} updated, {skipped} skipped{}", if filters.is_empty() { String::new() } else { format!(" (filtered to: {})", filters.join(", ")) })); ok(json!({ "success": true, "output": json!({ "ok": true, "added": added, "updated": updated, "skipped": skipped,
rust/verbs/src/lib.rs+4−3@@ -816,7 +816,7 @@ pub fn dispatch(ctx: &Ctx, caller: &Caller, verb: &str, args: &Value) -> Value { if let Ok(mut inner) = serde_json::from_str::<Value>(&inner_s) { if let Some(i) = inner.as_object_mut() { let sid = i.get("sessionId").and_then(|v| v.as_str()).map(|s| s.to_string()).or_else(|| o.get("sessionId").and_then(|v| v.as_str()).map(|s| s.to_string())); if let Some(sid) = sid { if !i.contains_key("window") { i.insert("window".into(), json!(sid)); if !i.contains_key("aiThread") { if let Some(owner) = ctx.sessions.get(&sid).and_then(|x| x.owner) { i.insert("aiThread".into(), json!(owner)); } } } }- for k in ["_reportIssues", "_cleanupReport", "_autoLogin", "_credentialCaptured", "_hint", "_next", "related", "pitfalls", "renderCheck", "_verifyRender"] { if let Some(v) = o.get(k) { if !i.contains_key(k) { i.insert(k.into(), v.clone()); } } }+ for k in ["_reportIssues", "_cleanupReport", "_autoLogin", "_loginTip", "_importTip", "loginPage", "savedLogins", "_credentialCaptured", "_hint", "_next", "related", "pitfalls", "renderCheck", "_verifyRender"] { if let Some(v) = o.get(k) { if !i.contains_key(k) { i.insert(k.into(), v.clone()); } } } o.insert("output".into(), json!(inner.to_string())); } } }@@ -1643,7 +1643,7 @@ fn navigate_ext(ctx: &Ctx, caller: &Caller, s: &Session, url: &str) -> Value { // floaty pencil) and the wiki view probe with the desktop-token auto-sign. let tab_now = ctx.sessions.get(&s.id).and_then(|x| x.tab_by_id(&tab.tab_id).cloned()).unwrap_or_else(|| tab.clone()); inject_page_scripts_ext(ctx, s, &lane, &tab_now);- let wiki_extra = wiki::after_navigate_ext(ctx, s, &lane, &tab_now, &u, None, false);+ let wiki_extra = pup_core::merge(wiki::after_navigate_ext(ctx, s, &lane, &tab_now, &u, None, false), vault::login_check_ext(ctx, &lane, &tab_now, &u)); // 2.0.644 (pup-bridge#119): a cancelled HTTP auth challenge is reported as a typed error, not a blank page. if let Some((host, answered)) = transport::ext_id(&tab).as_i64().and_then(vault::unanswered_challenge) { if !answered { return pup_core::merge(vault::no_credential_error(&host, false), json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": tab.tab_id, "url": u, "title": title, "transport": "ext" })); }@@ -2064,7 +2064,8 @@ pub fn open_window_ext(ctx: &Ctx, caller: &Caller, args: &Value, sid: &str, owne ctx.sessions.insert(s.clone()); ctx.sessions.save_file(&s); // 2.0.653: page scripts + wiki probe on the extension lane's first page (the CDP open always did both) let mut wiki_extra = json!({});- if let (Ok(lane), Some(t0)) = (transport::lane_for(ctx, &s), s.tabs.get(0).cloned()) { if t0.url != "about:blank" { inject_page_scripts_ext(ctx, &s, &lane, &t0); wiki_extra = wiki::after_navigate_ext(ctx, &s, &lane, &t0, &t0.url, args.get("wikiView").and_then(|v| v.as_str()), false); } }+ if let (Ok(lane), Some(t0)) = (transport::lane_for(ctx, &s), s.tabs.get(0).cloned()) { if t0.url != "about:blank" { inject_page_scripts_ext(ctx, &s, &lane, &t0); wiki_extra = wiki::after_navigate_ext(ctx, &s, &lane, &t0, &t0.url, args.get("wikiView").and_then(|v| v.as_str()), false); wiki_extra = pup_core::merge(wiki_extra, vault::login_check_ext(ctx, &lane, &t0, &t0.url)); } }+ wiki_extra = pup_core::merge(wiki_extra, vault::import_tip_once(ctx, owner.unwrap_or(caller.thread.as_str()))); extdrive::apply_consumer_brands(ctx); // once per lane browser; before the first page the user cares about extdrive::close_orphan_blank_windows(ctx, chrome_win); // the startup window a relaunch left behind, if any extdrive::fit_now();
rust/verbs/src/settings.rs+2−1@@ -17,7 +17,8 @@ pub const KEYS: &[(&str, &str, &str)] = &[ ("defaultLane", "ext", "ext|cdp"), ("consumerBrands", "on", "on|off"), ("laneStockFlags", "on", "on|off"),- ("cdpLane", "off", "on|off"), // 2.0.652 (John): the CDP lane is opt-in per box; the extension lane must solve every use case+ ("cdpLane", "off", "on|off"),+ ("importTipBelow", "5", "int"), // 2.0.733: the first-window import tip shows while the vault has fewer saved logins than this // 2.0.652 (John): the CDP lane is opt-in per box; the extension lane must solve every use case ("laneFieldTrials", "on", "on|off"), ("consumerBrandsHeader", "", "str"), ("fgWatch", "on", "on|off"), ("birthSettleMs", "400", "int"), ("aumidShortcut", "off", "on|off"), ("idleBrowserQuitMin", "10", "int"), ("winAttentionFlash", "off", "on|off"), ("prePermissions", "off", "on|off"), ("cdpQuiet", "off", "on|off"), ("stockFlags", "off", "on|off"), ("fgCaption", "on", "on|off"), ("fgCaptionMs", "3500", "int"), ("notMineCaption", "off", "on|off"),
rust/verbs/src/vault.rs+44@@ -249,6 +249,50 @@ pub fn fill_card(ctx: &Ctx, caller: &Caller, args: &Value) -> Value { } /// Autofill on load (Node: autoLoginOnLoad): fill, never submit, and note it on the response.+/// 2.0.731 (John, 2026-10-20: "drew, who uses pup all the time, and runs into login problems all the time, you never+/// suggest to him that you can import all of his credentials"). The extension lane, every window's default, had NO+/// login handling on page load: no auto-fill, and nothing told the AI about pup_import_browser_logins until a+/// pup_login had already failed. After every extension-lane open and navigate: a page with a password field is+/// filled in the background from the vault when pup has the login (never submitted), and when it does not, the+/// answer tells the AI to OFFER the import of the user's own saved browser logins.+pub fn login_check_ext(ctx: &Ctx, lane: &crate::transport::Lane, t: &pup_session::Tab, url: &str) -> Value {+ if ctx.settings.get("autoLogin", "on") == "off" || !url.starts_with("http") { return json!({}); }+ let Ok(located) = crate::transport::evaluate(lane, t, pup_vault::locate_login_js()) else { return json!({}) };+ if located["ok"] != json!(true) { return json!({}); }+ let host = pup_vault::host_of(url).unwrap_or_default();+ match ctx.vault.get_for_url(&*ctx.host, url, None) {+ Some(cred) => {+ let v = crate::transport::evaluate(lane, t, &pup_vault::fill_login_js(&cred.username, &cred.password)).unwrap_or(Value::Null);+ if v["plen"].as_u64().unwrap_or(0) == 0 { return json!({}); }+ json!({ "_autoLogin": format!("pup AUTO-FILLED this login for saved host \"{}\" (username {}) in the BACKGROUND, NOT submitted. CONFIRM the live domain is the intended site, then pup_login {{submit:true, submitReason}} to sign in.", cred.host, cred.username) })+ }+ None => {+ let saved = ctx.vault.list().len();+ // The import of the browser's own saved passwords is Windows-only today (Chromium ABE/DPAPI through ab).+ // On a Mac, say what does work instead of offering something that will answer unsupported_platform.+ if !cfg!(windows) {+ return json!({ "loginPage": true, "savedLogins": saved, "_loginTip": format!(+ "This is a sign-in page for {host}, and pup has no saved login for it ({saved} saved in all). On this Mac pup cannot import the browser's saved passwords yet. What works: ask the user to sign in once in this pup window themselves; pup saves the login as they submit it and fills it here from then on. If they also use a Windows desktop on the same Adom account, pup_vault_sync can bring that desktop's logins over. Never ask the user to read out a password.") });+ }+ json!({ "loginPage": true, "savedLogins": saved, "_loginTip": format!(+ "This is a sign-in page for {host}, and pup has no saved login for it ({saved} saved in all). The user's own browser very likely has it: OFFER to import their saved logins, in these words or close to them: \"I can bring your saved passwords from Chrome/Edge into pup, so I can sign you in here and on other sites automatically. It takes one approval in Adom Bridge plus one Windows prompt. Want me to?\" On yes: pup_import_browser_logins {{}} (no hostFilter the first time: a filter hides logins saved under another domain). pup then fills this page by itself. Never type a password you were not given, and never ask the user to read one out.") })+ }+ }+}++/// 2.0.733 (John: "i'd rather him auto discover it"): the first pup window each AI thread opens on Windows carries a+/// one-time `_importTip` while the vault is nearly empty and no import has ever run, so an AI learns about the import+/// BEFORE the user is stuck at a sign-in page. Once per thread per pup run; never again after the first import.+pub fn import_tip_once(ctx: &Ctx, thread: &str) -> Value {+ static TOLD: std::sync::Mutex<Option<std::collections::HashSet<String>>> = std::sync::Mutex::new(None);+ if !cfg!(windows) || thread.is_empty() { return json!({}); }+ if ctx.host.state_dir().join("pup-logins-imported.json").exists() { return json!({}); }+ let saved = ctx.vault.list().len();+ if saved >= ctx.settings.get("importTipBelow", "5").parse::<usize>().unwrap_or(5) { return json!({}); }+ { let mut g = TOLD.lock().unwrap(); if !g.get_or_insert_with(Default::default).insert(thread.to_string()) { return json!({}); } }+ json!({ "_importTip": format!("Good to know before any sign-in: pup has only {saved} saved login(s), and the user's own Chrome or Edge almost certainly has many more. pup can bring them all over in one approval (an Adom Bridge consent plus one Windows prompt): pup_import_browser_logins {{}}. After that pup fills sign-in pages by itself. When this task needs a login, or at a natural pause, OFFER it in plain words: \"I can bring your saved passwords from Chrome or Edge into pup, so I can sign you into sites automatically. Want me to?\" Offer once; if they say no, do not ask again. See the pup-import-logins skill.") })+}+ pub fn autofill_on_load(ctx: &Ctx, s: &Session, cdp_session: &str, url: &str) -> Option<String> { if ctx.settings.get("autoLogin", "on") == "off" { return None; } let b = crate::browser_for(ctx, &s.profile)?;