← Commit history
dev-skills/pup-bridge-debug/SKILL.md+1−1
@@ -620,7 +620,7 @@ brands it a beat later. Harmless, but do not read that first line as a failure.  ## The header icon obeys the LIVE-TILE TRAP too — brand it BEFORE the stamp (v1.9.61) -AD's `adom-desktop-window-identity` skill states the rule that explains months of this: **Win11 bakes+AD's `adom-bridge-window-identity` skill states the rule that explains months of this: **Win11 bakes a taskbar button's art when the button is CREATED and never re-reads it.** That applies to the jump-list HEADER row, not just the tile. 
dev-skills/pup-bridge-dev/SKILL.md+3−3
@@ -18,7 +18,7 @@ and `PUBLISHING.md` (step-by-step recipe) in this repo alongside this skill.  ## ⚖ OWN YOUR DOMAIN — the capability-vs-policy test (John's rule, and he should never have had to say it) -Before filing ANYTHING against adom-desktop, apply this test:+Before filing ANYTHING against adom-bridge, apply this test:  - **CAPABILITY** = a generic OS/native mechanism pup cannot perform itself (z-order set-to-back,   window identity stamping with process-lifetime HICONs, taskbar overlay/flash, monitor@@ -850,7 +850,7 @@ mechanical requirement.  **The production design (AD feature request):** 1. AD installer registers the "Adom Pup" Start Menu .lnk (AUMID `Adom.Pup`, adom-pup.ico, target =-   adom-desktop-cli pup_open_window) — warm AppResolver cache before any window ever exists.+   adom-bridge-cli pup_open_window) — warm AppResolver cache before any window ever exists. 2. New AD verb `desktop_set_window_identity {hwnd|titleContains, appId, iconPath}`: AD loads the    .ico ONCE (handles live as long as AD runs), stamps the AUMID property + WM_SETICON per window. 3. pup calls it at park time, same place as the badge; overlay badge retired for pup windows@@ -1311,7 +1311,7 @@ FIRST verb), not by reading the code and assuming. ## Caller identity is PER-CALL, never an env var in a shared container (2026-07-25)  AD ≥1.9.180 refuses every relayed command that doesn't name the AI thread issuing it-(`errorCode: caller_identity_required`). The `adom-desktop-caller-identity` skill recommends setting+(`errorCode: caller_identity_required`). The `adom-bridge-caller-identity` skill recommends setting `ADOM_AI_THREAD` once per session and calls the env var "strictly better". **That guidance is wrong for Adom's topology, and following it makes the Activity Log lie.** John caught it instantly: *"an env var makes no sense... I typically have about 20 AI threads running... that env var would be getting changed
dev-skills/pup-browser-import/SKILL.md+1−1
@@ -187,7 +187,7 @@ Desktop itself does. It is legitimate. It is ALSO defeating a security control,   policy? ASR?) - and whether an UNmanaged profile on the same box imports cookies fine. That tells us   whether cookie import is ever worth offering or should be quietly omitted on managed devices. - Whether AD already has an elevate-to-SYSTEM primitive or that helper is net-new AD-core work (this-  is an AD-boundary question - see `adom-desktop-bridge-ownership`; the elevation primitive is+  is an AD-boundary question - see `adom-bridge-bridge-ownership`; the elevation primitive is   probably AD-core, the Chrome-blob decryption + vault import is ours). - Whether a pup CfT window actually triggers Windows Hello for a passkey WebAuthn ceremony, or whether   automated Chrome silently routes to a virtual authenticator and needs a flag.
dev-skills/pup-hero-image/SKILL.md+1−1
@@ -82,7 +82,7 @@ never just say "published".   tracked whitelisted hero file instead. That's why the file is still named `hero-v9.png`. - **hero-studio defaults `overlay.slug=true`** on a fresh port - turn off pill AND slug AND author   or the export gets a baked slug in the dynamic marquee band (hero-studio#29).-- **The studio package dropdown may list pup under the OLD slug** (`adom/adom-desktop-puppeteer-bridge`,+- **The studio package dropdown may list pup under the OLD slug** (`adom/adom-bridge-puppeteer-bridge`,   hero-studio issue: renamed pages keep stale option values). If `set page=` won't bind, select the   option in the DOM via pup_eval. The EXPORT slug follows `set slug=pup-bridge` regardless. - **Never take over another thread's hero-studio port** - John's other build threads run their own
rust/Cargo.lock+12−12
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-pup-bridge"-version = "2.0.651"+version = "2.0.658" dependencies = [  "pup-browser",  "pup-cdp",@@ -655,7 +655,7 @@ dependencies = [  [[package]] name = "pup-browser"-version = "2.0.651"+version = "2.0.658" dependencies = [  "pup-core",  "pup-host",@@ -667,7 +667,7 @@ dependencies = [  [[package]] name = "pup-cdp"-version = "2.0.651"+version = "2.0.658" dependencies = [  "serde_json",  "thiserror",@@ -677,7 +677,7 @@ dependencies = [  [[package]] name = "pup-core"-version = "2.0.651"+version = "2.0.658" dependencies = [  "serde",  "serde_json",@@ -687,7 +687,7 @@ dependencies = [  [[package]] name = "pup-dashboard"-version = "2.0.651"+version = "2.0.658" dependencies = [  "pup-cdp",  "pup-core",@@ -700,7 +700,7 @@ dependencies = [  [[package]] name = "pup-host"-version = "2.0.651"+version = "2.0.658" dependencies = [  "serde",  "serde_json",@@ -709,14 +709,14 @@ dependencies = [  [[package]] name = "pup-host-mac"-version = "2.0.651"+version = "2.0.658" dependencies = [  "pup-host", ]  [[package]] name = "pup-host-win"-version = "2.0.651"+version = "2.0.658" dependencies = [  "pup-core",  "pup-host",@@ -726,7 +726,7 @@ dependencies = [  [[package]] name = "pup-identity"-version = "2.0.651"+version = "2.0.658" dependencies = [  "fontdue",  "image",@@ -736,7 +736,7 @@ dependencies = [  [[package]] name = "pup-session"-version = "2.0.651"+version = "2.0.658" dependencies = [  "pup-core",  "serde",@@ -746,7 +746,7 @@ dependencies = [  [[package]] name = "pup-vault"-version = "2.0.651"+version = "2.0.658" dependencies = [  "pup-core",  "pup-host",@@ -757,7 +757,7 @@ dependencies = [  [[package]] name = "pup-verbs"-version = "2.0.651"+version = "2.0.658" 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.651"+version = "2.0.658" edition = "2021" license = "MIT" 
rust/host-win/src/lib.rs+9
@@ -412,6 +412,13 @@ mod secrets {         // NOT under the cache lock: 2.0.650 held it here and called save(), which locks it again; every vault         // verb on the laptop hung and the migration deleted 30 entries it had not yet persisted.         let mut m: std::collections::BTreeMap<String, String> = std::fs::read(store_path(h)).ok().and_then(|b| dpapi(&b, false)).and_then(|j| serde_json::from_slice(&j).ok()).unwrap_or_default();+        // 2.0.652: the service was renamed (adom-desktop-pup -> adom-bridge-pup); keys stored under the old name+        // are renamed in the file, and legacy Credential Manager entries are read under BOTH names below.+        let legacy_prefix = "adom-desktop-pup/";+        if service != "adom-desktop-pup" {+            let old_keys: Vec<String> = m.keys().filter(|k| k.starts_with(legacy_prefix)).cloned().collect();+            if !old_keys.is_empty() { for k in old_keys { if let Some(v) = m.remove(&k) { m.insert(format!("{service}/{}", &k[legacy_prefix.len()..]), v); } } let _ = save(h, &m); }+        }         // one-time migration, SAVE FIRST: read every legacy entry into the map, persist the whole map, and only         // then delete the Credential Manager copies (a failed save leaves the legacy store untouched).         let index = h.state_dir().join("pup-credentials.index.json");@@ -423,7 +430,9 @@ mod secrets {                     if acct.is_empty() { continue; }                     let key = format!("{service}/{acct}");                     if m.contains_key(&key) { continue; }+                    let legacy_key = format!("{legacy_prefix}{acct}");                     if let Some(secret) = cred_read(&key) { m.insert(key.clone(), secret); moved.push(key); }+                    else if let Some(secret) = cred_read(&legacy_key) { m.insert(key.clone(), secret); moved.push(legacy_key); }                 }                 if !moved.is_empty() {                     match save(h, &m) {
rust/vault/src/lib.rs+2−1
@@ -9,7 +9,8 @@ use pup_host::HostSecrets; use serde_json::{json, Value}; use std::path::{Path, PathBuf}; -pub const CRED_SERVICE: &str = "adom-desktop-pup";+pub const CRED_SERVICE: &str = "adom-bridge-pup";   // 2.0.652: renamed from adom-desktop-pup; the host store renames stored keys on load+pub const LEGACY_CRED_SERVICE: &str = "adom-desktop-pup"; pub const CARD_SERVICE: &str = "adom-pup-cards";  fn js_str(s: &str) -> String { serde_json::to_string(s).unwrap_or_else(|_| "\"\"".into()) }
rust/verbs/src/extras.rs+1−1
@@ -65,7 +65,7 @@ pub fn download(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {             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) {-                Ok(r) => { touch(ctx, &s0.id, None); ok(json!({ "success": true, "sessionId": s0.id, "window": s0.id, "aiThread": s0.owner, "output": json!({ "ok": true, "savedTo": r["savedTo"], "filename": r["filename"], "bytes": r["bytes"], "url": url, "transport": "ext" }) })) }+                Ok(r) => { touch(ctx, &s0.id, None); ok(json!({ "success": true, "sessionId": s0.id, "window": s0.id, "aiThread": s0.owner, "savedTo": r["savedTo"], "fileName": r["filename"], "bytes": r["bytes"], "output": json!({ "ok": true, "savedTo": r["savedTo"], "filename": r["filename"], "bytes": r["bytes"], "url": url, "transport": "ext" }) })) }                 Err(e) => e,             };         }
rust/verbs/src/lib.rs+30−3
@@ -1010,6 +1010,18 @@ pub fn inject_page_scripts(ctx: &Ctx, s: &Session, cdp: &Cdp, sid: &str) {     } } +/// 2.0.653: the extension-lane twin of inject_page_scripts (per page load; the smoke found no owner chip there).+pub fn inject_page_scripts_ext(ctx: &Ctx, s: &Session, lane: &transport::Lane, tab: &Tab) {+    if tab.url.is_empty() || tab.url.starts_with("about:") || tab.url.starts_with("chrome") { return; }   // nothing to decorate, evals forbidden there+    if !transport::wait_ready(lane, tab, &tab.url, Duration::from_secs(10)) { pup_core::log(format!("[page] \"{}\" page scripts skipped: the document was not ready within 10 s", s.id)); return; }+    let floaty = annotate::floaty_script(ctx);+    let _ = transport::evaluate(lane, tab, &floaty);+    if ctx.settings.get("ownerChip", "on") != "off" {+        let chip = OWNER_CHIP_JS.replace("__THREAD__", &serde_json::to_string(&s.owner.clone().unwrap_or_default()).unwrap()).replace("__PROFILE__", &serde_json::to_string(&s.profile).unwrap()).replace("__AVATAR__", &serde_json::to_string(&ctx.settings.get("identityAvatarB64", "")).unwrap()).replace("__IDENT__", &serde_json::to_string(&ctx.settings.get("identityName", "")).unwrap());+        let _ = transport::evaluate(lane, tab, &chip);+    }+}+ pub fn refresh_tab(cdp: &Cdp, t: &mut Tab) {     if let Ok(info) = cdp.target_info(&t.cdp_session) {         if let Some(u) = info.get("url").and_then(|u| u.as_str()) { t.url = u.into(); }@@ -1166,6 +1178,12 @@ fn open_window(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {     let explicit_lane = args.get("lane").and_then(|l| l.as_str()).map(|x| x.to_string());     let lane = explicit_lane.clone().unwrap_or_else(|| ctx.settings.get("defaultLane", "ext"));     let isolated = args.get("isolated").and_then(|b| b.as_bool()) == Some(true);+    // 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\"}."));+    }     if lane != "cdp" {         let needs_cdp = if profile != "adom-you" { Some(format!("profile:\"{profile}\" (a separate profile runs on the CDP lane; the extension lane is the shared adom-you browser)")) }             else if isolated { Some("isolated:true (a throwaway jar is a CDP-lane feature; see pup-bridge#112 for what it does today)".to_string()) }@@ -1399,6 +1417,7 @@ pub fn landed(url: &str, title: &str) -> Value { landed_with(url, title, None) } /// is a marketing page with a Log in link when signed out, and the URL rule alone called it signed in); a /// password field, or an email field next to a Continue/Next button, means the page IS a sign-in form. pub fn landed_ext(ctx: &Ctx, s: &Session, tab: &pup_session::Tab, url: &str, title: &str) -> Value {+    if let Ok(lane) = transport::lane_for(ctx, s) { let _ = transport::wait_ready(&lane, tab, url, Duration::from_secs(8)); }     const PROBE: &str = "(()=>{try{const vis=e=>{const r=e.getBoundingClientRect();return r.width>0&&r.height>0};const txt=e=>((e.innerText||e.value||e.getAttribute('aria-label')||'')+'').trim().toLowerCase().replace(/\\s+/g,' ');const els=[...document.querySelectorAll('a,button,input[type=submit],[role=button]')].filter(vis);const offered=els.some(e=>/^(log ?in|sign ?in|log ?in with .*|sign ?in with .*)$/.test(txt(e)));const pw=!!document.querySelector('input[type=password]');const em=!!document.querySelector('input[type=email],input[name*=email i],input[autocomplete=username],input[autocomplete=email]');const cont=els.some(e=>/^(continue|next|log ?in|sign ?in|continue with .*)$/.test(txt(e)));return JSON.stringify({offered,form:pw||(em&&cont)})}catch(e){return JSON.stringify({err:String(e)})}})()";     let probe = transport::lane_for(ctx, s).ok().and_then(|lane| transport::evaluate(&lane, tab, PROBE).ok()).and_then(|v| {         let txt = v.as_str().map(|x| x.to_string()).or_else(|| v.get("result").and_then(|r| r.as_str()).map(|x| x.to_string()))?;@@ -1452,11 +1471,16 @@ fn navigate_ext(ctx: &Ctx, caller: &Caller, s: &Session, url: &str) -> Value {     // 2.0.629: force, so a navigation inside the birth grace that Chrome ACTIVATED is still bottomed under the     // idle-hands rule; outside the grace, or with any input since birth, the primitive stands down as before.     let _ = park::bottom_unless_foreground(ctx, &s.id, true);+    // 2.0.653: what the CDP path always did after a navigate, now on this lane too: page scripts (owner chip,+    // floaty pencil) and the wiki view probe with the desktop-token auto-sign.+    let tab_now = ctx.sessions.get(&s.id).and_then(|x| x.tab_by_id(&tab.tab_id).cloned()).unwrap_or_else(|| tab.clone());+    inject_page_scripts_ext(ctx, s, &lane, &tab_now);+    let wiki_extra = wiki::after_navigate_ext(ctx, s, &lane, &tab_now, &u, None, false);     // 2.0.644 (pup-bridge#119): a cancelled HTTP auth challenge is reported as a typed error, not a blank page.     if let Some((host, answered)) = transport::ext_id(&tab).as_i64().and_then(vault::unanswered_challenge) {         if !answered { return pup_core::merge(vault::no_credential_error(&host, false), json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": tab.tab_id, "url": u, "title": title, "transport": "ext" })); }     }-    ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": tab.tab_id, "url": u, "title": title, "loaded": loaded, "transport": "ext", "landed": landed_ext(ctx, s, &tab, &u, &title) }))+    pup_core::merge(ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": tab.tab_id, "url": u, "title": title, "loaded": loaded, "transport": "ext", "landed": landed_ext(ctx, s, &tab, &u, &title) })), wiki_extra) }  fn eval(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {@@ -1852,6 +1876,9 @@ pub fn open_window_ext(ctx: &Ctx, caller: &Caller, args: &Value, sid: &str, owne     }     if early_stamped { if let Some((a, _, _)) = &plan { s.app_id = Some(a.clone()); } }     ctx.sessions.insert(s.clone()); ctx.sessions.save_file(&s);+    // 2.0.653: page scripts + wiki probe on the extension lane's first page (the CDP open always did both)+    let mut wiki_extra = json!({});+    if let (Ok(lane), Some(t0)) = (transport::lane_for(ctx, &s), s.tabs.get(0).cloned()) { if t0.url != "about:blank" { inject_page_scripts_ext(ctx, &s, &lane, &t0); wiki_extra = wiki::after_navigate_ext(ctx, &s, &lane, &t0, &t0.url, args.get("wikiView").and_then(|v| v.as_str()), false); } }     extdrive::apply_consumer_brands(ctx);   // once per lane browser; before the first page the user cares about     extdrive::close_orphan_blank_windows(ctx, chrome_win);   // the startup window a relaunch left behind, if any     extdrive::fit_now();@@ -1892,8 +1919,8 @@ pub fn open_window_ext(ctx: &Ctx, caller: &Caller, args: &Value, sid: &str, owne         }     }     let t = s.active().cloned().unwrap_or(t);-    ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": t.tab_id, "url": t.url, "title": t.title, "hwnd": s.hwnd, "windowId": s.window_id, "transport": "ext", "landed": landed_ext(ctx, &s, &t, &t.url, &t.title),-        "_hint": "Extension-driven window (no CDP debug port): a first-class pup window. Drive it with pup_navigate / pup_eval / pup_screenshot / pup_open_tab like any other." }))+    pup_core::merge(ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": t.tab_id, "url": t.url, "title": t.title, "hwnd": s.hwnd, "windowId": s.window_id, "transport": "ext", "landed": landed_ext(ctx, &s, &t, &t.url, &t.title),+        "_hint": "Extension-driven window (no CDP debug port): a first-class pup window. Drive it with pup_navigate / pup_eval / pup_screenshot / pup_open_tab like any other." })), wiki_extra) }  /// pup_clear_cookies {domain, names?}: drop a site's cookies in this window's jar, HttpOnly included. The case
rust/verbs/src/settings.rs+1
@@ -17,6 +17,7 @@ pub const KEYS: &[(&str, &str, &str)] = &[     ("defaultLane", "ext", "ext|cdp"),     ("consumerBrands", "on", "on|off"),     ("laneStockFlags", "on", "on|off"),+    ("cdpLane", "off", "on|off"),   // 2.0.652 (John): the CDP lane is opt-in per box; the extension lane must solve every use case     ("laneFieldTrials", "on", "on|off"),     ("consumerBrandsHeader", "", "str"),     ("fgWatch", "on", "on|off"), ("birthSettleMs", "400", "int"), ("aumidShortcut", "off", "on|off"), ("idleBrowserQuitMin", "10", "int"), ("winAttentionFlash", "off", "on|off"), ("prePermissions", "off", "on|off"), ("cdpQuiet", "off", "on|off"), ("stockFlags", "off", "on|off"), ("fgCaption", "on", "on|off"), ("fgCaptionMs", "3500", "int"), ("notMineCaption", "off", "on|off"),
rust/verbs/src/transport.rs+25
@@ -59,6 +59,31 @@ pub fn cdp_raw(lane: &Lane, t: &Tab, method: &str, params: Value, timeout_s: u64     } } +/// 2.0.655: wait until the tab's document is the one we navigated to and is past `loading` (up to `max`). A+/// script evaluated straight after a navigate ran in the OLD document and was wiped by the load (the owner+/// chip on the extension lane never appeared; the smoke caught it). Returns true when ready.+pub fn wait_ready(lane: &Lane, t: &Tab, want_url: &str, max: std::time::Duration) -> bool {+    // a blank or extension page has nothing to wait for and forbids evals ("Cannot access contents of url+    // chrome-extension://..."): 2.0.657 measured 10 s burned twice per about:blank navigate+    if want_url.is_empty() || want_url.starts_with("about:") || want_url.starts_with("chrome") { return true; }+    let t0 = std::time::Instant::now();+    let want_host = want_url.split("//").nth(1).and_then(|r| r.split('/').next()).unwrap_or("").to_string();+    while t0.elapsed() < max {+        let t1 = std::time::Instant::now();+        let r = evaluate(lane, t, "document.readyState + '|' + location.href");+        if r.is_err() || t0.elapsed() > std::time::Duration::from_secs(3) { pup_core::log(format!("[ready] tab {} want {} -> {} in {} ms", t.tab_id, want_url.chars().take(60).collect::<String>(), match &r { Ok(v) => v.as_str().unwrap_or("?").chars().take(90).collect::<String>(), Err(e) => format!("ERR {}", e["error"].as_str().unwrap_or("?").chars().take(90).collect::<String>()) }, t1.elapsed().as_millis())); }+        if let Ok(v) = r {+            let sv = v.as_str().unwrap_or("").to_string();+            let (state, href) = sv.split_once('|').unwrap_or(("", ""));+            let blank_wanted = want_url.starts_with("about:") || want_url.is_empty();+            let host_ok = blank_wanted || want_host.is_empty() || href.contains(&want_host);+            if state != "loading" && host_ok && (blank_wanted || href != "about:blank") { return true; }+        }+        std::thread::sleep(std::time::Duration::from_millis(200));+    }+    false+}+ pub fn evaluate(lane: &Lane, t: &Tab, expr: &str) -> Result<Value, Value> {     match lane {         Lane::Cdp(b) => b.cdp.evaluate(&t.cdp_session, expr, Duration::from_secs(20)).map_err(|e| err("eval_failed", e.to_string(), None)),
rust/verbs/src/wiki.rs+29
@@ -60,6 +60,32 @@ pub fn after_navigate(ctx: &Ctx, s: &pup_session::Session, cdp: &pup_cdp::Cdp, s         "_wikiHint": if authed { format!("Wiki opened LOGGED IN (as {}): private source, drafts, owner-only cards. For the anonymous view reopen with wikiView:\"public\" (same window, server-side render).", auth["user"].as_str().unwrap_or("the user")) } else if auth["unknown"] == json!(true) { "Wiki auth state could not be probed on this page (not evidence of logout).".to_string() } else { "Wiki is LOGGED OUT in the shared profile and no desktop token could sign it in. One-time login: pup_raise_os_window {foregroundReason:\"sign into the Adom wiki\"} and let the user complete the SSO; every pup window is then signed in.".to_string() } }) } +pub fn check_auth_ext(lane: &crate::transport::Lane, tab: &pup_session::Tab) -> Value {+    let js = r#"(async () => { try { const r = await fetch('/auth/me', { credentials: 'same-origin' }); if (!r.ok) return (r.status === 401 || r.status === 403) ? { authed: false } : { authed: false, unknown: true }; const j = await r.json(); return { authed: true, user: (j.user && (j.user.name || j.user.email)) || j.name || j.email || 'authed' }; } catch (e) { return { authed: false, unknown: true }; } })()"#;+    crate::transport::evaluate(lane, tab, js).ok().and_then(|v| v.as_str().and_then(|x| serde_json::from_str::<Value>(x).ok()).or_else(|| if v.is_object() { Some(v) } else { None })).unwrap_or(json!({ "authed": false, "unknown": true }))+}+pub fn auto_sign_ext(ctx: &Ctx, lane: &crate::transport::Lane, tab: &pup_session::Tab, next_path: &str) -> Option<String> {+    let magic = mint_magic_url(ctx, next_path)?;+    let _ = crate::transport::navigate(lane, tab, &magic);+    for _ in 0..6 { let a = check_auth_ext(lane, tab); if a["authed"] == json!(true) { return Some(a["user"].as_str().unwrap_or("authed").to_string()); } std::thread::sleep(Duration::from_millis(500)); }+    desktop_token(ctx).and_then(|(_, u)| u).or(Some("authed".into()))+}+/// 2.0.653: the extension-lane twin of after_navigate (the smoke found wikiView missing on that lane).+pub fn after_navigate_ext(ctx: &Ctx, s: &pup_session::Session, lane: &crate::transport::Lane, tab: &pup_session::Tab, url: &str, view: Option<&str>, isolated: bool) -> Value {+    if !pup_identity::is_adom_url(url) { return json!({}); }+    let _ = crate::transport::wait_ready(lane, tab, url, Duration::from_secs(8));+    if view == Some("public") { return json!({ "wikiView": "public", "_wikiHint": "Wiki PUBLIC view: the SERVER rendered this page exactly as a signed-out visitor sees it (?view=public). Reopen without wikiView (or view:\"authed\") for the logged-in view." }); }+    let mut auth = check_auth_ext(lane, tab);+    if auth["authed"] == json!(false) && auth["unknown"] != json!(true) && !isolated && desktop_token(ctx).is_some() {+        let next = url.split("//").nth(1).and_then(|r| r.find('/').map(|i| r[i..].to_string())).unwrap_or_else(|| "/adom".into());+        if let Some(who) = auto_sign_ext(ctx, lane, tab, &next) { auth = json!({ "authed": true, "user": who, "autoSigned": true }); pup_core::log(format!("[wikiview] \"{}\" AUTO-SIGNED from the desktop token ({who})", s.id)); }+    }+    let authed = auth["authed"] == json!(true);+    ctx.sessions.with_mut(&s.id, |x| { x.wiki_authed = Some(authed); x.wiki_view = Some(if authed { "authed" } else { "public" }.into()); });+    json!({ "wikiView": if authed { "authed" } else { "public" }, "wikiAuthed": authed, "wikiUser": auth["user"], "wikiLoginNeeded": if !authed && auth["unknown"] != json!(true) { json!(true) } else { Value::Null },+        "_wikiHint": if authed { format!("Wiki opened LOGGED IN (as {}): private source, drafts, owner-only cards. For the anonymous view reopen with wikiView:\"public\" (same window, server-side render).", auth["user"].as_str().unwrap_or("the user")) } else if auth["unknown"] == json!(true) { "Wiki auth state could not be probed on this page (not evidence of logout).".to_string() } else { "Wiki is LOGGED OUT in the shared profile and no desktop token could sign it in. One-time login: pup_raise_os_window {foregroundReason:\"sign into the Adom wiki\"} and let the user complete the SSO; every pup window is then signed in.".to_string() } })+}+ pub fn set_view(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 url = t.url.clone();@@ -70,6 +96,9 @@ pub fn set_view(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {     let target = if want == "public" { public_url(&url) } else { strip_public(&url) };     let loaded = b.cdp.navigate(&t.cdp_session, &target, Duration::from_secs(25)).map(|(_, l)| l).unwrap_or(false);     let extra = after_navigate(ctx, &s, &b.cdp, &t.cdp_session, &target, Some(want), false);+    // 2.0.656: the flip loads a new document; on the extension lane give it the page scripts (owner chip, floaty)+    // like every navigate does (the smoke checks the chip right after this flip).+    if crate::transport::is_ext(&s) { if let Ok(lane) = crate::transport::lane_for(ctx, &s) { let mut tt = t.clone(); tt.url = target.clone(); crate::inject_page_scripts_ext(ctx, &s, &lane, &tt); } }     let _ = crate::park::bottom_unless_foreground(ctx, &s.id, true);     touch(ctx, &s.id, None);     if let Some(a) = ctx.arc() { crate::identity::schedule(a, s.id.clone(), caller.clone(), true); }
src/chrome.js+3−3
@@ -1,4 +1,4 @@-// chrome.js — Chrome-for-Testing (CfT) self-heal for the Adom Desktop Puppeteer bridge.+// chrome.js — Chrome-for-Testing (CfT) self-heal for the Adom Bridge Puppeteer bridge. // // WHY THIS EXISTS (the cold-start bug it fixes): //   HD bundles AD; installing HD installs AD; AD ships THIS bridge as a pre-installed@@ -22,7 +22,7 @@ //     call answers with a pollable status instead of hanging 2–3 min. //   - readiness(): the snapshot powering pup_readiness + the /status led/summary/tooltip. //-// Everything here is self-contained in the bridge (no adom-desktop core changes).+// Everything here is self-contained in the bridge (no adom-bridge core changes).  'use strict'; @@ -677,7 +677,7 @@ function _download(url, destPath, redirects = 0) {   }); } -// ── Talking back to Adom Desktop from inside the bridge ───────────────────────+// ── Talking back to Adom Bridge from inside the bridge ─────────────────────── // AD spawned us; it also runs a loopback-only direct-API on the SAME machine // (`POST /command {app,command,args}`, same dispatcher as the CLI/WS path). That // lets THIS bridge fire a native toast + poll the clicked button on its OWN — no
src/server.js+7−7
@@ -1,6 +1,6 @@ // Puppeteer Bridge Server — multi-session persistent browser instances // Each session gets its own independent Chrome window (not tabs)-// Runs on desktop, controlled via HTTP from container or Adom Desktop app+// Runs on desktop, controlled via HTTP from container or Adom Bridge app // Port 8851 by default, configurable via --port const http = require('http'); const fs = require('fs');@@ -227,7 +227,7 @@ function ensureHeavyInit() {  // sharp is REQUIRED for resizing screenshots — never send full-res to container. // v1.8.40+: previously this was wrapped in try/catch with a `let sharp = null`-// fallback. The fallback path silently sent 2559px PNGs to /tmp/adom-desktop-screenshots/,+// fallback. The fallback path silently sent 2559px PNGs to /tmp/adom-bridge-screenshots/, // which CRASHES Claude many-image sessions at the 2000px limit. Hard-require // sharp is OPTIONAL — a heavy native dep whose install can fail on a fresh box (no // prebuild / no build tools / a managed-runtime PATH gap). pup must still spawn and@@ -3933,7 +3933,7 @@ function pupBadgeB64() { } // v1.8.73: FULL WINDOW-IDENTITY TAKEOVER (AD >=1.9.134, built to pup's spec). Two halves: // registration (once per bridge run: HKCU AUMID + Start Menu shortcut whose pin launches a REAL-// pup window via adom-desktop-cli) and per-window stamping (AUMID + WM_SETICON, done by AD-core+// pup window via adom-bridge-cli) and per-window stamping (AUMID + WM_SETICON, done by AD-core // because icon HICONs are process-owned USER objects that must outlive the caller — AD holds // them in a process-lifetime cache; see dev skill "Full icon/identity takeover"). The bridge // registers ITSELF over the loopback direct-API (trusted transport, Tier-2 verb is ungated@@ -4332,7 +4332,7 @@ function ensureNotchedIcons() {   return _notchReady; } // v2.0.40 (John: jump-list items do nothing): the CLI RENAMED from adom-desktop-cli.exe (in-// "Adom Desktop\\") to adom-bridge-cli.exe (in "Adom Bridge\\"). pup hardcoded the OLD path, so+// "Adom Bridge\\") to adom-bridge-cli.exe (in "Adom Bridge\\"). pup hardcoded the OLD path, so // every jump-list task + header click + pinned launch invoked a MISSING exe and silently no-opped // (verified live: the old path is gone, the new one exists). The bridge runs ON the desktop, so it // resolves the real exe from disk (Bridge name first, legacy fallback) and caches it.@@ -6141,7 +6141,7 @@ const BUSY_IDLE_MS = 2500; // every arg shape returns success here. The blocker is the ADDRESSING MODEL, not the capability. // macOS has no per-window taskbar button, so AD's mac path ignores titleContains/hwnd (it reports // hwnd:0) and paints AD's OWN Dock tile. pup's use is inherently PER-SESSION-WINDOW — "which of-// these five pup windows is this thread driving" — so un-gating would badge Adom Desktop rather+// these five pup windows is this thread driving" — so un-gating would badge Adom Bridge rather // than the pup window, and every session would fight over one tile. AD also skips overlay.icon on // mac outright (no Dock equivalent), which is what this call passes. // So: capability real, semantics incompatible. Do not un-gate on learning "desktop_taskbar works@@ -10193,7 +10193,7 @@ async function osClearFlashByActivate(pid, cdpPort, hwnd) { // appends a container-topology hint to a failed response whose URL is localhost. pup // doesn't duplicate it — and because AD keys on the connection FAILURE, Hydrogen // Desktop's working localhost URLs load fine and never trip it. (Enriching AD's hint-// with actionable alternatives is an adom/adom-desktop request.)+// with actionable alternatives is an adom/adom-bridge request.)  // v1.8.23: REAL taskbar flash (FlashWindowEx) for a session's window — the orange // nudge that tells the user "the agent touched this window" WITHOUT stealing focus.@@ -21553,7 +21553,7 @@ async function desktopRecordStartImpl(opts) {       'returns a single .webm at ~50fps, and does not need a HUD. ' +       'Hydrogen also has built-in desktop recording (recording start --share screen) which ' +       'is usually preferable for general "record my screen" asks. ' +-      'adom-desktop\'s desktop_record_* is mainly for the case where Hydrogen is already ' ++      'adom-bridge\'s desktop_record_* is mainly for the case where Hydrogen is already ' +       'doing tab recording (ralph-loop screenshot tests) and you want a parallel desktop ' +       'video at the same time. ' +       'If you have read this and you really do want full-desktop recording (not pup tab), ' +