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.727 (insiders): A pup window waiting on a slow page now explains itself instead of showing a blank PUPBIRTH tab: which AI thread opened it, why, where it is going, how long it has waited, and what a pup window is; after 10 s the tab and taskbar title read 'Opening <site> for <thread> (pup)' (John, 2026-10-19). AIs no longer give up with 'CDP is switched off on this box': a lane:"cdp" open on a box with that lane off now opens on the extension lane and says so, every hint that sent an AI to the CDP lane now names the extension-lane way, the skills say plainly that every pup window has CDP-level access, and the new pup_devtools {method, params} sends any DevTools command to a pup window through pup's extension. pup_maximize works on the extension lane (it fills the work area without raising the window; it used to fail). pup's background process queries now start PowerShell with no window, honour their timeout, and use one script file per call. Tested live on ConfRoomROG (birth page on a slow and an unreachable site, lane:cdp fallback, pup_devtools, maximize). Gates on arav-rog and ConfRoomROG: smoke all pass, foreground pass, jump list 11/14 (baseline), isolated-close replay kept every window; parity 48/56, because parity now actually reaches the window verbs it silently skipped since 2.0.652 (the gaps it finds are pup-bridge#131, none new in this build). Includes 2.0.725.
14 files changed
+211−41
rust/Cargo.lock+12−12rust/Cargo.toml+1−1rust/bridge/src/main.rs+5−1rust/host-win/src/lib.rs+24−5rust/verbs/assets/verb-catalog.json+24rust/verbs/src/extdrive.rs+1−1rust/verbs/src/extras.rs+2−2rust/verbs/src/input.rs+36−1rust/verbs/src/lib.rs+20−7rust/verbs/src/record.rs+1−1rust/verbs/src/vault.rs+1−1src/extension/birth.html+38−7src/extension/birth.js+42−1src/extension/worker.js+4−1rust/Cargo.lock+12−12@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-pup-bridge"-version = "2.0.725"+version = "2.0.727" dependencies = [ "pup-browser", "pup-cdp",@@ -781,7 +781,7 @@ dependencies = [ [[package]] name = "pup-browser"-version = "2.0.725"+version = "2.0.727" dependencies = [ "pup-core", "pup-host",@@ -793,7 +793,7 @@ dependencies = [ [[package]] name = "pup-cdp"-version = "2.0.725"+version = "2.0.727" dependencies = [ "serde_json", "thiserror",@@ -803,7 +803,7 @@ dependencies = [ [[package]] name = "pup-core"-version = "2.0.725"+version = "2.0.727" dependencies = [ "serde", "serde_json",@@ -813,7 +813,7 @@ dependencies = [ [[package]] name = "pup-dashboard"-version = "2.0.725"+version = "2.0.727" dependencies = [ "pup-cdp", "pup-core",@@ -826,7 +826,7 @@ dependencies = [ [[package]] name = "pup-host"-version = "2.0.725"+version = "2.0.727" dependencies = [ "serde", "serde_json",@@ -835,7 +835,7 @@ dependencies = [ [[package]] name = "pup-host-mac"-version = "2.0.725"+version = "2.0.727" dependencies = [ "core-foundation", "core-graphics",@@ -851,7 +851,7 @@ dependencies = [ [[package]] name = "pup-host-win"-version = "2.0.725"+version = "2.0.727" dependencies = [ "pup-core", "pup-host",@@ -861,7 +861,7 @@ dependencies = [ [[package]] name = "pup-identity"-version = "2.0.725"+version = "2.0.727" dependencies = [ "fontdue", "image",@@ -871,7 +871,7 @@ dependencies = [ [[package]] name = "pup-session"-version = "2.0.725"+version = "2.0.727" dependencies = [ "pup-core", "serde",@@ -881,7 +881,7 @@ dependencies = [ [[package]] name = "pup-vault"-version = "2.0.725"+version = "2.0.727" dependencies = [ "pup-core", "pup-host",@@ -892,7 +892,7 @@ dependencies = [ [[package]] name = "pup-verbs"-version = "2.0.725"+version = "2.0.727" 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.725"+version = "2.0.727" edition = "2021" license = "MIT"
rust/bridge/src/main.rs+5−1@@ -47,7 +47,11 @@ fn jumplist_call(verb: &str, b64: &str) -> i32 { // pup is not listening: the ab CLI spawns it and relays the verb (same fallback the .ps1 had). for name in ["adom-bridge.exe", "adom-bridge-cli.exe"] { let cli = lad.join("Adom Bridge").join(name);- if cli.is_file() { let _ = std::process::Command::new(cli).arg(verb).arg(args.to_string()).status(); return 0; }+ if cli.is_file() {+ let mut c = std::process::Command::new(cli); c.arg(verb).arg(args.to_string());+ #[cfg(windows)] { use std::os::windows::process::CommandExt; c.creation_flags(0x0800_0000); } // CREATE_NO_WINDOW: no console flash+ let _ = c.status(); return 0;+ } } 1 }
rust/host-win/src/lib.rs+24−5@@ -297,15 +297,34 @@ mod imp { } // Node: killBrowserProcess (taskkill /F) fn run_shell_script(&self, script: &str, timeout: std::time::Duration) -> String {- use std::io::Write;- let dir = std::env::temp_dir(); let f = dir.join(format!("pup-ps-{}.ps1", std::process::id()));+ // 2.0.726 (John, 2026-10-19, after a reboot: "a bunch of windows terminal cmd line windows pop open for 50ms").+ // Three fixes: CREATE_NO_WINDOW, so a PowerShell started by a pup with no console never flashes one (boot+ // recovery runs many of these); one script file PER CALL (it was one per process, so two concurrent queries+ // overwrote each other's script); and the timeout is honoured (it was ignored, so a hung query blocked forever).+ use std::io::Read;+ use std::os::windows::process::CommandExt;+ static SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);+ let n = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);+ let dir = std::env::temp_dir(); let f = dir.join(format!("pup-ps-{}-{n}.ps1", std::process::id())); if std::fs::write(&f, script).is_err() { return String::new(); } let mut cmd = std::process::Command::new("powershell.exe");- cmd.args(["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File"]).arg(&f);+ cmd.args(["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-ExecutionPolicy", "Bypass", "-File"]).arg(&f);+ cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW cmd.stdin(std::process::Stdio::null()).stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::null());- let out = match cmd.spawn() { Ok(c) => { let _ = timeout; let o = c.wait_with_output(); let _ = std::io::stdout().flush(); o } Err(_) => { let _ = std::fs::remove_file(&f); return String::new(); } };+ let mut child = match cmd.spawn() { Ok(c) => c, Err(_) => { let _ = std::fs::remove_file(&f); return String::new(); } };+ // read stdout on a thread so a full pipe never deadlocks the wait below+ let mut so = child.stdout.take();+ let reader = std::thread::spawn(move || { let mut s = String::new(); if let Some(o) = so.as_mut() { let _ = o.read_to_string(&mut s); } s });+ let deadline = std::time::Instant::now() + timeout.max(std::time::Duration::from_secs(1));+ loop {+ match child.try_wait() {+ Ok(Some(_)) => break,+ Ok(None) if std::time::Instant::now() < deadline => std::thread::sleep(std::time::Duration::from_millis(50)),+ _ => { let _ = child.kill(); let _ = child.wait(); break; }+ }+ } let _ = std::fs::remove_file(&f);- out.ok().map(|o| String::from_utf8_lossy(&o.stdout).to_string()).unwrap_or_default()+ reader.join().unwrap_or_default() } fn pids_for_image(&self, image: &str) -> Vec<u32> { use windows_sys::Win32::System::Diagnostics::ToolHelp::{CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, TH32CS_SNAPPROCESS};
rust/verbs/assets/verb-catalog.json+24@@ -883,6 +883,30 @@ "summary": "Low-level input (mouse/keyboard) at coords \u2014 beats some overlays.", "timeoutSeconds": 20 },+ {+ "name": "pup_devtools",+ "hint": "Send ONE DevTools protocol command (DOM, Runtime, Input, Page, Network, Emulation...) to a pup window, on any lane. On the default extension lane it goes through pup's own extension, so every pup window has CDP-level access with no debug port. Never give up or switch machines because the CDP lane is off: use this.",+ "input": {+ "method": "str, e.g. \"DOM.getDocument\"",+ "params": "optional object",+ "tabId": "optional str",+ "timeoutSec": "optional int 1-40 (default 20)",+ "sessionId": "optional str \u2014 OMIT IT: zero-id addressing resolves your --ai-thread name to your own window. Pass a window label only when one thread runs several windows (`window` is the new name for this param; sessionId is the legacy alias)"+ },+ "output": {+ "success": "bool",+ "result": "the command's result object",+ "lane": "\"ext\"|\"cdp\""+ },+ "example": {+ "method": "Runtime.evaluate",+ "params": {+ "expression": "document.title",+ "returnByValue": true+ }+ },+ "longRunning": false+ }, { "example": { "html": "<h1>Caf\u00e9</h1>",
rust/verbs/src/extdrive.rs+1−1@@ -427,7 +427,7 @@ fn open_inner(ctx: &Ctx, _caller: &Caller, args: &Value) -> Value { Ok(r) => { if let Some(t) = r["tabs"].as_array().and_then(|a| a.iter().find(|t| t["url"].as_str().map(|u| u.starts_with("http")).unwrap_or(false))) { pin(t); } pup_core::merge(ok(json!({ "opened": true, "url": url, "tabs": r["tabs"] })),- json!({ "_hint": "Extension-driven window: NO CDP debug port, so vendor edges that wall automation (st.com, mouser, arrow) load here. Drive it with pup_navigate / pup_eval. CDP-only features (focus emulation, UA override, precise trusted input, annotate) do NOT apply to this window." }))+ json!({ "_hint": "Extension-driven window: NO CDP debug port, so vendor edges that wall automation (st.com, mouser, arrow) load here. Drive it with pup_navigate / pup_eval. Trusted input (pup_click, pup_type, pup_input_dispatch), element and full-page screenshots, and annotate all work here, and pup_devtools {method, params} sends any DevTools command to this window." })) } Err(e) => e, };
rust/verbs/src/extras.rs+2−2@@ -61,7 +61,7 @@ pub fn screenshot_full_res(ctx: &Ctx, caller: &Caller, args: &Value) -> Value { pub fn download(ctx: &Ctx, caller: &Caller, args: &Value) -> Value { if let Ok(s0) = crate::resolve(ctx, caller, args) { if crate::transport::is_ext(&s0) {- let Some(url) = args["url"].as_str().filter(|u| !u.is_empty()) else { return err("url_required", "pup_download on the extension lane needs {url}", Some("Selector-driven downloads need CDP: reopen with lane:\"cdp\".")) };+ let Some(url) = args["url"].as_str().filter(|u| !u.is_empty()) else { return err("url_required", "pup_download on the extension lane needs {url}", Some("Read the link's address first, for example pup_eval {expr:\"document.querySelector('a.download').href\"}, then pup_download {url}. pup windows drive the DevTools protocol through pup's own extension: pup_devtools {method, params} sends any command (DOM, Runtime, Input, Page, Network...) to the window with no debug port. Do not stop, and do not move to another machine, because the CDP lane is off.")) }; let lane = match crate::transport::lane_for(ctx, &s0) { Ok(l) => l, Err(e) => return e }; let wait = args["waitMs"].as_u64().unwrap_or(30000); return match crate::transport::download(&lane, url, args["saveAs"].as_str(), wait) {@@ -71,7 +71,7 @@ pub fn download(ctx: &Ctx, caller: &Caller, args: &Value) -> Value { } } if crate::resolve(ctx, caller, args).map(|s| crate::transport::is_ext(&s)).unwrap_or(false) {- return err("lane_needs_cdp", "this verb waits on DevTools events, which the extension lane does not stream", Some("Reopen with pup_open_window {lane:\"cdp\"} for recording or selector-driven downloads; everything else works here."));+ return err("ext_lane_use_url", "on this window pass the download's address: pup_download {url}", Some("Get the address with pup_eval (a link's href), then pup_download {url}; recording uses pup_record_start. pup windows drive the DevTools protocol through pup's own extension: pup_devtools {method, params} sends any command (DOM, Runtime, Input, Page, Network...) to the window with no debug port. Do not stop, and do not move to another machine, because the CDP lane is off.")); } let (s, t, b) = match resolve_tab(ctx, caller, args) { Ok(x) => x, Err(e) => return e }; let (sel, url) = (args["selector"].as_str(), args["url"].as_str());
rust/verbs/src/input.rs+36−1@@ -358,6 +358,27 @@ pub fn dialog(ctx: &Ctx, caller: &Caller, args: &Value) -> Value { ok(json!({ "handled": action, "dialog": d, "sessionId": s.id, "window": s.id })) } +/// pup_devtools {method, params?, timeoutSec?}: one DevTools protocol command on the window's active tab (or tabId),+/// on either lane. On the extension lane it goes through pup's extension (chrome.debugger), so a pup window has the+/// whole protocol with no debug port (2.0.726, John: AIs gave up with "CDP is switched off on this box").+pub fn devtools(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {+ let Some(method) = args.get("method").and_then(|m| m.as_str()).filter(|m| m.contains('.')) else {+ return err("method_required", "pup_devtools needs {method:\"Domain.command\"}", Some("Example: pup_devtools {method:\"DOM.getDocument\", params:{depth:1}}. The protocol reference: https://chromedevtools.github.io/devtools-protocol/"));+ };+ // The shared browser and its other windows are not this window's to end.+ const BLOCKED: [&str; 5] = ["Browser.close", "Browser.crash", "Browser.crashGpuProcess", "Target.closeTarget", "Target.disposeBrowserContext"];+ if BLOCKED.contains(&method) { return err("devtools_method_blocked", format!("{method} would affect other pup windows in the shared browser"), Some("Close your own window with pup_close_window.")); }+ let (s, t, _b) = match resolve_tab(ctx, caller, args) { Ok(x) => x, Err(e) => return e };+ let lane = match crate::transport::lane_for(ctx, &s) { Ok(l) => l, Err(e) => return e };+ let params = args.get("params").cloned().filter(|p| p.is_object()).unwrap_or_else(|| json!({}));+ let secs = args.get("timeoutSec").and_then(|v| v.as_u64()).unwrap_or(20).clamp(1, 40);+ match crate::transport::cdp_raw(&lane, &t, method, params, secs) {+ Ok(result) => ok(json!({ "success": true, "sessionId": s.id, "window": s.id, "tabId": t.tab_id, "method": method, "lane": if crate::transport::is_ext(&s) { "ext" } else { "cdp" }, "result": result,+ "_hint": "Sent through pup's extension; no debug port is open. Events are not streamed back: poll with commands (for example Runtime.evaluate) instead of waiting on events." })),+ Err(e) => pup_core::merge(e, json!({ "method": method, "_hint": "The browser answered this DevTools command with an error; read it and fix the params. Some domains need an enable first (DOM.enable, Page.enable)." })),+ }+}+ pub fn input_dispatch(ctx: &Ctx, caller: &Caller, args: &Value) -> Value { let (s, t, b) = match resolve_tab(ctx, caller, args) { Ok(x) => x, Err(e) => return e }; let kind = args.get("type").and_then(|x| x.as_str()).unwrap_or("");@@ -507,8 +528,22 @@ fn sha256_hex(data: &[u8]) -> String { } pub fn maximize(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {- let (s, t, b) = match resolve_tab(ctx, caller, args) { Ok(x) => x, Err(e) => return e }; let state = args.get("state").and_then(|x| x.as_str()).unwrap_or("maximized");+ // 2.0.726: on the extension lane (every window by default) this answered window_failed, unseen for weeks because+ // parity's window was refused before it reached here. The browser's own maximize RAISES the window over the user's+ // work (pup-bridge-dev geometry rule), so "maximized" and "normal" fill the work area in place, as every pup window+ // already is, without touching the z-order.+ if let Ok(s0) = crate::resolve(ctx, caller, args) {+ if crate::transport::is_ext(&s0) {+ if state == "maximized" || state == "normal" {+ crate::extdrive::fit_now();+ return ok(json!({ "success": true, "sessionId": s0.id, "window": s0.id, "aiThread": s0.owner, "state": state, "fitted": true,+ "_hint": "Sized to fill the screen's work area without raising it (a true maximize would pull the window over the user's work). The window stays behind their work." }));+ }+ return err("state_unsupported_ext", format!("state \"{state}\" is not done on pup windows"), Some("Minimizing or full-screening would change what the user sees. To move a window behind their work use pup_lower_os_window; to show it, pup_raise_os_window {foregroundReason}."));+ }+ }+ let (s, t, b) = match resolve_tab(ctx, caller, args) { Ok(x) => x, Err(e) => return e }; let (wid, _) = match b.cdp.window_for_target(&t.target_id) { Ok(w) => w, Err(e) => return err("window_failed", e.to_string(), None) }; if let Err(e) = b.cdp.set_window_bounds(wid, json!({ "windowState": "normal" })) { return err("window_failed", e.to_string(), None); } if state != "normal" { if let Err(e) = b.cdp.set_window_bounds(wid, json!({ "windowState": state })) { return err("window_failed", e.to_string(), None); } }
rust/verbs/src/lib.rs+20−7@@ -599,7 +599,7 @@ pub fn install_event_hook(ctx: Arc<Ctx>) { /// The verbs this build serves natively. pub const PHASE1_VERBS: &[&str] = &["pup_describe", "pup_readiness", "pup_prewarm", "pup_status", "pup_list_windows", "pup_my_windows", "pup_list_sessions", "pup_open_window", "pup_navigate", "pup_eval", "pup_evaluate", "pup_screenshot", "pup_close_window", "pup_close", "pup_close_all_windows", "pup_window_info",- "pup_open_tab", "pup_list_tabs", "pup_switch_tab", "pup_close_tab", "pup_switch_window", "pup_click", "pup_select", "pup_hover", "pup_type", "pup_press_key", "pup_scroll", "pup_fill", "pup_wait", "pup_reload", "pup_back", "pup_forward", "pup_set_viewport", "pup_errors", "pup_dialog", "pup_input_dispatch", "pup_render_html", "pup_fetch_url", "pup_maximize",+ "pup_open_tab", "pup_list_tabs", "pup_switch_tab", "pup_close_tab", "pup_switch_window", "pup_click", "pup_select", "pup_hover", "pup_type", "pup_press_key", "pup_scroll", "pup_fill", "pup_wait", "pup_reload", "pup_back", "pup_forward", "pup_set_viewport", "pup_errors", "pup_dialog", "pup_input_dispatch", "pup_devtools", "pup_render_html", "pup_fetch_url", "pup_maximize", "pup_raise_os_window", "pup_focus_window", "pup_lower_os_window", "pup_alert_window", "pup_progress", "pup_configure", "pup_use", "pup_clear_cookies", "pup_clear_site_data", "pup_quit_idle_browsers", "pup_rescan", "pup_restart_browser", "pup_set_identity", "pup_audit_icons", "pup_dashboard", "pup_credentials", "credential_list", "credential_set", "credential_delete", "pup_login", "pup_google_accounts", "pup_google_signin", "pup_credential_reveal", "pup_card_set", "pup_cards", "pup_card_delete", "pup_fill_card", "pup_wiki_set_view", "pup_annotate", "pup_floaty_click", "pup_record_start", "pup_record_stop", "pup_record_status", "pup_record_list", "pup_recorder_open", "pup_recorder_close", "pup_screen_record_start", "pup_screen_record_stop", "pup_screen_record_status", "pup_screen_record_list", "pup_highfps_extend",@@ -644,6 +644,7 @@ pub fn dispatch(ctx: &Ctx, caller: &Caller, verb: &str, args: &Value) -> Value { "pup_errors" => input::errors(ctx, caller, args), "pup_dialog" => input::dialog(ctx, caller, args), "pup_input_dispatch" => input::input_dispatch(ctx, caller, args),+ "pup_devtools" => input::devtools(ctx, caller, args), "pup_render_html" => input::render_html(ctx, caller, args), "pup_fetch_url" => input::fetch_url(ctx, caller, args), "pup_maximize" => input::maximize(ctx, caller, args),@@ -1342,8 +1343,14 @@ fn open_window(ctx: &Ctx, caller: &Caller, args: &Value) -> Value { // 2.0.652 (John, 2026-09-19: "i want it almost NEVER used cuz then you may use it as a crutch instead of fixing // ext lane to solve all use cases"). lane:"cdp" is refused unless the user turned the CDP lane on for this box. if args.get("lane").and_then(|l| l.as_str()) == Some("cdp") && ctx.settings.get("cdpLane", "off") != "on" {- pup_core::log(format!("[lane] refused: thread \"{}\" asked for lane:\"cdp\" and cdpLane is off (cdp_lane_disabled)", caller.thread));- return err("cdp_lane_disabled", "the CDP lane is off on this box; every pup window runs on the extension lane", Some("Use the extension lane (omit lane). Trusted input, HTTP auth, fetch, screenshots and recording all work there. If a feature genuinely needs CDP, file it on adom/pup-bridge as an extension-lane gap; the user can enable the lane for one box with pup_configure {cdpLane:\"on\"}."));+ // 2.0.726 (John, 2026-10-19: AIs answered "CDP is switched off on this box, so pup can't open the page here at+ // all" and went to another machine). Never refuse an open over the lane: open it on the extension lane, which+ // has the DevTools protocol through pup's extension, and say so.+ pup_core::log(format!("[lane] thread \"{}\" asked for lane:\"cdp\"; cdpLane is off, so the window opens on the extension lane instead", caller.thread));+ let mut a2 = args.clone();+ if let Some(o) = a2.as_object_mut() { o.remove("lane"); }+ let r = open_window(ctx, caller, &a2);+ return pup_core::merge(r, json!({ "lane": "ext", "laneRequested": "cdp", "_laneNote": "You asked for lane:\"cdp\". The CDP lane is off on this box, so pup opened this window on its extension lane, which is the one every site loads on. Drive it as usual: pup_click, pup_type, pup_eval, pup_screenshot (element and full-page too), pup_input_dispatch for trusted input. pup windows drive the DevTools protocol through pup's own extension: pup_devtools {method, params} sends any command (DOM, Runtime, Input, Page, Network...) to the window with no debug port. Do not stop, and do not move to another machine, because the CDP lane is off." })); } if lane != "cdp" { // 2.0.659 (pup-bridge#112/#113): profile and isolated run on the EXTENSION lane now (a lane per profile)@@ -1353,7 +1360,7 @@ fn open_window(ctx: &Ctx, caller: &Caller, args: &Value) -> Value { if let Some(why) = needs_cdp { pup_core::log(format!("[lane] refused: thread \"{}\" passed a CDP-only option ({}) without lane:\"cdp\" (cdp_lane_required)", caller.thread, ["profile","isolated","highFps","webSecurity"].iter().filter(|k| args.get(**k).is_some()).cloned().collect::<Vec<_>>().join(","))); return err("cdp_lane_required", format!("This open needs the CDP lane because of {why}, and the CDP lane opens only when asked for."),- Some("The extension lane is pup's default: it is the lane every vendor site loads on, with no --remote-debugging-port for edges to detect. Pass lane:\"cdp\" ONLY if you specifically need what CDP alone can do (iframes, trusted input events, element/full-res screenshots, a separate profile, highFps, webSecurity:false); read the pup-lanes skill first. Note that lane:\"cdp\" on adom-you takes the profile away from the extension lane and closes its windows."));+ Some("Drop that option and open normally: the extension lane is pup's default and every site loads on it. Without highFps a window covered by other windows may render slowly until it is visible; screenshots still work. Instead of webSecurity:false, fetch cross-origin data with pup_fetch_url {url}. pup windows drive the DevTools protocol through pup's own extension: pup_devtools {method, params} sends any command (DOM, Runtime, Input, Page, Network...) to the window with no debug port. Do not stop, and do not move to another machine, because the CDP lane is off.")); } let owner = args.get("owner").and_then(|o| o.as_str()).map(|o| o.to_string()).or_else(|| if caller.thread.is_empty() { None } else { Some(caller.thread.clone()) }); let mut a2 = args.clone(); a2["_mintedProfile"] = json!(profile);@@ -1649,7 +1656,7 @@ fn eval(ctx: &Ctx, caller: &Caller, args: &Value) -> Value { let Some(expr) = expr else { return err("eval_expr_missing", "Missing required parameter \"expr\" (aliases: expression, script, js, code)", Some("pup_eval {expr:\"document.title\"}")) }; let s = match resolve(ctx, caller, args) { Ok(s) => s, Err(e) => return e }; if transport::is_ext(&s) {- if args.get("frame").and_then(|f| f.as_str()).filter(|f| !f.is_empty()).is_some() { return err("frame_unsupported_ext", "frame targeting needs the CDP lane", Some("Open the window with lane:\"cdp\" for iframe evaluation.")); }+ if args.get("frame").and_then(|f| f.as_str()).filter(|f| !f.is_empty()).is_some() { return err("frame_unsupported_ext", "pup_eval {frame} is not wired on the extension lane yet", Some("Reach the iframe with pup_devtools: {method:\"Page.getFrameTree\"} lists the frames, {method:\"Page.createIsolatedWorld\", params:{frameId}} gives an executionContextId, then {method:\"Runtime.evaluate\", params:{expression, contextId, returnByValue:true}}. A same-origin iframe is also reachable from pup_eval through document.querySelector('iframe').contentDocument. pup windows drive the DevTools protocol through pup's own extension: pup_devtools {method, params} sends any command (DOM, Runtime, Input, Page, Network...) to the window with no debug port. Do not stop, and do not move to another machine, because the CDP lane is off.")); } let lane = match transport::lane_for(ctx, &s) { Ok(l) => l, Err(e) => return e }; let tab = match args.get("tabId").and_then(|t| t.as_str()).and_then(|id| s.tab_by_id(id)).or(s.active()) { Some(t) => t.clone(), None => return err("no_tab", "the window has no tabs", None) }; return match transport::evaluate(&lane, &tab, &expr) { Ok(v) => ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": tab.tab_id, "result": v })), Err(e) => e };@@ -1827,6 +1834,12 @@ fn spawn_zwatch(ctx: &Ctx, sid: &str, before: std::collections::HashSet<i64>) { }).ok(); } pub fn open_window_ext(ctx: &Ctx, caller: &Caller, args: &Value, sid: &str, owner: Option<&str>, url: &str) -> Value {+ // 2.0.726 (John, 2026-10-19: "this interstitial window then explains the ai thread that opened this window, what+ // it's reason was, and then even a description of what pup is"). The birth page shows who opened the window, why,+ // and where it is going, for as long as a slow page keeps it there.+ let birth_reason = [caller.reason.as_str(), args["reason"].as_str().unwrap_or(""), args["foregroundReason"].as_str().unwrap_or("")].iter().find(|r| !r.trim().is_empty()).copied().unwrap_or("").to_string();+ let birth_thread = owner.unwrap_or(caller.thread.as_str()).to_string();+ let birth_info = json!({ "thread": birth_thread, "reason": birth_reason, "target": url, "window": sid }); let t_verb = Instant::now(); // 2.0.659 (pup-bridge#113): the lane is per profile. isolated:true minted an iso-<id> profile (passed as // _mintedProfile); a named profile is honoured too. Everything below talks to THAT profile's browser and queue.@@ -1890,7 +1903,7 @@ pub fn open_window_ext(ctx: &Ctx, caller: &Caller, args: &Value, sid: &str, owne extdrive::apply_consumer_brands(ctx); // 2.0.617: the lane launches with --no-startup-window, so there is no tab to adopt. Create the first // window the same way as every later one: through the extension, minimized, shown at the bottom.- match extdrive::run(json!({ "action": "open", "url": "about:blank" }), Duration::from_secs(25)) {+ match extdrive::run(json!({ "action": "open", "url": "about:blank", "birth": birth_info.clone() }), Duration::from_secs(25)) { Ok(r) => { birth_move_ms = r["birthMoveMs"].as_i64(); birth_token = r["token"].as_str().unwrap_or("").to_string(); let t = r.clone();@@ -1902,7 +1915,7 @@ pub fn open_window_ext(ctx: &Ctx, caller: &Caller, args: &Value, sid: &str, owne Err(e) => return e, } } else {- match extdrive::run(json!({ "action": "open", "url": url }), Duration::from_secs(20)) {+ match extdrive::run(json!({ "action": "open", "url": url, "birth": birth_info.clone() }), Duration::from_secs(20)) { Ok(r) => { birth_move_ms = r["birthMoveMs"].as_i64(); birth_token = r["token"].as_str().unwrap_or("").to_string(); (r["tabId"].as_i64().unwrap_or(0), r["windowId"].as_i64(), url.to_string(), String::new()) } Err(e) => { // 2.0.577: a lane whose worker polled within the last few seconds is BUSY, not dead: relaunching it
rust/verbs/src/record.rs+1−1@@ -84,7 +84,7 @@ pub fn start(ctx: &Ctx, caller: &Caller, args: &Value) -> Value { } } if crate::transport::is_ext(&s0) {- return err("recorder_unavailable", "this window's recording needs ab's native recorder (desktop_record_window_start), which did not answer", Some("Update Adom Bridge to 2.1.35+ (the native recorder). The DevTools screencast fallback exists only on the CDP lane: pup_open_window {lane:\"cdp\"}."));+ return err("recorder_unavailable", "this window's recording needs ab's native recorder (desktop_record_window_start), which did not answer", Some("Update Adom Bridge to 2.1.35+ (the native recorder). Until then, take pup_screenshot frames, or ask the user to update Adom Bridge.")); } let (s, t, b) = match resolve_tab(ctx, caller, args) { Ok(x) => x, Err(e) => return e }; if ctx.tab_recs.lock().unwrap().contains_key(&s.id) { return err("recording_in_progress", format!("window \"{}\" is already recording", s.id), Some("pup_record_stop first.")); }
rust/verbs/src/vault.rs+1−1@@ -346,7 +346,7 @@ fn login_ext(ctx: &Ctx, caller: &Caller, args: &Value, s: Session) -> Value { return pup_core::merge(err("submit_reason_required", "CONFIRM THE DOMAIN BEFORE SENDING CREDENTIALS", Some(&format!("You asked to submit a stored login: the page is on {live_host}. Re-call with submitReason:\"<why {live_host} is the intended site>\"."))), json!({ "liveHost": live_host, "url": cur_url })); } if args["realKeystrokes"].as_bool() == Some(true) || args["keystrokes"].as_bool() == Some(true) {- return err("lane_needs_cdp", "realKeystrokes needs CDP trusted input", Some("Reopen with pup_open_window {lane:\"cdp\"} for a React-masked login form."));+ return err("realkeystrokes_unwired", "pup_login's realKeystrokes is not wired on the extension lane yet", Some("For a form that ignores filled values, click the field with pup_click and type with pup_type: both are trusted input on this window. pup windows drive the DevTools protocol through pup's own extension: pup_devtools {method, params} sends any command (DOM, Runtime, Input, Page, Network...) to the window with no debug port. Do not stop, and do not move to another machine, because the CDP lane is off.")); } if submit { pup_core::log(format!("[cred] pup_login submit to {live_host} by \"{}\" (ext lane) — reason: {}", caller.thread, args["submitReason"].as_str().unwrap_or("").chars().take(120).collect::<String>())); } let located = match crate::transport::evaluate(&lane, &t, pup_vault::locate_login_js()) { Ok(v) => v, Err(e) => return e };
src/extension/birth.html+38−7@@ -1,9 +1,40 @@ <!doctype html>-<!-- pup birth page (2.0.630). Every extension-lane window is CREATED on this page, minimized, with a unique- token in the query string; the page's <title> is that token, so the OS window title is- "PUPBIRTH-<token> - Google Chrome for Testing" from the first instant, and pup binds the window by that- title alone: deterministic, no set-diff, no geometry (pup-no-wiggle, pup-window-targeting R4). The real- URL is navigated to only after the window is bound and parked at the bottom of the z-order. -->+<!-- pup birth page (2.0.630). Every extension-lane window is CREATED on this page with a unique token in the query+ string; the page's <title> is that token, so the OS window title is "PUPBIRTH-<token> - Google Chrome for Testing"+ from the first instant, and pup binds the window by that title alone (pup-no-wiggle, pup-window-targeting R4).+ The real URL is navigated to only after the window is bound and parked at the bottom of the z-order.+ 2.0.726: a slow page keeps this tab on screen, so it explains itself: which AI thread opened the window, why,+ where it is going, how long it has waited, and what pup is (John, 2026-10-19). --> <html><head><meta charset="utf-8"><title>PUPBIRTH</title>-<style>html,body{margin:0;background:#161b22;color:#8b949e;font:14px system-ui}</style>-<script src="birth.js"></script></head><body></body></html>+<style>+:root{--bg:#161b22;--card:#1c2230;--ink:#e6edf3;--dim:#8b949e;--line:#30363d;--teal:#00b8b1;--amber:#d29922}+html,body{margin:0;height:100%;background:var(--bg);color:var(--ink);font:15px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}+body{display:flex;align-items:center;justify-content:center;padding:24px;box-sizing:border-box}+.card{max-width:620px;width:100%;background:var(--card);border:1px solid var(--line);border-radius:12px;padding:28px 30px}+.brand{display:flex;align-items:center;gap:8px;color:var(--teal);font-weight:600;font-size:13px;letter-spacing:.04em;text-transform:uppercase}+.dot{width:8px;height:8px;border-radius:50%;background:var(--teal);animation:pulse 1.6s ease-in-out infinite}+@keyframes pulse{50%{opacity:.25}}+@media (prefers-reduced-motion:reduce){.dot{animation:none}}+h1{font-size:22px;margin:14px 0 4px;font-weight:600;word-break:break-word}+.row{margin-top:16px}+.lbl{color:var(--dim);font-size:12px;text-transform:uppercase;letter-spacing:.05em}+.val{margin-top:2px;word-break:break-all}+.val a{color:var(--teal)}+.status{margin-top:20px;padding:10px 12px;border-radius:8px;background:#0d1117;border:1px solid var(--line);color:var(--dim)}+.status.slow{color:var(--amber);border-color:#5a4a1a}+.about{margin-top:22px;padding-top:16px;border-top:1px solid var(--line);color:var(--dim);font-size:13.5px}+.hidden{display:none}+</style>+<script src="birth.js"></script></head>+<body><div class="card hidden" id="card">+ <div class="brand"><span class="dot"></span>pup window</div>+ <h1 id="head"></h1>+ <div class="row"><div class="lbl">Opened by</div><div class="val" id="thread"></div></div>+ <div class="row"><div class="lbl">Why</div><div class="val" id="reason"></div></div>+ <div class="row"><div class="lbl">Going to</div><div class="val" id="target"></div></div>+ <div class="status" id="status"></div>+ <div class="about">This is a pup window: a browser your AI drives in the background, signed in as you, so it can+ check pages and do web work without taking over your screen. Every new pup window starts on this page for a+ moment while pup tucks it behind your work, and you normally never see it. It is still showing because the+ page it is opening has not answered yet. Nothing is wrong with your computer, and you can keep working.</div>+</div></body></html>
src/extension/birth.js+42−1@@ -1,5 +1,46 @@ // pup birth page script (2.0.632). Lives in its own file because an extension page's default content // security policy refuses inline scripts: an inline version left every title at plain "PUPBIRTH", so pup's // token lookup timed out on every open and the window showed for the 3 s it waited (measured, 2.0.631).-const t = new URLSearchParams(location.search).get('token') || '';+// 2.0.726: fills in who opened the window, why and where it is going, and counts the wait (John, 2026-10-19).+const q = new URLSearchParams(location.search);+const t = q.get('token') || ''; if (t) document.title = 'PUPBIRTH-' + t;++const thread = (q.get('t') || '').trim();+const reason = (q.get('r') || '').trim();+const target = (q.get('u') || '').trim();+const born = Number(q.get('at')) || Date.now();+const real = /^https?:\/\//i.test(target);+let host = '';+try { if (real) host = new URL(target).host; } catch (e) { host = ''; }++function fill() {+ const $ = (id) => document.getElementById(id);+ $('head').textContent = host ? 'Opening ' + host : 'Opening a page';+ $('thread').textContent = thread ? 'the AI thread "' + thread + '"' : 'an AI thread that did not give its name';+ $('reason').textContent = reason || 'The thread did not say why when it opened this window.';+ const tv = $('target');+ if (real) {+ const a = document.createElement('a');+ a.href = target; a.textContent = target; // clicking it goes there now, in this same tab+ tv.appendChild(a);+ } else {+ tv.textContent = 'pup has not been told the address yet; the thread will send it next.';+ }+ const st = $('status');+ const tick = () => {+ const s = Math.max(0, Math.round((Date.now() - born) / 1000));+ const slow = s >= 20;+ st.className = 'status' + (slow ? ' slow' : '');+ st.textContent = real+ ? (slow ? 'Still waiting for ' + host + ' to answer (' + s + ' s). The site is slow right now; pup keeps waiting and tries again on its own.'+ : 'Waiting for ' + host + ' to answer (' + s + ' s).')+ : 'Waiting for the thread to say where to go (' + s + ' s).';+ };+ tick(); setInterval(tick, 1000);+ $('card').classList.remove('hidden');+ // The PUPBIRTH-<token> title is how pup finds this window, and it binds within 3 s of the create. Once that is+ // long done, give the tab and the taskbar a title a person can read.+ setTimeout(() => { document.title = (host ? 'Opening ' + host : 'Opening a page') + (thread ? ' for ' + thread : '') + ' (pup)'; }, 10000);+}+if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fill); else fill();
src/extension/worker.js+4−1@@ -308,7 +308,10 @@ async function pupExtPollOnce() { // minimize (z-watch, 2.0.638) and surfaced again for ~20 ms when pup restored it. pup now parks the window // while it is still invisible (the birth watcher catches it ~150 ms before the first show), so there is no // reason to minimize and no restore to surface.- const w = await chrome.windows.create({ url: chrome.runtime.getURL('birth.html') + '?token=' + token, focused: false, state: 'normal' });+ // 2.0.726: who opened it, why, and where it is going, so a slow page leaves the user an explanation, not a blank tab+ const bi = cmd.birth || {};+ const q = new URLSearchParams({ token, t: String(bi.thread || ''), r: String(bi.reason || ''), u: String(bi.target || cmd.url || ''), w: String(bi.window || ''), at: String(Date.now()) });+ const w = await chrome.windows.create({ url: chrome.runtime.getURL('birth.html') + '?' + q.toString(), focused: false, state: 'normal' }); const birthMoveMs = 0; const tab = w.tabs && w.tabs[0]; if (tab) await pupExtWaitComplete(tab.id, 8000);