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.
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.shenforces the host border. - Phase 1:
rust/builds forx86_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_windowlaunched 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 (
_autoLoginnames 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 insideoutput); 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 intodispatch: 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, theoutputmirror 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.shbuilds the exe and the zip (exe + a spawn.kind exe bridge.json + BRIDGE_VERSION, 2.5 MB);tools/ship.shbuilds the Rust package by default (--shell nodekeeps 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 realbridge_installpath on winvm before insiders saw one; each found something: ab's relay computes success fromsuccessand the CLI returns the parsedoutputalone (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-ab37 of 37,tools/ab-smoke.shall 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- 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-inanchor:truein ab 2.1.49;aumidShortcutmaps onto it (off by default, John's call). 2.0.421: theaumidShortcutrow 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--jumplistmode 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 refusednot_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.rswith 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.
- 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
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
- 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. - 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. - 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). - Incremental builds stay fast as features land: many small crates by topic, leaf crates that rarely change, one thin binary crate.
- 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):
desktop_send_to_bottom {hwnd}(SetWindowPos HWND_BOTTOM | SWP_NOACTIVATE), or azorder:"bottom"option ondesktop_set_window_state.clipboard_set {image?: path|b64, text?}multi-format write.desktop_register_app_identityleavesIconResourceNULL in HKCU (measured), so callers write it themselves.desktop_list_windowsrows should carry the stampedappId, so callers do not PowerShellSHGetPropertyStoreForWindow.- Native
desktop_record_*(WGC monitor capture + WASAPI loopback to MP4) in ab-core, adom-bridge#143, so thedesktop_*names stop routing to pup; pup keeps its Chrome recorder underpup_screen_record_*. Plus one question: doesprewarm.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-coreandpup-hostat the root andadom-pup-bridgeat the tip. Changing a screenshot handler rebuildspup-verbsand the binary, nothing else. Changing the tile renderer rebuildspup-identity,pup-verbs, the binary.pup-cdpandpup-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-verbsever passes about 8k lines, split it along family lines at that time. pup-hostis a trait crate with no implementation. That is the Kyle boundary.pup-verbs,pup-session,pup-identitydepend onpup-host(the traits), never onpup-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 intools/check-borders.sh: no crate excepthost-winmay mentionwindows_sys,powershell,HKCU,SetWindowPos; no crate excepthost-macmay mentionosascriptorNSWindow. It runs inship.shand 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.jsongrows 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_describeoutput 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-hosttrait crate andpup-host-macstub 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_installof the exe zip, firstpup_open_windowfetches 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.shandtests/dashboard_ralph.shpass 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",killImageNameset,runtimeanddependencies.noderemoved,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:
- 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.
- 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. - 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
stillRunningorblocked_on_userwithblockedByand 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. - The first open works on a box with nothing. No Chrome, no Edge, no cache, standard user, no
admin: pup answers
chrome_for_testing_installingwith progress on the first call and opens the window on a later call, background, identity-stamped, within the measured budget. - Proof is a wiped box, every ship.
tools/ship.shgains 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 inship.sh. tools/ship.shgains one step (cargo build --release --target x86_64-pc-windows-gnu) and packsadom-pup-bridge.exe + bridge.json + assetsinstead ofsrc/. Promote, rollback, insiders manifest and the pkg publish step are untouched.tools/check-borders.sh(section 4) andcargo check --target aarch64-apple-darwin -p pup-host-macrun 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
- 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).
chromiumoxideexists but drags in a large async stack and its own opinions about sessions. Plan: a thinpup-cdpontungstenite, 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. - 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.
- 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.
- 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.mdtracks 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:
- 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. - Kyle's mac repo. Once he points at it, the plan gains a section mapping his implementation
onto
pup-host-macand the parts of his code that are not host-specific (which should be most of it) get retired in favour of the shared crates.
# 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
26200) 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:
```toml
[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)**
```rust
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.