app
Pup - the AI's own browser
Public Made by Adomby adom
pup gives your AI a real browser on your desktop that it drives completely, in the background, signed in as you: windows that never jump in front of your work, one shared profile that learns your logins, and every window labeled on the taskbar with its AI thread's name.
← Commit history
docs: Rust port plan (plan only, nothing built); 2026-09-05 line counts and the ab-delegation audit
1 file changed
+293
docs/PLAN-rust-port.mdadded+293@@ -0,0 +1,293 @@+# pup in Rust: the port plan++**Status: PLAN ONLY. Nothing is built. Written 2026-09-05 against pup 2.0.396 (public and insiders).**+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. |+| Desktop recording verbs pup re-exports: `desktop_recorder_open/close`, `desktop_record_start/stop/status/list`, `detectFFmpeg`, `remuxWebmAddDuration` | ab's own `desktop_record_window_start`, `desktop_record_screen_start` family | **Dropped from pup.** pup keeps only tab recording (CDP screencast), which ab cannot do. |+| 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`.++**Four 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`.+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, desktop and tab recording | `pup-record` (tab only); desktop recording deleted in favour of ab | 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.++## 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.