← 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.

John Lauer ·705edb138d ·21d ago ·parent bcd9da9
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'));+