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
Update 10 file(s)
10 files changed
+229−16
CHANGELOG.md+46@@ -1,5 +1,51 @@ # Changelog — Puppeteer Bridge +## 2.0.424 — 2026-09-06 — jump-list tasks actually work again (human callers bypass ownership); thin LTO++John: "have you ralph tested your jump list items? i just tried annotate and it doesn't work." Measured:+the exe call reached pup in 24 ms and pup answered `not_your_window`. The Rust port had dropped Node's+USER_CALLERS rule (a jump-list, toolbar or dashboard caller IS the human, who owns every window), so every+window-targeting task (Annotate this page, Switch to public / logged-in view, the taskbar relaunch) has+been refused since the Rust build shipped; only Dashboard and Close-all, which take no window, worked.+Fixed in the ownership guard. New gate: `tools/jumplist-ralph.py --target <box>` fires every task through+the REAL path (ab's taskrun shim with the link's exact target and arguments) and asserts the effect in the+window; it is part of the ship checks from now on.++Build: the release profile moves from fat LTO to thin LTO with 16 codegen units. Measured on the build+container: a one-crate edit rebuilds in 28 s instead of 36.5 s; the exe grows from 4.9 MB to 5.7 MB+(2.5 MB to 2.9 MB zipped).++## 2.0.423 — 2026-09-06 — jump-list tasks call pup's own exe, no PowerShell++John saw the PowerShell icon on "Annotate this page" and "Close ALL Adom Pup windows" in the jump list+and asked why a PowerShell command runs from a taskbar menu. It was a Node-era relic: Node had no exe to+call back into, so it wrote pup-jumplist-call.ps1 and every task launched powershell.exe (through ab's+windowless taskrun shim) to POST the verb to pup's loopback. The exe does that itself now:+`adom-bridge-puppeteer.exe --jumplist <verb> <base64 args>` reads the port file, POSTs to the running+bridge as caller user-taskbar-menu and exits (about 20 ms instead of a PowerShell start); if no bridge+answers it hands the verb to the ab CLI, which wakes pup, as before. The taskbar relaunch command uses+the same mode. The .ps1 is removed at boot. Every task now carries an explicit pup icon, so no task+ever shows its launcher's icon again.++## 2.0.422 — 2026-09-06 — no more orphan about:blank windows (the stock Chrome for Testing taskbar buttons)++Regression from the Rust port, found by John on AdomLapper: every profile launch birthed an extra+untracked about:blank window (the Rust launcher passed "about:blank" as Chrome's start URL, then+created pup's real window over CDP), so each Chrome carried a stock "Chrome for Testing" taskbar+button wearing the attention tint, next to the thread tile. Node had solved this in v1.8.51 with+`--no-startup-window` (no window at launch at all; pup creates the one it wants). Restored. On every+browser connect (fresh launch and reconnect after a bridge respawn) pup now also closes any top-level+about:blank page target no session owns, so boxes that still carry orphans from 2.0.407 to 2.0.421+are cleaned the next time pup attaches. Popups (openerId) and session-owned tabs are never touched.++## 2.0.421 — 2026-09-06 — the Start Menu anchor row in the Settings dialog (pup-bridge#101)++The `aumidShortcut` setting (2.0.418, mapped onto ab 2.1.49 `anchor:true` in 2.0.420) now has its row in+the dashboard Settings dialog, under Right-click menu: "Start Menu anchor (first-click jump list)", off+by default, with the measured trade-off spelled out (one collapsed "Adom Pup" Start Menu folder buys the+first-click jump list; hidden shortcuts do not anchor). The Jump lists row no longer calls the first-click+flash a Windows timing race: the cause is the missing anchor (ab#155).+ ## 2.0.420 — 2026-09-06 — runtime data under ADOM_BRIDGE_DATA_DIR with retention; jump-list anchor mapped to ab 2.1.49 pup-bridge#98 / adom-bridge#153: profiles, sessions, screenshots and recordings now resolve under the
CLAUDE.md+8@@ -93,6 +93,14 @@ page (release, repo push, pkg, page.json), refresh the LIVE page in a pup window (cache-bust with `?v=<ver>`), screenshot it, and surface the link — never just say "published." Reuse one window (`sessionId: cft` or `pup-wiki`) instead of spawning new ones; keep test windows open. +## Ship gates (run before `--insiders-from-candidate`)++- `tools/parity.py --via-ab` and `tools/ab-smoke.sh <box>` on the candidate.+- `tools/jumplist-ralph.py --target <box>`: every jump-list task fired THROUGH ab's taskrun shim with the+ link's exact target and arguments, effect asserted in the window. Added 2026-09-06 after the Rust port+ shipped 17 versions with every window-targeting task refused (`not_your_window`) while a by-hand exe+ call of one verb had "passed". A hand call of the verb is not a test of the jump list.+ ## Hard rules - **The `.zip` is a RELEASE asset, never a git file.** `*.zip` is gitignored and `repo push --files X.zip`
docs/PLAN-rust-port.md+13−1@@ -102,7 +102,19 @@ John's words. Written 2026-09-05 against pup 2.0.396; John approved the plan the fallback to the legacy path when the variable is absent, which is the case until ab ships it) plus retention in the daily janitor (screenshots 14 d / 1 GB, recordings 30 d / 3 GB); both unit-tested natively in pup_core. ab#155 closed by ab: only a visible Start Menu shortcut anchors the jump list,- opt-in `anchor:true` in ab 2.1.49; `aumidShortcut` maps onto it (off by default, John's call). Jump lists: the two-right-clicks flash is ab#155+ opt-in `anchor:true` in ab 2.1.49; `aumidShortcut` maps onto it (off by default, John's call).+ 2.0.421: the `aumidShortcut` row in the dashboard Settings dialog (pup-bridge#101 answered and closed).+ 2.0.422: the orphan about:blank window regression (the Rust launcher passed "about:blank" instead of+ Node's `--no-startup-window`, so every profile carried an untracked stock-icon window); fixed at launch+ and swept on every connect. 2.0.423: jump-list tasks and the taskbar relaunch call the exe's own+ `--jumplist` mode instead of powershell.exe + a .ps1 (John: "why is a PowerShell command running from a+ jump list"); explicit icons on every task.+ 2.0.424: John clicked Annotate from the jump list and nothing happened. Measured: the call reached pup+ (HTTP 200, 24 ms) and pup refused `not_your_window`; the Rust port had dropped Node's USER_CALLERS rule,+ so every window-targeting task had been refused since 2.0.407. Fixed (USER_CALLERS bypass ownership) and+ gated: `tools/jumplist-ralph.py --target <box>` fires every task through ab's taskrun shim with the link's+ exact target and arguments and asserts the effect (6/6 on arav-rog). Lesson recorded: testing the exe by+ hand is not testing the jump list. Release profile: thin LTO (28 s vs 36.5 s per leaf edit, exe +0.8 MB). Jump lists: the two-right-clicks flash is ab#155 (registry-only AUMIDs, no anchoring shortcut); `aumidShortcut` (opt-in) asks ab for the anchor; human verification pending. Filed: ab#152 (kind:exe seed npm), ab#155 (jump-list anchor), ab#157 (forward bridge consent prompts into ah for boxes that block toasts).
rust/Cargo.toml+3−3@@ -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.413"+version = "2.0.424" edition = "2021" license = "MIT" @@ -20,9 +20,9 @@ png = "0.17" [profile.release] opt-level = "z"-lto = true+lto = "thin" strip = true-codegen-units = 1+codegen-units = 16 # Dev builds: keep our own crates unoptimized (fast incremental), dependencies lightly optimized. [profile.dev.package."*"]
rust/bridge/Cargo.toml+1@@ -11,6 +11,7 @@ path = "src/main.rs" [dependencies] serde_json.workspace = true+ureq.workspace = true tiny_http.workspace = true pup-core = { path = "../core" } pup-host = { path = "../host" }
rust/bridge/src/main.rs+26@@ -29,8 +29,34 @@ fn caller_from(req: &tiny_http::Request) -> Caller { Caller::from_headers(req.headers().iter().map(|h| (h.field.as_str().as_str(), h.value.as_str()))) } +/// `adom-bridge-puppeteer.exe --jumplist <verb> [<base64 json args>]`: the jump-list task / taskbar+/// relaunch callback. Reads the port file the running bridge wrote, POSTs `{command, args}` to its+/// loopback /command as caller user-taskbar-menu, and exits. If no bridge answers (asleep), hands the+/// verb to the ab CLI, which wakes pup. Replaces the Node-era pup-jumplist-call.ps1 (2.0.423).+fn jumplist_call(verb: &str, b64: &str) -> i32 {+ let mut args: serde_json::Value = if b64.is_empty() { serde_json::json!({}) } else { pup_cdp::base64_decode(b64).ok().and_then(|b| serde_json::from_slice(&b).ok()).unwrap_or_else(|| serde_json::json!({})) };+ args["caller"] = serde_json::json!({ "aiThread": "user-taskbar-menu" });+ let body = serde_json::json!({ "command": verb, "args": args }).to_string();+ let lad = std::env::var("LOCALAPPDATA").map(std::path::PathBuf::from).unwrap_or_default();+ let port = std::fs::read_to_string(lad.join("Adom Bridge").join("pup-bridge-port.json")).ok().and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok()).and_then(|v| v["port"].as_u64());+ if let Some(port) = port {+ let r = ureq::AgentBuilder::new().timeout(std::time::Duration::from_secs(60)).build().post(&format!("http://127.0.0.1:{port}/command"))+ .set("Content-Type", "application/json").set("x-adom-caller-thread", "user-taskbar-menu").set("x-adom-caller-reason", "jump-list click").send_string(&body);+ if r.is_ok() { return 0; }+ }+ // 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; }+ }+ 1+}+ fn main() { let args: Vec<String> = std::env::args().collect();+ if args.get(1).map(|a| a == "--jumplist").unwrap_or(false) {+ std::process::exit(jumplist_call(args.get(2).map(String::as_str).unwrap_or(""), args.get(3).map(String::as_str).unwrap_or("")));+ } let port: u16 = args.iter().position(|a| a == "--port").and_then(|i| args.get(i + 1)).and_then(|p| p.parse().ok()).unwrap_or(8851); let bind = std::env::var("ADOM_BIND_HOST").unwrap_or_else(|_| "127.0.0.1".into()); let h = host();
rust/browser/src/lib.rs+6−1@@ -404,7 +404,12 @@ pub fn chrome_switches(profile_dir: &Path, high_fps: bool, insecure_dev: bool, e if high_fps { v.extend(["--disable-background-timer-throttling", "--disable-backgrounding-occluded-windows", "--disable-renderer-backgrounding"].map(String::from)); } if insecure_dev { v.push("--disable-web-security".into()); } if !extensions.is_empty() { v.push(format!("--load-extension={}", extensions.iter().map(|p| p.to_string_lossy().to_string()).collect::<Vec<_>>().join(","))); }- v.push("about:blank".into());+ // Node v1.8.51: `--no-startup-window` is THE fix for the focus-steal war AND for the orphan+ // window. Chrome starts with NO window; pup creates the one it wants over CDP (Target.createTarget,+ // off-screen, then parked). The Rust port launched with "about:blank" instead, so every profile+ // 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 }
rust/verbs/src/identity.rs+19−10@@ -39,10 +39,10 @@ fn register_args(ctx: &Ctx, app_id: &str, display: &str, icon: &str) -> Value { pub fn boot(ctx: &Ctx) { if let Err(e) = id::write_icons(&icons_dir(ctx)) { pup_core::log(format!("[identity] icons not written: {e}")); } let lad = local_app_data(ctx);- let launcher = id::launcher_path(&lad);- let _ = std::fs::create_dir_all(launcher.parent().unwrap());- let script = id::launcher_script();- if std::fs::read_to_string(&launcher).ok().as_deref() != Some(script.as_str()) { let _ = std::fs::write(&launcher, script); }+ // 2.0.423: jump-list tasks call pup's OWN exe (`--jumplist <verb> <b64>`), no PowerShell script;+ // remove the Node-era launcher so nothing on disk still points at powershell.exe.+ let _ = std::fs::remove_file(id::launcher_path(&lad));+ let _ = std::fs::create_dir_all(id::port_file_path(&lad).parent().unwrap()); let _ = std::fs::write(id::port_file_path(&lad), json!({ "port": ctx.port, "pid": std::process::id(), "version": pup_core::version(), "host": "127.0.0.1", "assignedPort": ctx.port, "shell": "rust" }).to_string()); match ctx.ab.call("desktop_register_app_identity", register_args(ctx, id::APP_ID, id::DISPLAY_NAME, &icons_dir(ctx).join("adom-pup.ico").to_string_lossy()), &internal_caller(), AB_T) { Ok(_) => pup_core::log("[identity] base AUMID registered (Adom.Pup)"),@@ -70,20 +70,30 @@ fn ensure_tile(ctx: &Ctx, sid: &str, name: &str) -> Option<PathBuf> { } fn jumplist_tasks(ctx: &Ctx, sid: &str, url: &str) -> Vec<Value> {- let launcher = id::launcher_path(&local_app_data(ctx));- let launch = |tail: &str, q: bool| ctx.host.script_launch(&launcher, tail, q);+ let launch = exe_launch(); let st = &ctx.settings; let mut tasks = Vec::new(); if st.get("jlViewTasks", "on") != "off" && id::is_adom_url(url) { tasks.push(id::task(&launch, "pup_wiki_set_view", json!({ "sessionId": sid, "view": "authed" }), "Switch to logged-in view", "Reload this Adom wiki page SIGNED IN", Some(&icons_dir(ctx).join("pup-cat-wiki-in9.ico")))); tasks.push(id::task(&launch, "pup_wiki_set_view", json!({ "sessionId": sid, "view": "public" }), "Switch to public view", "Reload this Adom wiki page logged OUT, exactly what the public sees", Some(&icons_dir(ctx).join("pup-cat-wiki9.ico")))); }- if st.get("jlAnnotate", "on") != "off" { tasks.push(id::task(&launch, "pup_annotate", json!({ "sessionId": sid, "tool": "pen" }), "Annotate this page", "Draw on the page; the capture goes to the owning AI thread", None)); }+ if st.get("jlAnnotate", "on") != "off" { tasks.push(id::task(&launch, "pup_annotate", json!({ "sessionId": sid, "tool": "pen" }), "Annotate this page", "Draw on the page; the capture goes to the owning AI thread", Some(&icons_dir(ctx).join("adom-pup.ico")))); } if st.get("jlDashboard", "on") != "off" { tasks.push(id::task(&launch, "pup_dashboard", json!({}), "Open pup dashboard", "Every window pup is driving, live", Some(&icons_dir(ctx).join("adom-pup.ico")))); }- if st.get("jlCloseAll", "on") != "off" { tasks.push(id::task(&launch, "pup_close", json!({ "all": true, "everyone": true }), "Close ALL Adom Pup windows", "Closes every pup window on this machine", None)); }+ if st.get("jlCloseAll", "on") != "off" { tasks.push(id::task(&launch, "pup_close", json!({ "all": true, "everyone": true }), "Close ALL Adom Pup windows", "Closes every pup window on this machine", Some(&icons_dir(ctx).join("adom-pup.ico")))); } tasks } +/// The jump-list / relaunch launcher: pup's own exe in its `--jumplist` mode (`bridge/src/main.rs`),+/// which POSTs the verb to the running bridge over loopback as caller user-taskbar-menu and exits.+/// Until 2.0.422 this was powershell.exe running a .ps1 (Node had no exe to call back into), which+/// put the PowerShell icon on icon-less tasks and cost a PowerShell start per click (John, 2026-09-06).+/// ab's ab-taskrun shim still interposes the target so a console-subsystem exe never paints a window.+fn exe_launch() -> impl Fn(&str, bool) -> (String, String) {+ let exe = std::env::current_exe().map(|p| p.to_string_lossy().to_string()).unwrap_or_else(|_| "adom-bridge-puppeteer.exe".into());+ // tasks: bare target path + args (ab builds the shell link); relaunch: one command line, path quoted.+ move |tail: &str, quote_for_relaunch: bool| (if quote_for_relaunch { format!("\"{exe}\"") } else { exe.clone() }, format!("--jumplist {tail}"))+}+ /// Stamp one window's identity (idempotent; safe to call again after a navigation). pub fn stamp(ctx: &Ctx, sid: &str, caller: &Caller, full: bool) { let Some(s) = ctx.sessions.get(sid) else { return };@@ -106,8 +116,7 @@ pub fn stamp(ctx: &Ctx, sid: &str, caller: &Caller, full: bool) { }; if full || s.app_id.as_deref() != Some(app_id.as_str()) { if let Err(e) = ctx.ab.call("desktop_register_app_identity", register_args(ctx, &app_id, &display, &icon.to_string_lossy()), caller, AB_T) { pup_core::log(format!("[identity] \"{sid}\" register {app_id} failed: {e}")); return; }- let launcher = id::launcher_path(&local_app_data(ctx));- let launch = |tail: &str, q: bool| ctx.host.script_launch(&launcher, tail, q);+ let launch = exe_launch(); match ctx.ab.call("desktop_set_window_identity", json!({ "hwnd": hwnd, "appId": app_id, "displayName": display, "iconPath": icon.to_string_lossy(), "relaunchCommand": id::relaunch_command(&launch, sid), "relaunchDisplayName": display, "relaunchIconResource": icon.to_string_lossy() }), caller, AB_T) { Ok(_) => { pup_core::log(format!("[identity] \"{sid}\" stamped {app_id} ({m})")); ctx.sessions.with_mut(sid, |x| x.app_id = Some(app_id.clone())); if let Some(x) = ctx.sessions.get(sid) { ctx.sessions.save_file(&x); } } Err(e) => { pup_core::log(format!("[identity] \"{sid}\" stamp failed: {e}")); return; }
rust/verbs/src/lib.rs+27−1@@ -136,6 +136,8 @@ pub struct Ctx { pub last_audit: Mutex<Option<Value>>, } +/// Callers that ARE the human at the desk (Node: USER_CALLERS): ownership never applies to them.+pub const USER_CALLERS: [&str; 4] = ["user-taskbar-menu", "user-toolbar", "user-pill", "user-dashboard"]; pub const SHOT_KEEP_DAYS: u64 = 14; pub const SHOT_KEEP_BYTES: u64 = 1_000_000_000; pub const REC_KEEP_DAYS: u64 = 30; pub const REC_KEEP_BYTES: u64 = 3_000_000_000; @@ -409,7 +411,10 @@ pub fn resolve(ctx: &Ctx, caller: &Caller, args: &Value) -> Result<Session, Valu match ctx.sessions.get(&id) { Some(s) => { // Ownership guard (Node: ownershipRefusal): another thread's window needs takeover:true.- if let Some(owner) = s.owner.as_deref() { if !caller.thread.is_empty() && owner != caller.thread && args.get("takeover").and_then(|t| t.as_bool()) != Some(true) {+ // A HUMAN caller (jump-list task, toolbar button, dashboard click) is exempt, as in Node's+ // USER_CALLERS: the user owns every window. Missing in the Rust port until 2.0.424, so every+ // window-targeting jump-list task answered not_your_window (John, 2026-09-06).+ if let Some(owner) = s.owner.as_deref() { if !caller.thread.is_empty() && !USER_CALLERS.contains(&caller.thread.as_str()) && owner != caller.thread && args.get("takeover").and_then(|t| t.as_bool()) != Some(true) { return Err(pup_core::merge(err("not_your_window", format!("window \"{}\" belongs to thread \"{}\"", id, owner), Some("Pass takeover:true to drive another thread's window deliberately, or open your own with pup_open_window.")), json!({ "owner": owner, "window": id }))); } } Ok(s)@@ -420,6 +425,25 @@ pub fn resolve(ctx: &Ctx, caller: &Caller, args: &Value) -> Result<Session, Valu pub fn touch(ctx: &Ctx, sid: &str, note: Option<&str>) { ctx.sessions.with_mut(sid, |x| { x.last_agent_update_ms = pup_core::now_ms(); if let Some(n) = note { x.last_note = Some(n.to_string()); } }); } +/// Close top-level about:blank page targets nobody tracks: the initial window an older pup build+/// launched Chrome with (it never got a session, an identity or a close), which shows up as a stock+/// Chrome for Testing button on the taskbar. Runs once per browser connect, BEFORE the handle is+/// published, so no verb can be creating a target in the gap. Popups (openerId) and every target a+/// session owns are never touched.+fn close_orphan_blank_targets(ctx: &Ctx, cdp: &Cdp, profile: &str) {+ let Ok(targets) = cdp.targets() else { return };+ let owned: std::collections::HashSet<String> = ctx.sessions.all().iter().flat_map(|s| s.tabs.iter().map(|t| t.target_id.clone())).collect();+ let mut closed = 0;+ for t in targets {+ let tid = t["targetId"].as_str().unwrap_or("");+ let is_page = t["type"] == json!("page");+ let blank = t["url"].as_str().map(|u| u == "about:blank" || u.is_empty()).unwrap_or(false);+ let popup = t.get("openerId").and_then(|o| o.as_str()).map(|o| !o.is_empty()).unwrap_or(false);+ if is_page && blank && !popup && !tid.is_empty() && !owned.contains(tid) { if cdp.close_target(tid).is_ok() { closed += 1; } }+ }+ if closed > 0 { pup_core::log(format!("[launch] closed {closed} orphan about:blank window(s) in profile \"{profile}\" (untracked launch window, stock taskbar icon)")); }+}+ pub fn browser_for(ctx: &Ctx, profile: &str) -> Option<Arc<BrowserHandle>> { ctx.browsers.lock().unwrap().get(profile).cloned().filter(|b| b.cdp.is_alive()) } /// Get or launch the browser for a profile: reuse a live DevTools port, else spawn-verify each@@ -435,6 +459,7 @@ fn get_or_launch(ctx: &Ctx, profile: &str, high_fps: bool, insecure_dev: bool) - let pid = cdp.send(None, "SystemInfo.getProcessInfo", json!({}), Duration::from_secs(5)).ok().and_then(|v| v["processInfo"].as_array().and_then(|a| a.iter().find(|p| p["type"] == json!("browser")).and_then(|p| p["id"].as_u64()))).unwrap_or(0) as u32; let h = Arc::new(BrowserHandle { cdp, launched: Launched { pid, cdp_port: port, candidate: cand, profile_dir, reused: true } }); grant_default_permissions(&h.cdp);+ close_orphan_blank_targets(ctx, &h.cdp, profile); events::start_pump(profile.into(), h.cdp.clone(), ctx.sessions.clone(), ctx.tabstate.clone()); ctx.browsers.lock().unwrap().insert(profile.into(), h.clone()); return Ok(h);@@ -461,6 +486,7 @@ fn get_or_launch(ctx: &Ctx, profile: &str, high_fps: bool, insecure_dev: bool) - pup_browser::write_default(&*ctx.host, c, false); let h = Arc::new(BrowserHandle { cdp, launched: l }); grant_default_permissions(&h.cdp);+ close_orphan_blank_targets(ctx, &h.cdp, profile); events::start_pump(profile.into(), h.cdp.clone(), ctx.sessions.clone(), ctx.tabstate.clone()); ctx.browsers.lock().unwrap().insert(profile.into(), h.clone()); return Ok(h);
tools/jumplist-ralph.pyadded+80@@ -0,0 +1,80 @@+#!/usr/bin/env python3+"""Ralph test for the jump-list tasks, THROUGH THE REAL PATH: Explorer launches ab's windowless shim+(ab-taskrun.exe) with the link's target and arguments; this script runs exactly that command line on the+box for every task pup registers, then asserts the effect in the window. John, 2026-09-06: "have you ralph+tested your jump list items? i just tried annotate and it doesn't work" (the Rust port refused every+window-targeting task with not_your_window; a by-hand exe call of pup_dashboard had passed for nothing).++ tools/jumplist-ralph.py --target arav-rog+"""+import argparse, base64, json, subprocess, sys, time++THREAD = "jl-ralph"+WIKI = "https://wiki.adom.inc/adom/pup-bridge"++def ab(target, verb, args, thread=THREAD, timeout=120):+ p = subprocess.run(['adom-bridge', '--target', target, '--ai-thread', thread, verb, json.dumps(args)], capture_output=True, text=True, timeout=timeout + 30)+ try: d = json.loads(p.stdout)+ except Exception: return {'error': (p.stdout + p.stderr)[-300:]}+ o = d.get('output'); inner = json.loads(o) if isinstance(o, str) and o.startswith('{') else {}+ return {**inner, **d}++def ps(target, script, reason):+ r = ab(target, 'shell_execute', {'command': 'powershell -NoProfile -Command "' + script.replace('"', '\\"') + '"', 'timeoutSeconds': 90, 'reason': reason})+ return (r.get('stdout') or r.get('error') or '').strip()++def b64(o): return base64.b64encode(json.dumps(o, separators=(',', ':')).encode()).decode()++def main():+ ap = argparse.ArgumentParser(); ap.add_argument('--target', required=True); a = ap.parse_args(); t = a.target+ exe = ps(t, "Get-ChildItem (Join-Path $env:LOCALAPPDATA 'Adom Bridge\\bridges-cache\\puppeteer') -Filter *.exe | Select-Object -First 1 -ExpandProperty FullName", 'jump-list ralph: locate the pup exe')+ shim = ps(t, "Join-Path $env:LOCALAPPDATA 'Adom Bridge\\ab-taskrun.exe'", 'jump-list ralph: locate the taskrun shim')+ print(f'exe={exe}\nshim={shim}')+ results = []+ def fire(verb, args, reason):+ # EXACTLY what the shell link holds: shim "target" --jumplist <verb> <b64> (window_identity.rs quotes the target)+ cmd = f"& '{shim}' '{exe}' --jumplist {verb} {b64(args)}; 'exit=' + $LASTEXITCODE"+ return ps(t, cmd, reason)+ def check(name, ok, detail=''):+ results.append((name, ok)); print(f' {"PASS" if ok else "FAIL"} {name} {detail}')++ ab(t, 'pup_close', {'all': True, 'everyone': True}); time.sleep(1)+ r = ab(t, 'pup_open_window', {'window': 'jl-ralph', 'url': WIKI, 'owner': THREAD}, timeout=150)+ sid = r.get('sessionId') or 'jl-ralph'; print(f'window {sid} hwnd={r.get("hwnd")}'); time.sleep(6) # identity + jump list committed++ # 1. Annotate this page+ print('\n== Annotate this page'); out = fire('pup_annotate', {'sessionId': sid, 'tool': 'pen'}, 'jump-list ralph: Annotate task through the shim'); time.sleep(3)+ e = ab(t, 'pup_eval', {'window': sid, 'expr': '!!window.__pupAnnotate'})+ lau = (ab(t, 'pup_list_windows', {}).get('sessions') or [{}])+ lau = next((s.get('lastAgentUpdate') or {} for s in lau if s.get('sessionId') == sid), {})+ check('annotate overlay armed', e.get('result') is True, f'({out}; lastAgentUpdate={lau.get("verb")})')+ ab(t, 'pup_eval', {'window': sid, 'expr': 'window.__pupAnnotate && window.__pupAnnotate.exit && window.__pupAnnotate.exit(); true'})++ # 2. Switch to public view / logged-in view+ print('\n== Switch to public view'); out = fire('pup_wiki_set_view', {'sessionId': sid, 'view': 'public'}, 'jump-list ralph: public view task'); time.sleep(6)+ u = ab(t, 'pup_eval', {'window': sid, 'expr': 'location.href'}).get('result') or ''+ check('public view url', 'view=public' in u, f'({out}; url={u[:70]})')+ print('\n== Switch to logged-in view'); out = fire('pup_wiki_set_view', {'sessionId': sid, 'view': 'authed'}, 'jump-list ralph: logged-in view task'); time.sleep(6)+ u = ab(t, 'pup_eval', {'window': sid, 'expr': 'location.href'}).get('result') or ''+ check('logged-in view url', 'view=public' not in u and 'wiki.adom.inc' in u, f'({out}; url={u[:70]})')++ # 3. Taskbar relaunch (the header row click): pup_focus_window+ print('\n== Relaunch command (pup_focus_window)'); out = fire('pup_focus_window', {'sessionId': sid, 'foregroundReason': 'jump-list ralph: the relaunch command'}, 'jump-list ralph: relaunch command'); time.sleep(2)+ lau = next((s.get('lastAgentUpdate') or {} for s in (ab(t, 'pup_list_windows', {}).get('sessions') or []) if s.get('sessionId') == sid), {})+ check('focus reached pup', lau.get('verb') == 'pup_focus_window', f'({out}; lastAgentUpdate={lau.get("verb")})')++ # 4. Open pup dashboard+ print('\n== Open pup dashboard'); out = fire('pup_dashboard', {}, 'jump-list ralph: dashboard task'); time.sleep(5)+ sess = [s.get('sessionId') for s in (ab(t, 'pup_list_windows', {}).get('sessions') or [])]+ check('dashboard window exists', 'pup-dashboard' in sess, f'({out}; sessions={sess})')++ # 5. Close ALL Adom Pup windows+ print('\n== Close ALL Adom Pup windows'); out = fire('pup_close', {'all': True, 'everyone': True}, 'jump-list ralph: close-all task'); time.sleep(4)+ sess = [s.get('sessionId') for s in (ab(t, 'pup_list_windows', {}).get('sessions') or [])]+ check('all windows closed', not sess, f'({out}; sessions={sess})')++ fails = [n for n, ok in results if not ok]+ print(f'\nJUMPLIST RALPH: {"ALL PASS" if not fails else "FAIL " + ", ".join(fails)} ({len(results) - len(fails)}/{len(results)})')+ sys.exit(1 if fails else 0)++if __name__ == '__main__': main()