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.737 (insiders): WebRTC streaming apps work in pup again (pup-bridge#137): pup no longer launches its browser with --force-webrtc-ip-handling-policy=disable_non_proxied_udp and writes Chrome's default WebRTC routing back into the profile, repairing existing profiles. The restriction never stopped the Windows Firewall prompt (#91) and it left WebRTC media apps such as Google's Project Genie with no UDP candidates. Verified on ConfRoomROG after a browser restart: a STUN check now gathers a UDP host and a UDP srflx candidate (before: one dummy TCP candidate), and no firewall prompt appeared. Also: the site-session import reads Adom Bridge's source_browser / source_profile / http_only cookie fields and explains a 'no session' answer. 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.735.
4 files changed
+35−21
rust/Cargo.lock+12−12@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-pup-bridge"-version = "2.0.735"+version = "2.0.737" dependencies = [ "pup-browser", "pup-cdp",@@ -781,7 +781,7 @@ dependencies = [ [[package]] name = "pup-browser"-version = "2.0.735"+version = "2.0.737" dependencies = [ "pup-core", "pup-host",@@ -793,7 +793,7 @@ dependencies = [ [[package]] name = "pup-cdp"-version = "2.0.735"+version = "2.0.737" dependencies = [ "serde_json", "thiserror",@@ -803,7 +803,7 @@ dependencies = [ [[package]] name = "pup-core"-version = "2.0.735"+version = "2.0.737" dependencies = [ "serde", "serde_json",@@ -813,7 +813,7 @@ dependencies = [ [[package]] name = "pup-dashboard"-version = "2.0.735"+version = "2.0.737" dependencies = [ "pup-cdp", "pup-core",@@ -826,7 +826,7 @@ dependencies = [ [[package]] name = "pup-host"-version = "2.0.735"+version = "2.0.737" dependencies = [ "serde", "serde_json",@@ -835,7 +835,7 @@ dependencies = [ [[package]] name = "pup-host-mac"-version = "2.0.735"+version = "2.0.737" dependencies = [ "core-foundation", "core-graphics",@@ -851,7 +851,7 @@ dependencies = [ [[package]] name = "pup-host-win"-version = "2.0.735"+version = "2.0.737" dependencies = [ "pup-core", "pup-host",@@ -861,7 +861,7 @@ dependencies = [ [[package]] name = "pup-identity"-version = "2.0.735"+version = "2.0.737" dependencies = [ "fontdue", "image",@@ -871,7 +871,7 @@ dependencies = [ [[package]] name = "pup-session"-version = "2.0.735"+version = "2.0.737" dependencies = [ "pup-core", "serde",@@ -881,7 +881,7 @@ dependencies = [ [[package]] name = "pup-vault"-version = "2.0.735"+version = "2.0.737" dependencies = [ "pup-core", "pup-host",@@ -892,7 +892,7 @@ dependencies = [ [[package]] name = "pup-verbs"-version = "2.0.735"+version = "2.0.737" 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.735"+version = "2.0.737" edition = "2021" license = "MIT"
rust/browser/src/lib.rs+8−5@@ -575,7 +575,9 @@ pub fn chrome_switches(profile_dir: &Path, high_fps: bool, insecure_dev: bool, e // 0.0.0.0, which is exactly what makes Windows Firewall prompt "allow Google Chrome for Testing" (measured // on winvm: three UDP 0.0.0.0 sockets per launch, none with QUIC off). No prompt beats HTTP/3. "--disable-quic".into(), "--disable-async-dns".into(),- "--force-webrtc-ip-handling-policy=disable_non_proxied_udp".into(), "--webrtc-ip-handling-policy=disable_non_proxied_udp".into(),+ // 2.0.737 (pup-bridge#137): no WebRTC UDP restriction. disable_non_proxied_udp never stopped the Windows+ // Firewall prompt (#91: that came from network-service sockets, and ab's firewall rule handles it), and it+ // broke WebRTC media apps such as Google's Project Genie (no UDP candidates, the call never connects). format!("--disable-features={DISABLE_FEATURES}"), ]; if high_fps { v.extend(["--disable-background-timer-throttling", "--disable-backgrounding-occluded-windows", "--disable-renderer-backgrounding"].map(String::from)); }@@ -603,9 +605,11 @@ pub fn prepare_profile(profile_dir: &Path, pin_extensions: bool, host: Option<&d if !prefs.is_object() { prefs = json!({}); } prefs["profile"]["exit_type"] = json!("Normal"); prefs["profile"]["exited_cleanly"] = json!(true);- prefs["webrtc"]["ip_handling_policy"] = json!("disable_non_proxied_udp");- prefs["webrtc"]["nonproxied_udp_enabled"] = json!(false);- prefs["webrtc"]["multiple_routes_enabled"] = json!(false);+ // 2.0.737 (pup-bridge#137): Chrome's default WebRTC routing, written back over the old disable_non_proxied_udp so+ // existing profiles are repaired too (it broke WebRTC streaming apps and never helped the firewall prompt).+ prefs["webrtc"]["ip_handling_policy"] = json!("default");+ prefs["webrtc"]["nonproxied_udp_enabled"] = json!(true);+ prefs["webrtc"]["multiple_routes_enabled"] = json!(true); if pin_extensions { // the toolbar order IS the pinned_extensions array order (Node v2.0.150): foreign pins kept, ours appended in canonical order let ours: Vec<&str> = EXT_IDS.iter().map(|(_, id)| *id).collect();@@ -757,7 +761,6 @@ pub fn launch_extdrive_opts(host: &dyn Host, cand: &Candidate, profile_dir: &Pat // --disable-field-trial-config turns off the experiment config that GATES promos like the // launch-on-startup bar, and is also the version determinism WHY-CFT promises. "--disable-field-trial-config".into(),- "--force-webrtc-ip-handling-policy=disable_non_proxied_udp".into(), ]; if !extensions.is_empty() { args.push(format!("--load-extension={}", extensions.iter().map(|p| p.to_string_lossy().to_string()).collect::<Vec<_>>().join(",")));
rust/verbs/src/import.rs+14−3@@ -128,8 +128,19 @@ fn import_session(ctx: &Ctx, caller: &Caller, args: &Value, payload: &Value, sit let host_of = |c: &Value| c["host"].as_str().or(c["host_key"].as_str()).or(c["domain"].as_str()).unwrap_or("").to_lowercase(); let matches = |h: &str| { let h = h.trim_start_matches('.'); h == site || h.ends_with(&format!(".{site}")) }; let mut rows: Vec<Value> = all.into_iter().filter(|c| matches(&host_of(c))).collect();- if rows.is_empty() { return err("no_session", format!("None of the user's browsers has cookies for {site}, so there is no session to bring over."), Some("Ask the user to sign in to that site once in their own browser (or in this pup window), then try again.")); }- let src = |c: &Value| format!("{} / {}", c["sourceBrowser"].as_str().unwrap_or("?"), c["sourceProfile"].as_str().unwrap_or("?"));+ if rows.is_empty() {+ // 2.0.736: say what came back, so "no session" is never a mystery (John's laptop, 2026-10-28: X is signed in+ // in his Chrome, yet nothing matched). Counts and field names only; never a cookie value.+ let all2: Vec<Value> = payload["cookies"].as_array().cloned().unwrap_or_default();+ let mut per: std::collections::BTreeMap<String, usize> = Default::default();+ for c in &all2 { *per.entry(format!("{} / {}", c["sourceBrowser"].as_str().or(c["source_browser"].as_str()).unwrap_or("?"), c["sourceProfile"].as_str().or(c["source_profile"].as_str()).unwrap_or("?"))).or_default() += 1; }+ let near: usize = all2.iter().filter(|c| { let h = host_of(c); h.contains("x.com") || h.contains("twitter") }).count();+ let keys: Vec<String> = all2.first().and_then(|c| c.as_object()).map(|o| o.keys().cloned().collect()).unwrap_or_default();+ pup_core::log(format!("[import] session for {site}: no match among {} cookie(s); per profile {:?}; near-matches {near}; stats {}; row keys {:?}", all2.len(), per, payload["stats"], keys));+ return pup_core::merge(err("no_session", format!("None of the cookies the user's browsers returned are for {site} ({} cookies came back in all).", all2.len()), Some("Check cookiesByProfile: if the profile the user is signed into is missing or shows 0, its cookies could not be read. Ask the user to sign in to that site once in this pup window instead.")),+ json!({ "cookiesReturned": all2.len(), "cookiesByProfile": per, "nearMatches": near, "decryptStats": payload["stats"], "cookieFields": keys }));+ }+ let src = |c: &Value| format!("{} / {}", c["sourceBrowser"].as_str().or(c["source_browser"].as_str()).unwrap_or("?"), c["sourceProfile"].as_str().or(c["source_profile"].as_str()).unwrap_or("?")); let mut sources: Vec<String> = rows.iter().map(|c| src(c)).collect(); sources.sort(); sources.dedup(); if let Some(want) = args["sourceProfile"].as_str() { rows.retain(|c| src(c).to_lowercase().contains(&want.to_lowercase())); } else if sources.len() > 1 {@@ -149,7 +160,7 @@ fn import_session(ctx: &Ctx, caller: &Caller, args: &Value, payload: &Value, sit let host = host_of(c); let name = c["name"].as_str().unwrap_or(""); let value = c["value"].as_str().unwrap_or(""); if name.is_empty() { continue; } let secure = c["secure"].as_bool().or(c["isSecure"].as_bool()).or(c["is_secure"].as_bool()).unwrap_or(true);- let http_only = c["httpOnly"].as_bool().or(c["isHttpOnly"].as_bool()).or(c["is_httponly"].as_bool()).unwrap_or(false);+ let http_only = c["httpOnly"].as_bool().or(c["http_only"].as_bool()).or(c["isHttpOnly"].as_bool()).or(c["is_httponly"].as_bool()).unwrap_or(false); let expires = c["expires"].as_f64().or(c["expiresUtc"].as_f64()).filter(|e| *e > now_s).unwrap_or(now_s + 30.0 * 86400.0); let mut p = json!({ "name": name, "value": value, "path": c["path"].as_str().unwrap_or("/"), "secure": secure, "httpOnly": http_only, "expires": expires }); if name.starts_with("__Host-") { p["url"] = json!(format!("https://{}/", host.trim_start_matches('.'))); }