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.730 (insiders): pup_eval and pup_login work on strict pages again (pup-bridge#130): American Express's login page refuses page scripts with 'eval is disabled', a wording pup's fallback did not recognize, so both failed on the extension lane; they now fall back to the DevTools path as on other strict pages (verified on arav-rog: eval returns the Amex page title, pup_login fills the username). macOS (Kyle's pull request #5): --use-mock-keychain on every Chrome launch so a new browser build never pops a keychain prompt, a depth-counted capture guard, and tools/rust-build-macos.sh. Gates on arav-rog and ConfRoomROG: smoke all pass, foreground pass, jump list at baseline (same three known failures), isolated-close replay kept every window, parity 48/56 (pup-bridge#131). Includes 2.0.729.
7 files changed
+53−19
rust/Cargo.lock+12−12@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-pup-bridge"-version = "2.0.729"+version = "2.0.730" dependencies = [ "pup-browser", "pup-cdp",@@ -781,7 +781,7 @@ dependencies = [ [[package]] name = "pup-browser"-version = "2.0.729"+version = "2.0.730" dependencies = [ "pup-core", "pup-host",@@ -793,7 +793,7 @@ dependencies = [ [[package]] name = "pup-cdp"-version = "2.0.729"+version = "2.0.730" dependencies = [ "serde_json", "thiserror",@@ -803,7 +803,7 @@ dependencies = [ [[package]] name = "pup-core"-version = "2.0.729"+version = "2.0.730" dependencies = [ "serde", "serde_json",@@ -813,7 +813,7 @@ dependencies = [ [[package]] name = "pup-dashboard"-version = "2.0.729"+version = "2.0.730" dependencies = [ "pup-cdp", "pup-core",@@ -826,7 +826,7 @@ dependencies = [ [[package]] name = "pup-host"-version = "2.0.729"+version = "2.0.730" dependencies = [ "serde", "serde_json",@@ -835,7 +835,7 @@ dependencies = [ [[package]] name = "pup-host-mac"-version = "2.0.729"+version = "2.0.730" dependencies = [ "core-foundation", "core-graphics",@@ -851,7 +851,7 @@ dependencies = [ [[package]] name = "pup-host-win"-version = "2.0.729"+version = "2.0.730" dependencies = [ "pup-core", "pup-host",@@ -861,7 +861,7 @@ dependencies = [ [[package]] name = "pup-identity"-version = "2.0.729"+version = "2.0.730" dependencies = [ "fontdue", "image",@@ -871,7 +871,7 @@ dependencies = [ [[package]] name = "pup-session"-version = "2.0.729"+version = "2.0.730" dependencies = [ "pup-core", "serde",@@ -881,7 +881,7 @@ dependencies = [ [[package]] name = "pup-vault"-version = "2.0.729"+version = "2.0.730" dependencies = [ "pup-core", "pup-host",@@ -892,7 +892,7 @@ dependencies = [ [[package]] name = "pup-verbs"-version = "2.0.729"+version = "2.0.730" 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.729"+version = "2.0.730" edition = "2021" license = "MIT"
rust/browser/src/lib.rs+16@@ -509,6 +509,17 @@ pub fn sessions_dir(host: &dyn Host) -> PathBuf { static STOCK_FLAGS: AtomicBool = AtomicBool::new(false); pub fn set_stock_flags(on: bool) { STOCK_FLAGS.store(on, Ordering::SeqCst); } +/// 2.0.720: switches a platform needs so Chrome never raises an OS prompt of its own. macOS: Chromium's+/// OSCrypt otherwise creates/reads a "Chromium Safe Storage" item in the user's LOGIN KEYCHAIN to encrypt+/// cookies, and the keychain ACL is per BINARY, so EVERY new Chrome-for-Testing build pops "Google Chrome+/// for Testing wants to use your confidential information…" (Kyle, 2026-10-20, via H). pup's profiles are+/// isolated throwaways, so an in-memory key is right; the mock keychain's key is constant across launches,+/// so cookies written under it still persist. Cookies encrypted under a real keychain key before this flag+/// are unreadable once (one re-login). Windows/Linux: nothing (DPAPI / basic store prompt nothing).+pub fn platform_switches() -> Vec<String> {+ if cfg!(target_os = "macos") { vec!["--use-mock-keychain".into()] } else { vec![] }+}+ /// The switches every pup launch carries (server.js v2.0.293 merged form: ONE --disable-features). pub fn chrome_switches(profile_dir: &Path, high_fps: bool, insecure_dev: bool, extensions: &[PathBuf]) -> Vec<String> { if STOCK_FLAGS.load(Ordering::SeqCst) {@@ -526,6 +537,7 @@ pub fn chrome_switches(profile_dir: &Path, high_fps: bool, insecure_dev: bool, e // Preferences so pre-acknowledging is impossible. Keep ONLY pup's own extensions enabled. v.push(format!("--disable-extensions-except={}", extensions.iter().map(|p| p.to_string_lossy().to_string()).collect::<Vec<_>>().join(","))); } v.push("--no-startup-window".into());+ v.extend(platform_switches()); return v; } let mut v: Vec<String> = vec![@@ -563,6 +575,7 @@ pub fn chrome_switches(profile_dir: &Path, high_fps: bool, insecure_dev: bool, e // birthed an extra untracked about:blank window that pup never stamped: a stock Chrome for Testing // button with the attention tint on the taskbar (John, 2026-09-06). Never pass a URL here. v.push("--no-startup-window".into());+ v.extend(platform_switches()); v } @@ -694,6 +707,7 @@ pub fn launch_extdrive_opts(host: &dyn Host, cand: &Candidate, profile_dir: &Pat // (the CDP lane's note said "Never pass a URL here"), and the cold lane was launched with "about:blank": // that made an unowned about:blank window that sat ON TOP for 18 s until the orphan sweep closed it, // on every cold start (measured, 2.0.631).+ args.extend(platform_switches()); if !url.is_empty() { args.push(url.to_string()); } return host.spawn_detached(&cand.executable_path, &args, None).map_err(|e| format!("{}: {e}", cand.kind)); }@@ -743,6 +757,7 @@ pub fn launch_extdrive_opts(host: &dyn Host, cand: &Candidate, profile_dir: &Pat // 2.0.633: same rule as the stock branch above (2.0.632 only patched that one; this is the DEFAULT branch). // An empty argument was handed to Chrome as "" and it opened its working directory as a file listing, // on top of the user, for the whole session (measured on ConfRoomROG: "Index of C:\Users\john\.cache\puppeteer\").+ args.extend(platform_switches()); if !url.is_empty() { args.push(url.to_string()); } host.spawn_detached(&cand.executable_path, &args, None).map_err(|e| format!("{}: {e}", cand.kind)) }@@ -824,3 +839,4 @@ mod tests { } } +
rust/host-mac/src/lib.rs+14−5@@ -299,9 +299,11 @@ fn process_table() -> Vec<(u32, u32, u64, String)> { } /// Apps capture_frames_begin unhid, for capture_frames_end to park again.-fn capture_unhidden() -> &'static Mutex<Vec<u32>> {- static U: std::sync::OnceLock<Mutex<Vec<u32>>> = std::sync::OnceLock::new();- U.get_or_init(|| Mutex::new(Vec::new()))+// Depth-counted so two concurrent captures share one unhide/re-hide (John's review on PR #4): the apps list+// is set by the OUTERMOST begin and re-hidden by the LAST end; inner calls only bump the count.+fn capture_unhidden() -> &'static Mutex<(usize, Vec<u32>)> {+ static G: std::sync::OnceLock<Mutex<(usize, Vec<u32>)>> = std::sync::OnceLock::new();+ G.get_or_init(|| Mutex::new((0, Vec::new()))) } /// Is the whole app that owns `pid` hidden (Cmd+H state)? None when the pid has no app.@@ -401,19 +403,25 @@ impl HostWindows for MacHost { // again in capture_frames_end. The user's own Chrome is never touched (pup-profiles marker), and an app the // user has frontmost is left alone in both directions. fn capture_frames_begin(&self) -> bool {+ let mut g = capture_unhidden().lock().unwrap();+ if g.0 > 0 { g.0 += 1; return !g.1.is_empty(); } // a capture is already holding the apps unhidden let mut apps: Vec<u32> = Vec::new(); for (pid, _) in self.pup_browser_processes() { if app_hidden(pid) == Some(true) && !app_is_frontmost(pid) && !apps.contains(&pid) { apps.push(pid); } }+ g.0 = 1; if apps.is_empty() { return false; } for pid in &apps { imp::set_app_hidden(*pid, false); } // one compositor turn so the first capture after the unhide finds a frame std::thread::sleep(std::time::Duration::from_millis(150));- *capture_unhidden().lock().unwrap() = apps;+ g.1 = apps; true } fn capture_frames_end(&self) {- let apps = std::mem::take(&mut *capture_unhidden().lock().unwrap());+ let mut g = capture_unhidden().lock().unwrap();+ g.0 = g.0.saturating_sub(1);+ if g.0 > 0 { return; }+ let apps = std::mem::take(&mut g.1); for pid in apps { if !app_is_frontmost(pid) { imp::set_app_hidden(pid, true); } } } // Windows: FlashWindowEx(FLASHW_STOP). No taskbar flash on macOS; nothing to stop.@@ -746,3 +754,4 @@ mod tests { } } +
rust/verbs/src/record.rs+2@@ -275,6 +275,7 @@ fn ensure_hud(ctx: &Ctx, reason: &str) -> Result<(), Value> { let url = format!("file:///{}?reason={}", html.to_string_lossy().replace('\\', "/"), reason.bytes().map(|b| if b.is_ascii_alphanumeric() { (b as char).to_string() } else { format!("%{b:02X}") }).collect::<String>()); let port_file = profile_dir.join("DevToolsActivePort"); let _ = std::fs::remove_file(&port_file); let args = vec![format!("--user-data-dir={}", profile_dir.display()), "--remote-debugging-port=0".into(), format!("--app={url}"), "--window-size=420,260".into(), "--use-fake-ui-for-media-stream".into(), "--auto-select-desktop-capture-source=Entire screen".into(), "--enable-features=GetDisplayMediaSet".into(), "--auto-accept-this-tab-capture".into(), "--no-first-run".into(), "--no-default-browser-check".into(), "--disable-features=CalculateNativeWinOcclusion,MediaRouter".into()];+ let args = [args, pup_browser::platform_switches()].concat(); let pid = ctx.host.spawn_detached(&cand.executable_path, &args, None).map_err(|e| err("recorder_launch_failed", e.to_string(), None))?; let t0 = Instant::now(); let mut port = None; while t0.elapsed() < Duration::from_secs(20) { if let Ok(s) = std::fs::read_to_string(&port_file) { if let Some(p) = s.lines().next().and_then(|l| l.trim().parse::<u16>().ok()) { port = Some(p); break; } } std::thread::sleep(Duration::from_millis(200)); }@@ -350,3 +351,4 @@ pub fn recorder_close(ctx: &Ctx) -> Value { ok(json!({ "success": true, "_hint": SCREEN_HINT, "output": json!({ "ok": true, "sessionSummary": summary }).to_string() })) } +
rust/verbs/src/transport.rs+4−1@@ -96,7 +96,10 @@ pub fn evaluate(lane: &Lane, t: &Tab, expr: &str) -> Result<Value, Value> { // injected eval; Runtime.evaluate through the DevTools driver ignores page CSP, as the CDP lane does. // 2.0.591 (pup-bridge#116): Trusted Types refuse with "violates this document's Trusted Type assignment // requirements", which the CSP wording never matched, so Google Calendar and Admin got no fallback.- if m.contains("Content Security Policy") || m.contains("unsafe-eval") || m.contains("Trusted Type") || m.contains("Evaluating a string as JavaScript") {+ // 2.0.730 (pup-bridge#130): American Express's login page refuses it as "eval is disabled", which none of the+ // phrases above matched, so pup_eval and pup_login failed there on the extension lane.+ if m.contains("Content Security Policy") || m.contains("unsafe-eval") || m.contains("Trusted Type") || m.contains("Evaluating a string as JavaScript")+ || m.contains("eval is disabled") || m.contains("EvalError") || m.contains("Refused to evaluate") { return crate::extdrive::ext_handle().cdp.evaluate(&t.cdp_session, expr, Duration::from_secs(20)).map_err(|e| err("eval_failed", e.to_string(), None)); } return Err(err("eval_failed", format!("pup_eval could not run your code: {m}"), None));
src/server.js+4@@ -10478,6 +10478,8 @@ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPer '--start-maximized', '--remote-debugging-port=0', // Suppress "Sign in to Chromium" popup and first-run dialogs '--no-first-run', '--no-default-browser-check', '--disable-default-apps',+ // 2.0.720 macOS: in-memory cookie key; otherwise OSCrypt pops a login-keychain prompt per CfT binary.+ ...(process.platform === 'darwin' ? ['--use-mock-keychain'] : []), '--disable-sync', '--disable-background-networking', '--disable-infobars', // v2.0.57 (John: the "Chromium now launches when Windows starts" promo infobar on fresh@@ -21306,6 +21308,7 @@ async function ensureRecorderWindowOpen(reason) { '--no-sandbox', '--no-first-run', '--no-default-browser-check',+ ...(process.platform === 'darwin' ? ['--use-mock-keychain'] : []), // 2.0.720: see main launch // v2.0.293: MediaRouter's mDNS binds 0.0.0.0:5353 — a non-loopback listener that pops the // Windows Firewall dialog (per-exe). pup's hard rule is ZERO firewall prompts, so the // recorder Chrome disables it exactly like the main launch path (one flag, comma-merged —@@ -21994,3 +21997,4 @@ async function gracefulShutdown(signal) { } process.on('SIGINT', () => gracefulShutdown('SIGINT')); process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));+