master
John Lauer Update 21 file(s) 0edb3ff 1mo ago

pup in Rust: the port plan

Status (2026-09-05, late night): SHIPPED TO INSIDERS as 2.0.408, the first Rust-exe build (2.0.407 went first; 2.0.408 adds the recovered-tab script injection). Phases 0 to 4 are done; the public tier stays on the Node build (2.0.396) until a Rust build is promoted on John's words. Written 2026-09-05 against pup 2.0.396; John approved the plan the same day ("lets get back to the plan").

  • Phase 0: fixtures recorded from the Node build into docs/contract/*.json; the five ab asks filed (adom-bridge#144 send-to-bottom, #145 clipboard_set, #146 IconResource, #147 appId on window rows, #148 prewarm question); workspace laid down; tools/check-borders.sh enforces the host border.
  • Phase 1: rust/ builds for x86_64-pc-windows-gnu; crates core, host, host-win, host-mac (stubs), cdp, browser, session, verbs, dashboard, bridge carry real code; every test passes. Measured on winvm with the exe hand-launched on a spare port and its own profile: /health, /status, pup_readiness native; pup_open_window launched Chrome for Testing in 769 ms and had a background window with a resolved handle, parked at the bottom and loaded, in 1,750 ms; eval, navigate, screenshot and close_window answered in the Node shapes. The Node build on the box was untouched.
  • Phase 2: 48 verbs native. Tabs, trusted input, history, reload, viewport pin, dialogs, console errors (one event pump per browser), render_html, fetch_url, maximize; raise gated on a reason with caption + toast, lower, alert, taskbar progress; configure / use / quit_idle / rescan; session recovery after a bridge kill (measured), the 15 s health loop; taskbar identity through ab (thread tile rendered by the browser's own canvas from the embedded font, AUMID register + stamp, jump list, favicon overlay badge); the dashboard page with its SSE snapshot, action and image routes, /api/status, /identity; the four MV3 extensions embedded and loaded with pinned prefs.
  • Phase 3 (2026-09-05, night): the rest of the verb surface, measured on winvm through the hand-launched exe. Credential vault on the OS keychain (host-win: CredRead/Write/Delete in the keytar layout, so the Node build's saved logins carry over): credential_set / pup_credentials / credential_delete, background autofill on load (_autoLogin names the live host), pup_login with the submit_reason_required gate, real-keystroke fill, 2FA/captcha wall detection (blocked_on_user), login capture from the page binding (deduped against the vault), the verified-origin-gated reveal behind a consent toast (measured: refused as origin_unverified from a plain caller, nothing echoed); payment cards (pup_card_set / pup_cards / pup_card_delete / pup_fill_card with per-fill consent; brand + last4 only in responses). Wiki: auto-sign from the desktop token (measured: window auto-signed as the box's user), public/authed view flip through the server's ?view=public, wikiView/wikiAuthed/wikiLoginNeeded on every navigate. Annotate: the overlay armed by verb, floaty pencil, owner chip on every document (open, navigate, new tab), capture processing (clip, caption bar, iTXt sidecar, clipboard through ab's desktop_clipboard_set). Recording: tab screencast with frame ack (measured: 19 frames over 6.2 s of an animated page in an occluded window; a static page yields one frame, which is what screencast does), ffmpeg VP9 mux when present else the built-in MJPEG AVI; the Chrome screen-share recorder HUD (pup_recorder_open is reason-gated, pup_screen_record_* status/list); the highFps lease with the lapse relaunch. Extras: screenshot_element, screenshot_full_res (2012x1117 native), pup_download through Browser.setDownloadBehavior + downloadProgress (measured: 40-byte file landed where asked), the 5-minute identity read-back audit (desktop_get_window_identity), launch, deps, install_toolbar, reap_profiles, uninstall. Dashboard: the Hydrogen (ah) proxy with dynamic port discovery (measured on winvm: responder on 47080 pointed at 47084), byte-faithful relays for /updater/status, /ai-threads/run-prompt and its status poll, /api/demo-send with the release gate and the 30 s state wait (measured: a job opened). Consent registry shared by reveal, card fill and raise. The placeholder wiki/annotate/record crates were removed: their code lives as modules in the verbs crate because every one of them needs the shared Ctx and the open/close verbs (highFps relaunch, autofill on load, wiki after-navigate); splitting them out means lifting Ctx and the window verbs into a runtime crate first, which is a Phase 4 refactor, not a stub.
  • Phase 4 (in progress, 2026-09-05 night): the parity gate is BUILT and GREEN. tools/parity.py --target <box> replays the 37-step fixture sequence against the exe on a spare port, pulls the responses back and diffs every one against its golden (missing keys, JSON-type drift, and the same inside output); keys the ab CLI stamps on the way back (_timeoutHint, timeoutSeconds, savedTo, statusVerb) are excluded, extra keys are allowed. Measured on winvm: 37 of 37 match (pup_fetch_url's golden was a CORS failure on the Node build; the Rust build fetched fine, so that one compares scenarios, not shapes). Getting there ported the Node response envelope into dispatch: the verb catalog (rust/verbs/assets/verb-catalog.json, generated from the golden pup_describe minus the dropped desktop_ aliases) stamps hint/related/next/pitfalls, the bug-report invite fires on failures and once per process, the output mirror carries the advisory fields, render verbs run the render probe (404 text, blank, unstyled, stack traces, broken images) and lead the hint with the verdict; the session row carries lastAgentUpdate {tabId, verb, note, ts}, ownerSource, overlay, flash/progress counters; pup_status.loops reports real loop cadences; and the 32 permissive default permissions are granted at launch (they were missing from the Rust build until the gate caught the count). Then the exe packaging and the ship (2026-09-05, late night): tools/rust-build.sh builds the exe and the zip (exe + a spawn.kind exe bridge.json + BRIDGE_VERSION, 2.5 MB); tools/ship.sh builds the Rust package by default (--shell node keeps the Node path) and gained --candidate (everything except the insiders push; the manifest lands on the product page as adom-bridge-puppeteer-manifest-candidate.json for ONE desktop to install) and --insiders-from-candidate. Eight candidates (2.0.401 to 2.0.408) went through ab's real bridge_install path on winvm before insiders saw one; each found something: ab's relay computes success from success and the CLI returns the parsed output alone (so every success now carries both); the Node-era Chrome had no DevToolsActivePort file (recovery now reads the session file's cdpPort); a window whose Chrome is gone must come back (relaunch from the file at boot and from the health loop, twice at most, with _cleanupReport); a killed exe could lose the port for a second (bind retry); panics now land in ~/.adom/bridge-logs/pup-rust-panic.log; the wiki's CSP blocks inline scripts in a pup_render_html document (hint added); a window recovered across an update had lost its owner chip, floaty and login capture (recovered tabs get them back). Measured on 2.0.408 through ab: tools/parity.py --via-ab 37 of 37, tools/ab-smoke.sh all pass (vault, cards, wiki auto-sign and view flip, annotate, recording, recorder HUD, download, dashboard, taskbar identity), exe kill then respawn with the window reattached, Chrome kill then every window relaunched, and the first-run provisioning path (Chrome for Testing cache hidden: readiness fell back to Edge, pup_use cft fetched the build, the next window opened on it). Open on ab's side: adom-bridge#152, ab runs the node dependency install against its bundled seed on recover_not_listening for a kind:exe bridge (10 to 15 s of bridge_installing_deps after an unexpected exe death). Later the same night (2.0.414 to 2.0.419): pup_vault_sync {peer, reason}, a BIDIRECTIONAL adom-you credential union across the user's desktops (one consent toast, newest-wins, pup-to-pup over ab's cross-desktop egress, no AI ever holds a password; measured AdomLapper -> arav-rog: 32 pushed, 24 added, union on both). The serve-from-the-vault doctrine is in the pup-credentials skill so no agent asks the human for a password again. The pup demo tour: tools/pup-demo.py (eight beats through ab's demo kit, recorded with pup's own screen recorder) and the dashboard's Play-the-demo prompt now carry the same script; the first full take on arav-rog ran end to end (Google to its 2FA wall, honestly; UltraLibrarian fully signed in). Take 1 failed the demo-review tile (the mouse arrow parked on the taskbar button for a minute; no progress fill). Take 2 fixed the driver (mouse parked at the screen edge, window born in the background then raised by a REAL click on its taskbar button, which is also the only thing that clears the Windows 11 attention tint) and measured pup-bridge#99: Windows 11 25H2 (build
    1. paints NO taskbar progress for any app, plain COM on Notepad included; the skills say so now. The recorder HUD shows once the last window closes, so the closing line minimizes it and captions top. 2.0.420 (2026-09-06, candidate on arav-rog, smoke pending): pup-bridge#98 runtime data under ADOM_BRIDGE_DATA_DIR (profiles, sessions, screenshots, recordings; legacy dirs moved once by rename, 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). 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). 2.0.425: the taskbar activity contract restored from Node (taskbarActivity progress|flash|off, dead keys refused, attention tint cleared after birth, icon-apply and screenshot progress bars, alert = pulse in progress mode, four-count flash in flash mode). John: "you seemed to have strayed from that"; he had. 2.0.426: the z-order doctrine (Node v1.9.181 / v2.0.26 / v2.0.261) ported: park.rs with birth grace, the user-in-use latch (granted raise, observed foreground, recent input; 10 min memory) and bottom-unless-foreground at every re-park; foreground:true opens honoured; the 2.0.425 birth-time attention clear removed (ab flickers the window to the front for it; Node never activated at birth). 2.0.427: the overlay badge pipeline ported from Node after John's audit (48 px compositor, globe/PDF fallback instead of the Adom mark, the favicon rails and retries, tab and settings triggers, paint signature, dashboard provenance). 2.0.429: the CSP-safe favicon loader (data: before blob:), tiles rendered natively at every ICO size as 32-bit DIBs (Node v2.0.160), and the no-flip birth: hide the newborn, stamp thread tile + AUMID + jump list while hidden, show once. Identity audit demoted to a detector. 2.0.437 (candidate): the final birth order after eight measured candidates: DeleteTab at the handle, park, navigate, stamp, AddTab; never hide a newborn (Chrome queues its activation); focus handback when the user is in a pup window. The attention tint on a background-born button is Windows marking Chrome's denied activation (KiCad in front: tinted; a pup window in front: clean); Node had it too. 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). Next: the runtime-crate split (wiki/annotate/record out of verbs), macOS in host-mac (Kyle), and promotion to public on John's words.

Commissioned by John: "make a version of pup next that is a rust binary like we've recently done for fusion and for altium ... make sure that you did not reimplement anything in ab already does for you ... make lots of crates for the appropriate topic ... all windows stuff in its own area so Kyle can make a mac version of that area."

The 2026-07-31 restructure plan argued against a rewrite. Its reasons were real (the hard-won knowledge in server.js) but its premise has changed: the Node bridge is now 22k lines in one file, the first-run dependency chain (portable Node, 90 MB of node_modules, koffi, keytar, sharp) is what breaks new users, and fusion has proven the exe path end to end with ab (spawn.kind exe, sha-verified zip, os-win / os-mac crate split). The knowledge in server.js is the thing this port carries over. The Node code is the spec, not the enemy.


1. Where pup stands today (measured 2026-09-05)

Metric Value
src/server.js 21,942 lines (21,566 non-blank)
All own JavaScript (src/*.js) 24,501 lines
Toolbar extension (MV3) 706 lines
tools/ (ship, promote, sweeps) 1,695 lines
tests/ 1,016 lines
Own code total about 27,900 lines
Skills (skills/, dev-skills/, publish-skills/) 6,723 lines of markdown
CHANGELOG + docs about 1,800 lines
Runtime dependencies shipped to every box 129 npm packages, 90 MB, plus a portable Node
Verbs in the public contract 55 (bridge.json) plus 30 internal ones (85 dispatch cases)
Top-level functions in server.js 348
catch blocks 1,348 (one every 16 lines)
setTimeout / setInterval 170 / 20
createCDPSession call sites 28 (still no single CDP pipeline)
page.evaluate call sites 88
ab verbs pup already delegates to 26 distinct verbs, 78 call sites
PowerShell invocations pup still runs itself 63
Direct Win32 (koffi / user32) call sites 44
Unique CDP methods pup actually uses 22 (plus 42 puppeteer convenience calls that wrap them)

Since the 2026-07-31 restructure plan, server.js grew from 12,485 to 21,942 lines and the catch count from 525 to 1,348. Restructuring in place never happened. It will not happen inside the Node file; it happens by moving the code into crates with borders the compiler enforces.

2. Goals and non-goals

Goals

  1. One sha-verified Windows exe, zero interpreters, zero node_modules. First run on a fresh PC: ab extracts the zip, spawns adom-pup-bridge.exe --port N, pup fetches Chrome for Testing itself. Nothing else to provision.
  2. The verb contract does not change. Every pup_* verb, every response shape, every _hint, every error code stays. The 11 shipped user skills remain true without edits. The container-side CLI (adom-bridge ... pup_open_window) does not know the port happened.
  3. Nothing pup can ask ab to do is done in pup. Every remaining PowerShell and Win32 call is either an ab verb or lives in exactly one crate (pup-host-win).
  4. Incremental builds stay fast as features land: many small crates by topic, leaf crates that rarely change, one thin binary crate.
  5. Kyle can build the mac version by implementing one crate (pup-host-mac) against a trait the Windows crate already satisfies, in this repo, on this contract.

Non-goals

  • No new features during the port. Feature parity first, then features.
  • No change to ship/promote/insiders tooling except the build step (cargo instead of zip-the-source).
  • No rewrite of the toolbar extension, dashboard HTML, annotate overlay, or newbie-panel. Those are browser-side JavaScript and stay JavaScript, embedded in the exe as static assets.

3. What ab already does: pup stops doing these itself

This is the audit John asked for. Left column is what server.js does with its own PowerShell, koffi or npm dependency today; right column is what replaces it.

pup does today (own code) Replaced by Notes
HKCU AppUserModelId writes: writeAumidIcon, pruneStaleAumids, stripAllPupIdentities (reg.exe + PowerShell, 18 sites) registry_set / registry_get / registry_delete and desktop_register_app_identity / desktop_unregister_app_identity pup wrote IconResource itself because ab left it NULL (measured 129 of 129 keys). That is an ab bug to file, not a reason to keep a registry writer in pup.
Window enumeration and pid mapping: _psProcs, _pidsWithVisibleWindow, enumChromeWidgetWindows, findChromesByProfile (PowerShell + koffi) desktop_list_windows, desktop_enum_windows, desktop_find_window, process_list pup already uses three of these; the PowerShell copies are the older duplicates.
Process kill: _taskkillPids, killBrowserProcess (taskkill) process_kill pup keeps the decision (which pid, why); ab does the kill.
Z-order to bottom without activation: osBottomUnlessForeground, parkSessionWindowDirect, reassertBottomAfterNav (SetWindowPos via PowerShell / koffi) desktop_set_window_state + desktop_set_window_bounds today; ask ab for desktop_send_to_bottom {hwnd} (SWP_NOACTIVATE bottom is pup's core etiquette and belongs in ab's window verbs) Until ab ships it, this is the one Win32 call that stays in pup-host-win.
Foreground and focus: forceActivateHwnd, osRaiseSessionWindow, osGetForegroundWindow desktop_bring_to_front, desktop_get_foreground, desktop_window_show Already partially delegated; the direct koffi fallbacks go.
Taskbar progress, overlay, flash (badgePupTaskbar, flashViaAD, setSessionProgress) desktop_taskbar, desktop_flash_window, desktop_clear_window_attention Already delegated. pup keeps only the badge image composition (its own art).
Jump lists (updateWikiJumplist, refreshAllJumplists) desktop_set_window_jumplist Already delegated.
Window stamping (stampPupIdentity) desktop_set_window_identity Already delegated. readStampedAumids (PowerShell SHGetPropertyStoreForWindow) becomes ask ab: return appId in desktop_list_windows rows.
OS screenshot fallback desktop_screenshot_window Already delegated.
Native clicks, hover, UI automation (rawTrustedClick, pup_assisted_click) desktop_click, desktop_hover, desktop_ui_click, desktop_ui_watch, desktop_ui_events Already delegated.
User idle (osIdleMs) desktop_user_idle_ms Already delegated.
Toasts, captions, consent buttons notify_user, notify_dismiss, notify_response, desktop_caption Already delegated.
The Chrome screen-share recorder: pup_screen_record_* / pup_recorder_* (canonical since 2.0.399, pup-bridge#94), recorder.html, detectFFmpeg, remuxWebmAddDuration Stays in pup as pup-record (John, 2026-09-05: both recorders stay, each does what the other cannot: Chrome gives the OS share picker, system audio + mic, WebM, cross-platform; ab-core gives background no-browser MP4 of one window). Only the desktop_* alias names and prefix claims leave when ab-core's native desktop_record_* lands (adom-bridge#143). Not a copy of ab code: pup inherited this from ab's originally bundled puppeteer bridge, and ab's recording skill still documents it as ab's feature while pup's verb prefix answers it (verified on winvm 2026-09-05). It captures the screen through a Chrome window (getDisplayMedia, MediaRecorder, VP9 webm) and needs ffmpeg on the box to write the duration header, a first-run dependency new users do not have.
Clipboard image + text write (copyCaptureToClipboard, PowerShell STA helper) Ask ab for clipboard_set {image, text} Until then it lives in pup-host-win.
Hydrogen API calls (hydrogenGet/Post) hd_api Already delegated for one route; the rest follow.
Version probe of ab (probeAdVersion) bridge_info Already delegated.
Chrome for Testing download (@puppeteer/browsers) pup's own (pup-browser crate, ureq + zip) bridge.json already declares prewarm.assets: ["chrome-for-testing"]. Ask ab whether it honours that today; if yes pup only resolves the path, if no pup downloads (with progress into pup_readiness as today).
Image work: resize, PNG to ICO, favicon analysis, caption bar (sharp, own ICO encoder) pure-Rust png + image (resize) + own ICO writer No native dependency.
Credential and card vault (keytar) keyring crate (Windows Credential Manager, macOS Keychain, same API) Same storage backend as keytar on Windows, so existing saved logins survive if the service name stays adom-pup.
Node HTTP server, JSON tiny_http + serde_json, the fusion stack

Net effect: after the port the only OS-specific code pup owns is pup-host-win (send-to-bottom and clipboard until ab takes them, plus HWND-from-pid helpers) and it is a few hundred lines, the size of fusion's os-win.

Five ab issues to file before Phase 1 starts (so they can land in parallel):

  1. desktop_send_to_bottom {hwnd} (SetWindowPos HWND_BOTTOM | SWP_NOACTIVATE), or a zorder:"bottom" option on desktop_set_window_state.
  2. clipboard_set {image?: path|b64, text?} multi-format write.
  3. desktop_register_app_identity leaves IconResource NULL in HKCU (measured), so callers write it themselves.
  4. desktop_list_windows rows should carry the stamped appId, so callers do not PowerShell SHGetPropertyStoreForWindow.
  5. Native desktop_record_* (WGC monitor capture + WASAPI loopback to MP4) in ab-core, adom-bridge#143, so the desktop_* names stop routing to pup; pup keeps its Chrome recorder under pup_screen_record_*. Plus one question: does prewarm.assets: ["chrome-for-testing"] actually download CfT today?

4. The crate map

Workspace at rust/ (same shape as fusion). One version for the workspace, in lockstep with BRIDGE_VERSION, bumped by tools/ship.sh.

rust/
  Cargo.toml                 workspace, resolver 2, shared deps, release profile (opt z, lto, strip)
  core/         pup-core     types, settings, error codes, caller identity, the AbClient trait, response envelope + _hint builders
  host/         pup-host     the OS traits ONLY: HostWindows, HostProcess, HostPaths, HostClipboard, HostIdentity. No cfg(windows) anywhere.
  host-win/     pup-host-win the Windows impl (windows-sys). The ONLY crate with Win32 code.
  host-mac/     pup-host-mac Kyle's crate: same traits, todo!() per method naming the Windows counterpart (fusion os-mac pattern)
  cdp/          pup-cdp      the CDP client: one websocket per browser, target/session multiplexing, a per-session command queue, typed wrappers for the 22 methods pup uses, screencast. Replaces the 28 ad-hoc sessions.
  browser/      pup-browser  find installed Chrome/Edge, CfT resolve + download + verify, profile dirs, launch flags (the CfT-first policy, firewall flags, prefs), spawn + devtools port handshake
  session/      pup-session  the window/tab model, ownership + takeover, session files, reconnect/rescan/recovery, health loop. One implementation of recovery, not six.
  identity/     pup-identity thread tiles (name on teal), AUMID naming, badge composition (pure image code), taskbar mode policy. Calls ab via AbClient; no OS calls.
  vault/        pup-vault    credentials + payment cards: keyring storage, Luhn, brand, expiry, autofill selectors. The reveal gate.
  wiki/         pup-wiki     wiki auth from the desktop token, magic-link sign-in, public/authed view flip, URL lint, container-URL lint
  annotate/     pup-annotate capture processing: tight crop, caption bar, iTXt sidecar; overlay JS shipped as an asset
  record/       pup-record   tab recording over CDP screencast to webm (desktop recording is ab's)
  verbs/        pup-verbs    the 85 handlers, one module per family (window, tab, navigate, input, screenshot, dialog, settings, readiness, vault, wiki, annotate, record). Each handler is a function of (ctx, args) -> Response. No HTTP here.
  dashboard/    pup-dashboard the HTTP routes under /dashboard, /health, /status, /identity, the proxy routes to Hydrogen, static assets via include_str! (dashboard.html, welcome.html, recorder.html, newbie-panel.js, the MV3 extension files, icons)
  bridge/       adom-pup-bridge  the binary: arg parsing (--port), bind ADOM_BIND_HOST, tiny_http loop, /command dispatch into pup-verbs, graceful shutdown, log format

Fifteen crates. Why this split and not fewer:

  • Compile time is bounded by the crate you touched plus its dependents. The dependency graph is a tree with pup-core and pup-host at the root and adom-pup-bridge at the tip. Changing a screenshot handler rebuilds pup-verbs and the binary, nothing else. Changing the tile renderer rebuilds pup-identity, pup-verbs, the binary. pup-cdp and pup-browser, the two biggest and most stable crates, almost never rebuild during feature work.
  • Verb families stay in one crate on purpose. They share the context type and the response builders; splitting them further would put the shared context in yet another crate for little gain. If pup-verbs ever passes about 8k lines, split it along family lines at that time.
  • pup-host is a trait crate with no implementation. That is the Kyle boundary. pup-verbs, pup-session, pup-identity depend on pup-host (the traits), never on pup-host-win. Only the binary picks the implementation, exactly like fusion:
[target.'cfg(windows)'.dependencies]
pup-host-win = { path = "../host-win" }
[target.'cfg(target_os = "macos")'.dependencies]
pup-host-mac = { path = "../host-mac" }
  • A clippy lint enforces the border. cargo deny-style check in tools/check-borders.sh: no crate except host-win may mention windows_sys, powershell, HKCU, SetWindowPos; no crate except host-mac may mention osascript or NSWindow. It runs in ship.sh and fails the ship.

The host traits (first cut, from what server.js does today)

pub trait HostWindows {
    fn windows_for_pids(&self, pids: &[u32]) -> Vec<HostWindow>;   // hwnd, pid, title, class, rect, visible
    fn is_alive(&self, hwnd: Hwnd) -> bool;
    fn send_to_bottom_no_activate(&self, hwnd: Hwnd) -> Result<()>; // until ab has desktop_send_to_bottom
    fn tag(&self, hwnd: Hwnd, key: &str, value: u64) -> Result<()>; // SetProp session tag
    fn read_tag(&self, hwnd: Hwnd, key: &str) -> Option<u64>;
    fn work_area(&self, hwnd: Option<Hwnd>) -> Rect;
    fn dpi_scale(&self, hwnd: Hwnd) -> f64;
}
pub trait HostProcess { fn alive(&self, pid: u32) -> bool; fn children(&self, pid: u32) -> Vec<u32>; fn spawn_detached(&self, exe: &Path, args: &[String]) -> Result<u32>; }
pub trait HostPaths   { fn state_dir(&self) -> PathBuf; fn cft_cache_dir(&self) -> PathBuf; fn profile_root(&self) -> PathBuf; fn temp_dir(&self) -> PathBuf; }
pub trait HostClipboard { fn set(&self, image_png: Option<&[u8]>, text: Option<&str>) -> Result<()>; }
pub trait HostIdentity  { fn app_icon_format(&self) -> IconFormat; }  // ico on Windows, icns on mac

Everything in the taskbar family (AUMID, jump lists, overlays, progress, flash, toasts) is NOT in the host trait: it goes through AbClient, so the mac crate does not need to invent taskbar equivalents. Kyle's mac has a Dock; ab's mac side decides what the Dock shows.

5. What each crate carries over from server.js (the mapping)

server.js region (lines) Goes to Size today
18–360 loops, deps install, heavy init, screen hotness pup-core (loops), deleted (deps install has no meaning for an exe) 350
405–1044 sessions, birth rect, timings, progress frames, identity audit pup-session, pup-core (timings) 640
1044–2755 attachTab, popup tracking, crash respawn, session files, reconnect, rescan, recoverSessions pup-session + pup-cdp 1,700
2755–3000 pup cursor, captions, waitForUserReady pup-verbs::input (cursor overlay JS as asset) 250
3000–3700 reaping, profiles, extension enable, branding, permissions, downloads pup-browser 700
3700–4450 settings, display names, url classify, icon paths, port files, launcher pup-core (settings), pup-identity, pup-dashboard (port files) 750
4450–6500 stampPupIdentity, AUMID register, tiles, ICO, nudge, audits, overlay badges, progress, tooltips pup-identity (art + policy), ab for everything else 2,050
6500–7000 wiki auth, magic link, favicon analysis pup-wiki, pup-identity (favicon scoring) 500
7000–8250 applyAppOverlay, annotate overlay JS, floaty, title JS, extension staging, capture processing pup-annotate, assets 1,250
8250–10340 raise, hwnd resolution, reconcile, reclaim, park, highFps, flash, agent activity pup-session (model), pup-host calls, ab calls 2,100
10340–12000 getOrLaunchBrowser, createPageInNewWindow, launchSession, resolveSession, recovery, health tick pup-browser (launch), pup-session (the rest) 1,650
12000–13040 url lint, render probe, notices, sendJSON, session info, dashboard snapshot, hydrogen proxy pup-core (envelope), pup-wiki (lint), pup-dashboard 1,040
13040–14950 DASHBOARD_HTML (inline JS app) pup-dashboard/assets/dashboard.html verbatim 1,900
14950–20080 the 85 verb cases pup-verbs, one module per family 5,100
20080–21080 HTTP server, routes, OPTIONS, proxy adom-pup-bridge main + pup-dashboard 1,000
21080–21940 ffmpeg, recorder window, screen and tab recording pup-record: tab recording and the Chrome screen-share recorder (pup_screen_record_*), both for good 860
chrome.js pup-browser 1,067
credential_vault.js, card_vault.js pup-vault 621
win32.js pup-host-win 180
newbie-panel.js, recorder.html, welcome.html, src/extension/ assets, byte-identical 2,450

Expected Rust size: roughly 14k to 16k lines (fusion went from a Python bridge to 14k lines of Rust for a comparable surface). Half of that is the verb handlers, which translate nearly one to one.

6. Phases

Each phase ships to insiders only as spawn.kind: exe. Public stays on the Node build until Phase 4 passes. Same bridge name, same page, same manifests; only the zip contents change.

Phase 0: freeze the contract (no Rust yet). About two days.

  • tests/baseline.json grows into a golden fixture: for every verb, a recorded request and the response with volatile fields masked. Generated from the Node build on winvm. This is the parity oracle for every later phase.
  • pup_describe output saved as the contract file (docs/CONTRACT-2.0.396.json).
  • File the four ab issues and the prewarm question (section 3).
  • Write the pup-host trait crate and pup-host-mac stub so Kyle can start reading immediately.

Phase 1: skeleton that opens a window. About a week.

  • Workspace, pup-core, pup-host, pup-host-win, pup-cdp, pup-browser, the binary.
  • Verbs: pup_readiness, pup_prewarm, pup_open_window, pup_navigate, pup_screenshot, pup_eval, pup_close_window, pup_list_windows, pup_describe, pup_status.
  • Proof: on a wiped winvm (no Node, no CfT cache), bridge_install of the exe zip, first pup_open_window fetches CfT and opens a background, bottom-ordered, identity-stamped window inside the timing budget the Node build records today.

Phase 2: the window model. About two weeks.

  • pup-session (ownership, session files, reconnect, rescan, crash recovery, health loop, the five-probe escalation from 2.0.392), pup-identity (tiles, badges, modes, audits), pup-dashboard (all routes, assets), the tab and input and dialog verb families.
  • Proof: the 10-window sweep on AdomLapper, CRR and winvm; identity audit clean; bridge restart keeps every window (the 9 to 9 count from 2.0.392).

Phase 3: the rest of the surface. About a week.

  • pup-vault, pup-wiki, pup-annotate, pup-record, credential reveal, cards, login, import-browser-logins, highFps lease, annotate modes and hotkeys, Send-to-AI proxy routes.
  • Proof: golden fixture passes for all 85 verbs; tools/mode-ralph.sh and tests/dashboard_ralph.sh pass unchanged.

Phase 4: parity gate and cutover.

  • Golden fixture 85 of 85; three-box sweep; skills reviewed line by line against the exe (they should need no edits, and any that do are contract drift to fix in the exe, not the skill).
  • John's words promote it. The Node source moves to legacy/ for one release, then deleted.
  • bridge.json: spawn.kind: "exe", entrypoint: "adom-pup-bridge.exe", killImageName set, runtime and dependencies.node removed, languages: ["Rust"].

Total: about five weeks of work to parity, if ab's four asks land in the first two.

6b. The first-run contract (John, 2026-09-05: "massively critical")

What broke on arav-rog on 2026-09-05, measured: ab installed pup's source-only zip and ran npm install; the puppeteer package's postinstall tried to download Chrome for Testing, found a Chrome 148 folder from July with 3 files and no exe, and exited 1. Every one of the 11 logged installs failed the same way, ab backed off after 496 spawn attempts, and the KiCad thread saw only timeouts. pup's own repair code for exactly that folder never ran, because pup never started. The Node fix (2.0.397) is a config file that forbids the postinstall download plus a cache prune at warmup. The Rust port must make this whole class of failure impossible by construction:

  1. No install-time work at all. The zip is the exe plus data files. ab sha-verifies and extracts. There is no npm, no postinstall, no interpreter, nothing that can half-complete.
  2. Chrome for Testing is provisioned by pup, after pup is up and answering. The download is a background task with progress in pup_readiness (bytes, rate, ETA), atomic: download to a temp file, verify size, unzip into a temp folder, rename into place. A folder without an executable is junk by definition and is pruned at every start.
  3. Every wait on something that is not pup's is a structured answer, never a timeout. A missing browser, a download in flight, a firewall dialog, a consent toast, a 2FA wall: each returns stillRunning or blocked_on_user with blockedBy and the verb to poll, inside every ab budget (45 s in-verb cap). The calling AI is never left guessing whether pup is dead or waiting.
  4. The first open works on a box with nothing. No Chrome, no Edge, no cache, standard user, no admin: pup answers chrome_for_testing_installing with progress on the first call and opens the window on a later call, background, identity-stamped, within the measured budget.
  5. Proof is a wiped box, every ship. tools/ship.sh gains a gate that bootstraps a throwaway Windows VM (or winvm with the cache and bridges-cache wiped) from the zip and runs the first-open sequence; the release is refused if that sequence does not pass. This is Phase 1's exit test and it never stops running.

7. Build and ship mechanics

  • Cross-compiled from the container with x86_64-pc-windows-gnu, the target fusion already uses; the toolchain is installed here. No Windows build box in the loop.
  • Dev builds: cargo build -p adom-pup-bridge (incremental, opt-level 1 for deps via [profile.dev.package."*"]). Release: the fusion profile (opt z, lto, strip, codegen-units 1), release builds only in ship.sh.
  • tools/ship.sh gains one step (cargo build --release --target x86_64-pc-windows-gnu) and packs adom-pup-bridge.exe + bridge.json + assets instead of src/. Promote, rollback, insiders manifest and the pkg publish step are untouched.
  • tools/check-borders.sh (section 4) and cargo check --target aarch64-apple-darwin -p pup-host-mac run every ship, so a Windows leak or a broken mac stub fails the ship, not Kyle's afternoon.
  • Manifests are single-zip today (ab wiki #111 caveat), so the exe zip is Windows only. When Kyle's crate compiles, the mac build needs either per-platform manifests in ab or a second page. That is an ab ask for later; it does not block anything here.

8. Risks, stated plainly

  1. The CDP client is the one piece with no Rust equivalent as good as puppeteer. pup uses 22 CDP methods and about 42 puppeteer conveniences (waitForNavigation, frame walking, trusted typing, screencast). chromiumoxide exists but drags in a large async stack and its own opinions about sessions. Plan: a thin pup-cdp on tungstenite, typed for exactly the methods pup uses, with the queue and lifecycle the Node code never had. This is where the rewrite earns its keep and where most of Phase 1's time goes.
  2. The tacit knowledge. FedCM real-cursor sequence, park geometry, DPI, Chrome reparenting on restart, the AUMID destroy/recreate trick, the throttle/highFps tradeoff. Mitigation: the Node file is the spec; every ported function cites its server.js line range in a comment until Phase 4, and the golden fixture catches shape drift.
  3. Trusted typing into React-masked fields, Stripe iframes, cross-origin frames. All CDP, all reproducible, but each is a test to write, not a line to translate.
  4. Time. Five weeks with pup features frozen. Bugs found in the Node build during that window get fixed in Node and ported the same day (a PORT-LOG.md tracks them).

9. Decisions John has already made, and two he has not

Made: Rust, one exe, many crates, host code isolated for Kyle, no ab reimplementation, plan before build.

Open:

  1. Bridge name and page. Plan assumes the same bridge name (adom-bridge-puppeteer), same page (adom/pup-bridge), same verbs, so nothing downstream changes. Say so if you want a new name.
  2. Kyle's mac repo. Once he points at it, the plan gains a section mapping his implementation onto pup-host-mac and the parts of his code that are not host-specific (which should be most of it) get retired in favour of the shared crates.