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
Publish 2.0.553
4 files changed
+127−133
SKILL.md+113−119@@ -1,10 +1,10 @@ --- name: pup user-invocable: true-description: "Drive a browser on the user's desktop from the cloud — the Puppeteer 'pup' bridge (pup_* verbs via the adom-bridge-cli CLI). pup drives Chrome for Testing (the automation build: no automation banner, deterministic version, toolbar extension rides the launch — see docs/WHY-CFT.md; installed Chrome/Edge are fallbacks) in a pup-managed DURABLE profile (logins/localStorage persist and are shared across pup windows; {isolated:true} for a throwaway), driven BY DEFAULT through pup's own extension channel with NO --remote-debugging-port (lane:'cdp' is the opt-in for CDP-only features), in the BACKGROUND so it never disturbs the user; NOT the user's real signed-in profile (that's the Adom extension's nbrowser_* verbs), NOT headless (a real rendered window, so screenshots/recording are true-to-life). Chrome for Testing only as a last resort. Open/close windows + tabs, navigate, screenshot, eval JS, record. This is the START-HERE skill: the mental model + when-to-use + the skill map; deeper topics route to the sub-skills. For 'is it ready' use pup_readiness (not pup_status). Trigger words: pup, puppeteer, browser window, open in pup, open my app in pup, browser screenshot, browser eval, browser reload, visual debug, headful chrome, pup_open_window, pup_screenshot, pup_readiness, pup_prewarm, chrome for testing, open in browser, open my web app, show my app in the browser, preview my app, open the app I just built, view my app, open localhost, show my dev server, pup_login, log into a site in pup, autofill password, saved login, durable profile, isolated window."+description: "Drive a browser on the user's desktop from the cloud: the Puppeteer 'pup' bridge (pup_* verbs via the adom-bridge-cli CLI). pup drives Chrome for Testing (the automation build: no automation banner, deterministic version, toolbar extension rides the launch, see docs/WHY-CFT.md; installed Chrome/Edge are fallbacks) in a pup-managed DURABLE profile (logins/localStorage persist and are shared across pup windows; {isolated:true} for a throwaway), driven BY DEFAULT through pup's own extension channel with NO --remote-debugging-port (lane:'cdp' is the opt-in for CDP-only features), in the BACKGROUND so it never disturbs the user; NOT the user's real signed-in profile (that's the Adom extension's nbrowser_* verbs), NOT headless (a real rendered window, so screenshots/recording are true-to-life). Chrome for Testing only as a last resort. Open/close windows + tabs, navigate, screenshot, eval JS, record. This is the START-HERE skill: the mental model + when-to-use + the skill map; deeper topics route to the sub-skills. For 'is it ready' use pup_readiness (not pup_status). Trigger words: pup, puppeteer, browser window, open in pup, open my app in pup, browser screenshot, browser eval, browser reload, visual debug, headful chrome, pup_open_window, pup_screenshot, pup_readiness, pup_prewarm, chrome for testing, open in browser, open my web app, show my app in the browser, preview my app, open the app I just built, view my app, open localhost, show my dev server, pup_login, log into a site in pup, autofill password, saved login, durable profile, isolated window." --- -# Pup — browser control on the user's desktop (start here)+# Pup: browser control on the user's desktop (start here) Drive a browser on the user's desktop from a cloud container via the `adom-bridge-cli` CLI. Every verb is a **Bash command** (NOT an MCP tool):@@ -49,7 +49,7 @@ dropdown. Backed by the OS keychain (encrypted at rest); passwords are never log decision (no second toast), or watch `pup_readiness {}` `active[]`. `pup_fill_card` behaves the same; `pup_assisted_click` and a `pup_login` submit that lands on a 2FA/captcha wall answer `blocked_on_user` with `stillRunning: false` (the human must act, then you re-issue the verb).- (v2.0.374: the gate rides ab's forge-proof `X-Adom-Verified-Origin` header — only a VERIFIED+ (v2.0.374: the gate rides ab's forge-proof `X-Adom-Verified-Origin` header, only a VERIFIED local transport passes; a missing/bridge origin is refused with `origin_unverified`.) ## Payment cards: pup can finish a checkout (v2.0.374)@@ -57,14 +57,14 @@ dropdown. Backed by the OS keychain (encrypted at rest); passwords are never log pup keeps a **card vault** beside the credential vault, so the AI can complete a purchase end-to-end instead of stalling at the payment step: -- `pup_card_set {nickname, number, expiry, cvv, name}` stores a card in the OS keychain — ask the+- `pup_card_set {nickname, number, expiry, cvv, name}` stores a card in the OS keychain, ask the USER for the details, never harvest them from a page. `pup_cards {}` lists nickname, brand, last4 and expiry only; `pup_card_delete {nickname}` removes one. **No verb ever returns the number or CVV.** - `pup_fill_card {nickname, merchant, amount}` fills the card into the checkout in your window WITHOUT the digits ever reaching you: pup types them into the card fields itself (walking payment-processor iframes like Stripe) and returns booleans. **Every fill is approved by the- user in the moment** on a desktop toast naming the merchant, amount and last4 — pass+ user in the moment** on a desktop toast naming the merchant, amount and last4, pass `merchant` + `amount` so that toast describes the real charge. Then screenshot, verify, and submit. Works from cloud threads: the human click is the authorization. @@ -73,31 +73,29 @@ end-to-end instead of stalling at the payment step: You never need an hwnd, `desktop_find_window`, or `desktop_screenshot_window` for a pup window. pup keys every window to its owning `--ai-thread` name and resolves ids for you: -- **Open**: `pup_open_window {"url":"..."}` — omit sessionId. Your thread gets its OWN window+- **Open**: `pup_open_window {"url":"..."}`, omit sessionId. Your thread gets its OWN window (first open creates it, later opens become tabs in it). It opens full-work-area sized with a 10px inset on every edge (the visible signature of a deliberately placed window; never the OS-maximized state, which raises), in the background, on screen and clickable from birth.-- **Come back to it, from any later turn**: just call verbs with NO ids — `pup_screenshot {}`,+- **Come back to it, from any later turn**: just call verbs with NO ids, `pup_screenshot {}`, `pup_reload {}`, `pup_navigate {"url":"..."}` all resolve to your thread's window.-- **See everything you own**: `pup_my_windows {}` — windows, tabs, urls, titles, state, plus+- **See everything you own**: `pup_my_windows {}`, windows, tabs, urls, titles, state, plus the ready-to-run next steps. Start here whenever you are unsure what you have open.-- **Address a thread by name**: any targeting verb accepts `"ai-thread"` instead of an id —- the CLI's `--ai-thread` flag, same spelling (e.g. `pup_screenshot {"ai-thread":"sxsw"}`).-- **Several windows on one thread?** Label them with `window` (e.g. `{"window":"mytask-web"}`) —- the ONLY time you need an explicit id. `sessionId` is the legacy alias for the same label and+- **Address a thread by name**: any targeting verb accepts `"ai-thread"` instead of an id, the CLI's `--ai-thread` flag, same spelling (e.g. `pup_screenshot {"ai-thread":"sxsw"}`).+- **Several windows on one thread?** Label them with `window` (e.g. `{"window":"mytask-web"}`), the ONLY time you need an explicit id. `sessionId` is the legacy alias for the same label and still works everywhere; responses carry both (`window` + `aiThread` alongside `sessionId`). Prefer the zero-id / thread-name forms above whenever your thread runs one window. - Screenshots: `pup_screenshot {}` (Claude-safe file path comes back). Raising onto the user's screen: `pup_raise_os_window {"foregroundReason":"..."}` (gated). Every open/navigate response- carries `_next` repeating these — trust the hints over hand-rolling desktop verbs.+ carries `_next` repeating these, trust the hints over hand-rolling desktop verbs. ## THE CORE GOAL (John's words, so everyone truly understands it) -## "Open my web app in the browser" — the #1 use case, and its one trap+## "Open my web app in the browser", the #1 use case, and its one trap When the user asks to see an app they just built, pup IS the right surface (a real browser window on their desktop that you can keep driving: reload after fixes, screenshot, eval). The ONE trap: an app-running in a CLOUD container is NOT reachable from the desktop as `localhost:<port>` — pup's Chrome+running in a CLOUD container is NOT reachable from the desktop as `localhost:<port>`, pup's Chrome runs on the DESKTOP. Use the container's public proxy URL instead: ```@@ -107,17 +105,17 @@ https://{container-slug}.adom.cloud/proxy/{port}/ Open it background-first (`foreground:false` + the taskbar flash signals the user), give the window your thread's sessionId, and re-use the SAME window on every subsequent "check it now" (navigate / reload, don't spawn new windows). Under Hydrogen (local container) `http://localhost:{port}` IS-correct — the trap is cloud-only. pup lints obvious container-local URLs and will warn you.+correct, the trap is cloud-only. pup lints obvious container-local URLs and will warn you. **Vocabulary + routing (teach the user the words):** call this surface **pup** in conversation ("I opened it in pup") so users learn the name for the AI-driven browser. When the user wants the-page in THEIR OWN browser — their logins, their tabs, a hand-off with no AI driving — that is+page in THEIR OWN browser, their logins, their tabs, a hand-off with no AI driving, that is **nb** (native browser), a different surface: say so and use nb instead. Displaying inside a Hydrogen panel next to the editor is **wv**. When in doubt: driving/testing → pup, handing to the human → nb, docked preview → wv. **Platform qualifier:** pup requires Bridge (ab). Under **Hydrogen (h)** ab is bundled, so pup is-always available. On **Hydrogen Cloud (hc)** ab may NOT be connected — check `adom-bridge-cli ping`+always available. On **Hydrogen Cloud (hc)** ab may NOT be connected, check `adom-bridge-cli ping` first; if there's no desktop, show the app in a **wv** panel (the proxy URL works there too) and offer the Bridge install for real-browser windows. @@ -127,13 +125,12 @@ assistant in the world**. The engine of that goal is the shared durable **adom-y - **Surf as adom-you by default.** Every pup window on adom-you contributes every login it makes to the shared credential store. The more windows surf as adom-you, the better the username/password database gets, and over time pup can automatically sign in to every website it has ever seen.-- **Isolation is the exception, not a style choice — and it is REASON-GATED (v2.0.101).**+- **Isolation is the exception, not a style choice, and it is REASON-GATED (v2.0.101).** `isolated:true` without an `isolationReason` (min 10 chars: why this window must NOT be adom-you) is refused with `isolation_reason_required`. Legit reasons: testing logged-out views, acting as a different user, untrusted pages. `webSecurity:false` carries its own built-in reason. An isolated window's logins are thrown- away: they build nothing. (`wikiView:"public"` no longer isolates at all as of 2.0.132 —- the wiki's server-side `?view=public` param renders the anonymous view in the same window.)+ away: they build nothing. (`wikiView:"public"` no longer isolates at all as of 2.0.132, the wiki's server-side `?view=public` param renders the anonymous view in the same window.) - A non-adom-you window shows the ANONYMOUS identity tile in the toolbar (the pup four-dot mark on a dark disc, where signed-in windows show the user's avatar) so a not-you window is visible at a glance. The dashboard additionally shows a "surfing as adom-you" chip per window: if your@@ -162,38 +159,38 @@ credential is persisted, not just used once. - The ONE exception: an isolated/throwaway window (`isolated:true`) builds nothing, so saving there is pointless, skip it. -## The mental model — what pup actually is (read this once)+## The mental model: what pup actually is (read this once) pup is a small **Node bridge** that Bridge spawns on the user's machine. When you open a window it does NOT attach to the user's running browser and does NOT run headless. It: -1. **Launches a brand-new browser process — Chrome for Testing by policy** (2026-08-10, pup 2.0.53+;+1. **Launches a brand-new browser process, Chrome for Testing by policy** (2026-08-10, pup 2.0.53+; the full dated record is `docs/WHY-CFT.md`). CfT is the build Google ships FOR automation: no "controlled by automated test software" banner flicker, and it still honors `--load-extension`,- which branded Chrome 137+ removed — the pup toolbar family ONLY loads on CfT. Installed Chrome,+ which branded Chrome 137+ removed, the pup toolbar family ONLY loads on CfT. Installed Chrome, then Edge, remain the spawn-verified FALLBACK ladder (used while CfT is still downloading, or if the cached CfT breaks), so an open never hard-fails; but do not describe pup as "driving the- installed browser" — that was the mid-2026 policy and it was reversed. Check `pup_readiness`+ installed browser", that was the mid-2026 policy and it was reversed. Check `pup_readiness` for which browser a box actually runs. Either way pup uses a *browser binary*, never the user's *profile*. 2. **…with a pup-managed `--user-data-dir`.** By DEFAULT there is **no** `--remote-debugging-port` at all: pup drives the window through its own MV3 extension channel, because that port is the loudest automation signal a site can read. `pup_open_window {lane:"cdp"}` opts into a CDP port when you need a CDP-only feature (iframe targeting, trusted input, annotate, recording). The CDP lane is driven over the Chrome DevTools Protocol. This is a profile **pup owns and persists**, never the user's own signed-in OS Chrome/Edge profile. By default every window shares ONE durable profile (`adom-you`), so logins,- cookies and localStorage **accumulate and are shared across all pup windows** — sign into a site once and+ cookies and localStorage **accumulate and are shared across all pup windows**, sign into a site once and every pup window is signed in; app state (localStorage) survives between windows. Sharing a profile does NOT merge windows: each window is still its own OS window with its own taskbar identity. For a private, throwaway window (a fresh-login test, or acting as a different user) pass `{isolated:true}`, or pass an explicit `{profile:"<name>"}` for a separate durable jar. (Need the user's REAL signed-in profile/SSO?- That's the **Adom extension** `nbrowser_*` — see below.)-3. **Renders a REAL window, but keeps it in the BACKGROUND.** It is *not* headless — a real GPU-composited+ That's the **Adom extension** `nbrowser_*`, see below.)+3. **Renders a REAL window, but keeps it in the BACKGROUND.** It is *not* headless, a real GPU-composited window, so `pup_screenshot` / recording are true-to-life and canvas/WebGL/media render correctly. But pup immediately z-orders it to the **bottom** (Windows: Win32 `SetWindowPos` with- `SWP_NOACTIVATE`; macOS: the browser process is **hidden**, the Cmd+H state — same contract), so it+ `SWP_NOACTIVATE`; macOS: the browser process is **hidden**, the Cmd+H state, same contract), so it renders **without stealing focus and without covering the user's active window**. You drive and screenshot it fully while it stays hidden. `foreground:true` is the ONLY thing that shows it. 4. **Spawn-verifies + falls through.** The effective order is **pinned/cached default first** (CfT, per policy) **→ installed Chrome → Edge → cached CfT**; pup actually launches each and, if one- won't start (corrupt install), falls to the next — so an open never hard-fails on a broken+ won't start (corrupt install), falls to the next, so an open never hard-fails on a broken browser. The one that launches is **cached as the default** and goes first next time. On a box where CfT is not yet cached, `pup_prewarm` fetches it (~150 MB, once) so the first real open is instant.@@ -202,25 +199,24 @@ does NOT attach to the user's running browser and does NOT run headless. It: `sessionId`) is needed only when one thread deliberately runs several windows. Keep this model in mind and every verb makes sense: it's a pup-managed, persistent, backgrounded browser-(the machine's own Chrome/Edge binary, a profile pup owns) driven by CDP — durable and shared by default,+(the machine's own Chrome/Edge binary, a profile pup owns) driven by CDP, durable and shared by default, isolated only when you ask. -## Power & rendering — throttling is DEFAULT and correct; `highFps` is the leased override+## Power & rendering, throttling is DEFAULT and correct; `highFps` is the leased override **Background pup windows are THROTTLED on purpose, to protect the user's CPU + battery.** Chrome-pauses/slows rendering on occluded/unfocused windows, and pup relies on that. This is deliberate —-issue #14 (James): three idle pup windows with the anti-throttle flags on held his laptop out of+pauses/slows rendering on occluded/unfocused windows, and pup relies on that. This is deliberate, issue #14 (James): three idle pup windows with the anti-throttle flags on held his laptop out of deep idle overnight and FLATTENED his battery. So the default is: a background window renders slowly or not at all until it's focused. -**Consequence you MUST know:** a healthy background window can LOOK frozen — Google Slides stops+**Consequence you MUST know:** a healthy background window can LOOK frozen, Google Slides stops animating, a video pauses, the cursor doesn't blink. That is THROTTLING, not a hang. It wakes the instant the window is focused (`visibilityState` flips to `visible`). Before ever calling a window "frozen", check: is it just backgrounded? (on-screen + CDP-responsive + `readyState:complete` = it's throttled, not broken.) -**When you genuinely need a background window to keep rendering** — recording it, or a live view the-user watches WITHOUT focusing it (a dashboard, a running demo) — upgrade that ONE window:+**When you genuinely need a background window to keep rendering**, recording it, or a live view the+user watches WITHOUT focusing it (a dashboard, a running demo), upgrade that ONE window: `pup_open_window {highFps:true}` (or the recorder path sets it). This applies the anti-throttle flags so the occluded window renders full-rate. It is **LEASED**: 20 min by default, 120 max, and it **auto-downgrades when idle / when the lease lapses**, so it can NEVER drain the battery the way@@ -228,9 +224,9 @@ issue #14 did. Renew a still-needed lease with `pup_highfps_extend {sessionId, m **The balance, in one line:** throttled by default (battery), `highFps` only for a window that must keep rendering while unfocused, and the lease makes that self-limiting. Do NOT reach for `highFps` to-"fix" a window that merely LOOKS frozen — just focus it (or tell the user to click it).+"fix" a window that merely LOOKS frozen, just focus it (or tell the user to click it). -## A native dialog FREEZES EVERY pup window — this is the #1 thing that looks like "pup is dead"+## A native dialog FREEZES EVERY pup window, this is the #1 thing that looks like "pup is dead" If a page raises `alert()`, `confirm()`, `prompt()`, or the **"Reload site? Changes you made may not be saved."** bar, that modal blocks the BROWSER, not just its own window. pup's windows share one@@ -265,7 +261,7 @@ state. On a form-heavy or editor-like page, check before you navigate rather tha re-adopts its windows through a restart. That costs you only the window that was stuck; the others come back at their URLs. -## Under heavy load or a crash, pup self-heals — do NOT panic-kill or declare it dead+## Under heavy load or a crash: pup self-heals, do NOT panic-kill or declare it dead pup is built to stay usable when the shared Chrome is under heavy load, and to recover on its own if Chrome ever crashes. Read these signals correctly instead of assuming the worst:@@ -273,12 +269,12 @@ Chrome ever crashes. Read these signals correctly instead of assuming the worst: - **`_degraded` on `pup_list_windows` / `pup_status`.** These verbs ALWAYS return the full window list, fast, even when a window is too busy (Chrome under heavy load on a heavy SPA) to answer a live read. A busy window comes back marked `degraded` with its LAST-KNOWN url/title. `_degraded`- means "that window is busy, NOT crashed — retry shortly for live values." It does NOT mean pup is+ means "that window is busy, NOT crashed, retry shortly for live values." It does NOT mean pup is broken. (Before v1.9.172 these verbs could hang and return null under load, which read as "pup is- frozen" — that can no longer happen.)+ frozen", that can no longer happen.) - **A window vanished / a `_cleanupReport` says a window crashed.** If the shared Chrome crashes, pup's- health loop (15s) detects it and RELAUNCHES every affected window on its own — signed in, in the- background, at its last URL — then reports it via `_cleanupReport` ("pup auto-recovered your …+ health loop (15s) detects it and RELAUNCHES every affected window on its own, signed in, in the+ background, at its last URL, then reports it via `_cleanupReport` ("pup auto-recovered your … window after a Chrome crash"). You do NOT need to kill Chrome, clean the profile, or reopen windows by hand. Wait ~15-30s and re-list; the windows come back. Only if a page crashes Chrome REPEATEDLY does pup stop retrying it (after 2 attempts) and tell the user the page itself looks unstable.@@ -286,27 +282,27 @@ Chrome ever crashes. Read these signals correctly instead of assuming the worst: concurrent; a slow/jammed window does not block operations on OTHER windows, and never blocks list/status. If `pup_eval` on window A times out, window B still works and the bridge is fine. - **Genuinely frozen vs throttled vs busy:** a background window that looks static is almost always- just THROTTLED (see Power & rendering above) — focus it and it wakes. A window that is BUSY shows up+ just THROTTLED (see Power & rendering above), focus it and it wakes. A window that is BUSY shows up as `degraded`. A window that CRASHED gets auto-relaunched. None of these is "pup is dead." ## ⛔ HARD RULE: NEVER send NATIVE OS input without warning the user FIRST (John, 2026-07-29) **Native OS input (`desktop_click`, `desktop_type`, `desktop_press_key`, `desktop_hover`) does NOT go to a window. It goes to WHATEVER WINDOW HAS OS FOCUS AT THAT INSTANT.** If the user has alt-tabbed-away — and this user runs many AI threads at once and alt-tabs away from a foregrounded window almost-immediately, every time — your keystrokes land in THEIR document, THEIR terminal, THEIR editor.+away, and this user runs many AI threads at once and alt-tabs away from a foregrounded window almost+immediately, every time, your keystrokes land in THEIR document, THEIR terminal, THEIR editor. This happened: an agent foregrounded a pup window, typed a username with `desktop_type` without ever telling the user, the user had already alt-tabbed back to their own work, and the text went into their-window. The agent then compounded it by concluding "Windows failed to foreground the window" — blaming+window. The agent then compounded it by concluding "Windows failed to foreground the window", blaming the OS for what was really "the human took their focus back, because nobody asked them not to." **NEVER do that again. Before ANY native input, in this order:** -1. **`notify_user` TOAST** — tell them what you are about to do and that you need the window.-2. **`desktop_caption`** — on-screen, so it is visible while they look at the window.+1. **`notify_user` TOAST**, tell them what you are about to do and that you need the window.+2. **`desktop_caption`**, on-screen, so it is visible while they look at the window. 3. **Say explicitly: "please don't type or click until I say done."** They cannot know otherwise.-4. **WAIT for them to be ready** — `waitForUserReady` (poll `osGetForegroundWindow()` vs the session+4. **WAIT for them to be ready**, `waitForUserReady` (poll `osGetForegroundWindow()` vs the session hwnd until it is stable). NEVER dispatch while the foreground is not your window. 5. **Re-verify foreground IMMEDIATELY before EACH native event**, not once at the start. Focus can change between two events, and a half-typed password in the wrong window is a real harm.@@ -316,7 +312,7 @@ the OS for what was really "the human took their focus back, because nobody aske raise-then-click. If you need native typing, follow the same protocol around it. **Corollaries that cost this project real time:**-- CDP input (`pup_click`, `pup_type`) needs NO foreground and disturbs nobody — **always+- CDP input (`pup_click`, `pup_type`) needs NO foreground and disturbs nobody, **always prefer it**. Reach for native input ONLY when the target genuinely cannot be driven any other way (a cross-origin FedCM chooser, native OS chrome, a site that rejects synthetic events). - Native input returning `ok:true` means "the event was dispatched to the OS", **NOT** "it reached@@ -326,8 +322,8 @@ raise-then-click. If you need native typing, follow the same protocol around it. ## Standing rule: TOAST THE USER before foregrounding (John, repeatedly) -NEVER put a window on the user's screen silently. Any time you FOREGROUND a pup window — a demo, a-"show me", a reveal, a real-cursor click that needs the window up — send an AD toast FIRST (or let+NEVER put a window on the user's screen silently. Any time you FOREGROUND a pup window, a demo, a+"show me", a reveal, a real-cursor click that needs the window up, send an AD toast FIRST (or let pup's auto-toast do it, v1.9.166+) so the user's attention is drawn to what just appeared. A window that pops up with no heads-up reads as the agent hijacking the screen. pup now auto-fires `notify_user` on every granted foreground, but you should still narrate WHY in the reason. For a@@ -335,9 +331,9 @@ background nudge (not a full foreground), use `pup_alert_window` (taskbar flash) **If you foreground in order to CLICK something pup's CDP click can't drive** (a native OS dialog, or a cross-origin FedCM "Sign in with Google" account chooser that rejects in-page clicks), use-**`pup_assisted_click`** — never raise-then-click by hand. It captions the reason + toasts first,+**`pup_assisted_click`**, never raise-then-click by hand. It captions the reason + toasts first, foregrounds, then WAITS and detects if the user alt-tabbed away (holding, captioning "Adom is waiting"-until they're back — it clicks NOTHING while they're not ready), then does the tight real-cursor+until they're back, it clicks NOTHING while they're not ready), then does the tight real-cursor hover→click and narrates each step. This is the protocol so pup never foregrounds-and-acts while the user is not ready. @@ -394,29 +390,29 @@ everything else. **When it will NOT help.** A site that denies a profile with no established trust denies every client, including the user's real Chrome driven by the real extension (analog.com does exactly this on a cold-profile). That is a human-visit problem, not a driver problem — surface it and let the human load it once.+profile). That is a human-visit problem, not a driver problem, surface it and let the human load it once. -## When pup — vs the Adom extension (`nbrowser_*`)+## When pup: vs the Adom extension (`nbrowser_*`) | The task | Tool | |---|---| | Screenshot / scrape / test / demo a page; drive an app you're building; anything that does NOT need the user's logins | **pup (this)** |-| Log into a site, use saved passwords, fill a form on a signed-in page, download behind a login, sail past captchas | **Adom browser extension** (`nbrowser_*`, the `adom-browser-extension` skill) — drives their REAL signed-in Chrome/Edge |+| Log into a site, use saved passwords, fill a form on a signed-in page, download behind a login, sail past captchas | **Adom browser extension** (`nbrowser_*`, the `adom-browser-extension` skill), drives their REAL signed-in Chrome/Edge | -pup drives the machine's browser binary in a profile **pup manages** — durable and shared across pup windows,+pup drives the machine's browser binary in a profile **pup manages**, durable and shared across pup windows, but never the user's own signed-in OS Chrome/Edge profile, so it can't borrow logins the user already has there. pup accumulates its OWN logins over time (sign in once inside pup, it sticks). The extension is the counterpart when you need the user's REAL profile/SSO directly. ## Logins & credentials (the durable profile pays off here) -- Sign into a site ONCE in any pup window and the durable profile keeps it — every later window+- Sign into a site ONCE in any pup window and the durable profile keeps it, every later window (any thread) is already signed in. Google/wiki proven; works for any cookie-session site. - `pup_login {sessionId, username?, password?, submit?, save?}` fills an HTML login form in the BACKGROUND (no Chrome dropdown, no foregrounding, React/Vue-safe). With `save:true` the cred is stored (OS keychain via credential_set) and next time a bare `pup_login {sessionId, submit:true}` signs in from the vault.-- Vault-matched login pages AUTO-FILL on page load (fill only, never auto-submit) — response+- Vault-matched login pages AUTO-FILL on page load (fill only, never auto-submit), response carries `_autoLogin`. Off switch: `pup_configure {autoLogin:"off"}`. - Chrome's own "Save password?" bubble works in pup windows (the durable profile enables the password manager), so manual logins accumulate too.@@ -424,7 +420,7 @@ counterpart when you need the user's REAL profile/SSO directly. on open. Need cross-origin eval for dev? `{webSecurity:false}` (forces isolation, keeps the credentialed profile safe). -## Skill map — this is a pack+## Skill map: this is a pack | Skill | Read it for | |---|---|@@ -432,72 +428,72 @@ counterpart when you need the user's REAL profile/SSO directly. | **`pup-windows-sessions-tabs`** | sessionIds + naming, **window ownership** (don't steal another AI thread's window), **background vs foreground** rules, **many tabs in one window**, list/switch/close, verifying what you opened | | **`pup-screenshots-recording`** | Screenshots (full-page, full-res), reading the shot, **recording** a window vs the whole desktop, framing a window for a clean shot without foregrounding | | **`pup-browsers-and-chrome`** | The browser detection/CDP-launch details, `pup_use` to pin a browser, **installing Chrome / Chrome-for-Testing** (incl. the UAC-notify flow), **readiness**, the cold-start error table, Node provisioning, the shell-approval gate |-| **`pup-adom-wiki`** | Driving + verifying **`wiki.adom.inc`** pages in pup (show/screenshot/verify a published page; the login situation — pup views the wiki logged out for now) |-| **`pup-why-cft`** | WHY the driving browser is Chrome for Testing — the five dated reasons, the trade-offs, and what a "Google Chrome for Testing" window title means. Read before proposing native-first or answering "what is this browser?" |+| **`pup-adom-wiki`** | Driving + verifying **`wiki.adom.inc`** pages in pup (show/screenshot/verify a published page; the login situation, pup views the wiki logged out for now) |+| **`pup-why-cft`** | WHY the driving browser is Chrome for Testing, the five dated reasons, the trade-offs, and what a "Google Chrome for Testing" window title means. Read before proposing native-first or answering "what is this browser?" | *(Maintaining the bridge itself? The `pup-bridge-dev` + `pup-bridge-publish` skills are source-only in the-repo — not shipped here.)*+repo, not shipped here.)* -## Quick start — the 90% path+## Quick start: the 90% path ```bash-# Open in the BACKGROUND (automatic — never disturbs the user), then drive it invisibly.+# Open in the BACKGROUND (automatic, never disturbs the user), then drive it invisibly. # Zero-id addressing: every call resolves your --ai-thread name to YOUR thread's own window. adom-bridge-cli --ai-thread "<your thread>" pup_open_window '{"url":"http://localhost:3000"}' adom-bridge-cli --ai-thread "<your thread>" pup_eval '{"expr":"document.title"}' # verify it really loaded adom-bridge-cli --ai-thread "<your thread>" pup_screenshot '{}' # true-to-life PNG ``` -- **Your `--ai-thread` name IS the owner** — pup owns every window by the ai-thread that opened it, so- just always pass `--ai-thread "<your conversation name>"` (you already must — see below) and the window+- **Your `--ai-thread` name IS the owner**, pup owns every window by the ai-thread that opened it, so+ just always pass `--ai-thread "<your conversation name>"` (you already must, see below) and the window is yours; no separate `owner` arg needed (it's a legacy alias). A `pup_open_window` with no ai-thread (and no `owner`) is **refused** with `caller_identity_required`. Task-prefix your `sessionId` (it's the window's KEY, never its identity) so two of your own windows don't collide. See `pup-windows-sessions-tabs`.-- **`ok:true` means navigation *started*, not that content rendered** — sanity-check with `pup_eval`+- **`ok:true` means navigation *started*, not that content rendered**, sanity-check with `pup_eval` on `document.title` (404s / login walls change it) or a screenshot. - **On a fresh PC the first open may include the one-time CfT fetch** (~150 MB; `pup_prewarm` does it ahead of time, and installed Chrome/Edge serve as the interim fallback so nothing blocks). The rare "no browser at all" / Node-provisioning / disk cases are all in `pup-browsers-and-chrome`. -## macOS differences (v1.9.74+) — same verbs, mac-native mechanics+## macOS differences (v1.9.74+), same verbs, mac-native mechanics Every `pup_*` verb works on macOS; only the OS-presentation layer differs: - **Background = hidden app.** A background open launches the session's browser process hidden- (the Cmd+H state) — truly invisible, still fully drivable/screenshottable/recordable.+ (the Cmd+H state), truly invisible, still fully drivable/screenshottable/recordable. `pup_lower_os_window` re-hides; `pup_raise_os_window` (with `foregroundReason`) unhides + activates. One session = one browser process, so a shared-profile window group hides/shows together. - **`pup_alert_window` = un-hide (not focused) + a native toast** whose click focuses the- window — macOS has no taskbar to flash. `{stop:true}` dismisses the toast.-- **No taskbar identity on mac** — the per-window icons/badges/jump-lists below are+ window, macOS has no taskbar to flash. `{stop:true}` dismisses the toast.+- **No taskbar identity on mac**, the per-window icons/badges/jump-lists below are Windows-only; on mac the window title's `(session: <id>)` suffix is the identity.-- **Recording** needs ffmpeg (`brew install ffmpeg` — the error hint says so; re-detects+- **Recording** needs ffmpeg (`brew install ffmpeg`, the error hint says so; re-detects without a restart). -## Taskbar preference — the USER can ask for "combined mode"+## Taskbar preference: the USER can ask for "combined mode" > **WINDOWS ONLY.** macOS has no taskbar; `pup_configure` stores the preference and returns > `taskbarSupported:false`, but nothing changes on screen. Don't propose it to a Mac user. -The DEFAULT is `taskbarMode: tiles` — ONE button per AI thread, wearing the thread's NAME on the+The DEFAULT is `taskbarMode: tiles`, ONE button per AI thread, wearing the thread's NAME on the teal tile, all that thread's windows grouped under it (v2.0.152+; see `pup-taskbar-states` for the six states). The `taskbarGrouping` split/grouped knob below only applies in `aumid` mode. If the-user says "combine my pup windows", "one taskbar button for pup", "my taskbar is cluttered" — in+user says "combine my pup windows", "one taskbar button for pup", "my taskbar is cluttered", in tiles mode that is already the behavior per thread; across ALL threads it means `pup_configure {"taskbarMode":"aumid","taskbarGrouping":"grouped"}` (one "Adom Pup" button whose badge shows windows + total tabs). "Split them back" / "separate icons" → `{"taskbarGrouping":"split"}`. The setting persists across updates. Change it ONLY when-the user asks — it's their taskbar. Details: `pup-windows-sessions-tabs`.+the user asks, it's their taskbar. Details: `pup-windows-sessions-tabs`. -## Window-type taskbar icons (split mode) — what the user sees per window+## Window-type taskbar icons (split mode), what the user sees per window > **WINDOWS ONLY.** Everything in this section is a Windows taskbar behaviour. **macOS has no > taskbar**, no per-window tile, and no jump list, so on a Mac none of these icons, overlays or > right-click affordances exist. The category signal that DOES survive cross-platform is the **tab > title** (solid dot = signed in, hollow dot = public). On macOS, use `pup_list_windows` /-> `pup_list_tabs` to tell windows apart — do not refer the user to an icon they cannot see,+> `pup_list_tabs` to tell windows apart, do not refer the user to an icon they cannot see, > and do not skip a check on the assumption an icon already answered it. In split mode every pup window's taskbar icon states its CONTENT KIND. All five share the teal@@ -512,54 +508,54 @@ Adom tile with a solid browser-window drawing; the glyph inside the window is th | two panes | MIXED (tabs span more than one category) | The category derives from ALL tabs and updates live (about a second). The corner overlay stays the-ACTIVE TAB'S FAVICON (or window/tab counters in grouped mode) — never repurpose it. Adom wiki tab+ACTIVE TAB'S FAVICON (or window/tab counters in grouped mode), never repurpose it. Adom wiki tab titles also lead with a text glyph: solid dot = signed in, hollow dot = public. You never manage any of this; pup does it automatically. It is worth KNOWING because **on Windows** the user can see-login state and window kind at a glance — there, do not open a second window to "check" what a+login state and window kind at a glance, there, do not open a second window to "check" what a taskbar icon already answers, and when the user says "the window with the book/globe icon" you know which one they mean. **On macOS neither shortcut applies**: there is no icon to read, so check with `pup_list_tabs` rather than assuming, and expect the user to describe a window by its title or URL rather than by an icon. A WIKI window's taskbar right-click jump list also carries a view toggle ("Adom wiki: switch to-logged-in view" / "...to public view") — a USER affordance that relaunches that window under the+logged-in view" / "...to public view"), a USER affordance that relaunches that window under the other cookie jar with on-screen captions. Its callback verb `pup_wiki_set_view` is not for AI threads; pass `wikiView:"authed"|"public"` on pup_open_window instead. Details: `pup-adom-wiki`. -## Key rules (the essentials — details in the sub-skills)+## Key rules (the essentials, details in the sub-skills) -- **Always pass `sessionId`** on session-scoped verbs — `'{}'` targets the *active* session, which may not+- **Always pass `sessionId`** on session-scoped verbs, `'{}'` targets the *active* session, which may not be the one you mean. There is no top-level `pup` subcommand; it's `adom-bridge-cli pup_<cmd> <json>`.- **ONE DANGEROUS EXCEPTION: `pup_close`.** There `'{}'` does NOT mean "the active session" — it means+ **ONE DANGEROUS EXCEPTION: `pup_close`.** There `'{}'` does NOT mean "the active session", it means **ALL sessions, every window, across every AI thread on the machine**, immediately and without confirmation. If you want to close one window it is `pup_close_window {"sessionId":"…"}`, never `pup_close '{}'`. The rule above would otherwise lead you to expect the opposite, which is exactly how a tidy-up call becomes a data-loss event on someone's long-running dashboards.-- **Background is guaranteed; `foreground:true` is the only way to show a window** — not sizing, not+- **Background is guaranteed; `foreground:true` is the only way to show a window**, not sizing, not navigating, not screenshotting.-- **Own your window** — always pass `--ai-thread "<your conversation name>"`; that name is the window's+- **Own your window**, always pass `--ai-thread "<your conversation name>"`; that name is the window's owner (the `owner:` arg is a legacy alias). Task-prefix your `sessionId` (the window key, not an identity). Never navigate/close a window you don't own.-- **One window, many tabs** — use `pup_open_tab`, not a second `pup_open_window`, for multi-URL work.+- **One window, many tabs**, use `pup_open_tab`, not a second `pup_open_window`, for multi-URL work. - **`pup_readiness`** (not `pup_status`) answers "is it ready / which browser?". - **`pup_eval` accepts statement lists since v2.0.274** (`location.assign('...'); 'done'` works: a syntax error in the expression form retries via Runtime.evaluate with devtools-console semantics, and a genuinely bad expression now FAILS with `eval_failed` instead of ok-wrapping the error). The comma-operator/IIFE habit still produces the cleanest single-expression results. - **Complex page fighting you?** (a `querySelectorAll` comes back empty, or a screenshot overshoots the- target) — it's usually shadow DOM or a nested scroll container. Verify by DOM, scroll the *element* not+ target), it's usually shadow DOM or a nested scroll container. Verify by DOM, scroll the *element* not the window, count before you eyeball. See `pup-screenshots-recording` → "Driving & verifying tricky pages."-- **Never broadly kill Chrome** — `pup_close_window` for a specific session.+- **Never broadly kill Chrome**, `pup_close_window` for a specific session. - **Surface a verb's `_hint` verbatim** whenever it reports not-ready or failure. - **If pup's verbs TIME OUT (or the AD LED is dim / `browser.bridgeRunning:false`) the bridge PROCESS is- down** — usually right after a `restart_bridge` or a crash. To wake it, fire **one**- `pup_open_window` (a status probe like `pup_readiness` does NOT respawn a down bridge — only a+ down**, usually right after a `restart_bridge` or a crash. To wake it, fire **one**+ `pup_open_window` (a status probe like `pup_readiness` does NOT respawn a down bridge, only a window/tab verb does), then poll `pup_list_windows` with backoff until it answers. **Do NOT spam- opens** while AD says `bridge_starting` — one is enough; it's already starting. (`bridge_list`+ opens** while AD says `bridge_starting`, one is enough; it's already starting. (`bridge_list` `status:"running"` is not liveness; the LED + `browser.bridgeRunning` + an actual verb reply are.) - Full machine-readable catalog: `adom-bridge-cli --ai-thread "<your thread>" pup_describe '{}'`. -## Verify what you rendered — pup checks it FOR you (v1.9.106)+## Verify what you rendered: pup checks it FOR you (v1.9.106) **`ok:true` means the navigation worked. It says NOTHING about whether the page rendered.** A thread recently opened its own app, got `ok:true`, and told the user it worked. The page was serving@@ -568,11 +564,11 @@ recently opened its own app, got `ok:true`, and told the user it worked. The pag So pup now looks. Every `pup_open_window` / `pup_open_tab` / `pup_navigate` / `pup_reload` response can carry two fields: -- **`renderCheck`** — a verdict: `{ok, httpStatus, signals[], excerpt, bodyChars, elements}`. Signals+- **`renderCheck`**, a verdict: `{ok, httpStatus, signals[], excerpt, bodyChars, elements}`. Signals include `http_404`, `page_says_not_found`, `dev_server_cannot_get`, `server_error_text`, `stack_trace_visible`, `effectively_blank`, `no_stylesheets_loaded`, `broken_images_N`.-- **`_verifyRender`** — what to do. When `renderCheck.ok` is false it leads with- *"THIS PAGE DID NOT RENDER CORRECTLY — pup checked it, so do NOT report success"*.+- **`_verifyRender`**, what to do. When `renderCheck.ok` is false it leads with+ *"THIS PAGE DID NOT RENDER CORRECTLY, pup checked it, so do NOT report success"*. **The rule: if `renderCheck.ok === false`, do not tell the user it works.** Screenshot it (`pup_screenshot {sessionId}`) and READ the image, fix the cause, `pup_reload`, and re-check@@ -581,8 +577,7 @@ is on a different port, or the path is wrong; unstyled HTML means the CSS 404'd; app threw. `renderCheck` is a cheap DOM probe, not a screenshot, so it can miss things a human eye catches (wrong-layout, wrong content, an element off-screen). A clean `renderCheck` is not proof the page is *right* —-when you generated or edited the content, screenshot it anyway.+layout, wrong content, an element off-screen). A clean `renderCheck` is not proof the page is *right*, when you generated or edited the content, screenshot it anyway. ## Name your thread so the user can tell your window apart @@ -590,7 +585,7 @@ Pass `--ai-thread "<what this conversation is>"` on every `adom-bridge-cli` call `--target`). A typical user runs ~20 AI threads against one desktop, so pup puts your thread name at the FRONT of the window title (visible in the taskbar hover-preview header, which truncates ~25-30 chars) and in the hover tooltip alongside the container, browser and session. That is how the user knows which-of their conversations opened a window. Do NOT export `ADOM_AI_THREAD` — threads share one container, so+of their conversations opened a window. Do NOT export `ADOM_AI_THREAD`, threads share one container, so it names the shell rather than you. ## Found a pup bug? Please report it@@ -613,61 +608,60 @@ adom-bridge-cli --ai-thread "chip-fetcher tab 3" pup_open_window '{"sessionId":" ``` Pick a name the USER would recognise for this conversation, never a UUID. It is **attribution, not-permission** — self-reported, unverifiable, and nothing is gated on it. It exists so a user running ~20+permission**, self-reported, unverifiable, and nothing is gated on it. It exists so a user running ~20 threads can see which tab is driving their machine, and so a pup window can be labelled with the thread that owns it. -**Do NOT `export ADOM_AI_THREAD`** — threads share one container and one `$HOME`, so it names the shell+**Do NOT `export ADOM_AI_THREAD`**, threads share one container and one `$HOME`, so it names the shell rather than you: every thread reports the same name, which is worse than none because it is confident and wrong. Pass it per call (or `{"caller":{"aiThread":"..."}}` in the args). -## AUDIT YOURSELF — the standard procedure when John says "audit your windows" (2026-07-30)+## AUDIT YOURSELF: the standard procedure when John says "audit your windows" (2026-07-30) **"Audit your windows" ALWAYS includes the ICONS.** John had to tell me this twice: I reported sessions, health, geometry and logs and left the taskbar out, and the icon state was where the real regressions were hiding (a window that silently lost its badge after a restart; 8 stale AUMID keys for-3 windows). An audit without the icon section is not an audit — and it is not finished until you have+3 windows). An audit without the icon section is not an audit, and it is not finished until you have looked at the taskbar ON GLASS, because the API reports a badge as applied while it is invisible. Run ALL SEVEN sections, in this order, and report them as a table: -1. **Sessions (bridge view)** — `pup_list_windows`: count, sessionId, owner, tabCount, profile,+1. **Sessions (bridge view)**, `pup_list_windows`: count, sessionId, owner, tabCount, profile, ageMinutes, `_degraded`. Flag: owner:null (ownership protection is off), duplicate sessions pointing at one window, a tabCount that does not match what was opened. 2. **ICONS / TASKBAR (never skip)**- - per window: the `overlay` field — `favicon+adom:<host>` (correct), `count:N`, or `none` **plus+ - per window: the `overlay` field, `favicon+adom:<host>` (correct), `count:N`, or `none` **plus its reason**. A `none` on a favicon-serving page is a regression; a `none` on local PDFs is legitimate (no favicon exists). - AUMID keys: `reg query HKCU\Software\Classes\AppUserModelId` filtered to `Adom.Pup*`. In tiles mode (the default) expect the `Adom.Pup` anchor **plus one thread key per live- thread** (`Adom.Pup.thread.<slug>.<gen><tile-token>`; the token hashes the tile's content —- v2.0.271). MANY more than live threads = stale keys the janitor missed. The old "exactly 1"+ thread** (`Adom.Pup.thread.<slug>.<gen><tile-token>`; the token hashes the tile's content, v2.0.271). MANY more than live threads = stale keys the janitor missed. The old "exactly 1" expectation was the minimalTouch-era rule and reads every healthy tiles-mode machine as broken. - **ON GLASS:** `desktop_screenshot_screen`, crop the taskbar strip, and LOOK. Confirm each pup button wears its favicon badge at the configured `overlaySize` (default 75%) with the tile text readable under it. There is NO Adom mark on the badge (removed v2.0.192; only the generic faviconless glyph keeps it). Report how many of N windows are actually badged.- Better: run `pup_audit_icons {}` first — and remember its clean verdict is model-vs-shell,+ Better: run `pup_audit_icons {}` first, and remember its clean verdict is model-vs-shell, not pixels; `{force:true}` re-asserts art when a human says the glass is wrong.-3. **OS ground truth** — resolve windows by HWND (`pup_window_info`), never by title (`titleTag`+3. **OS ground truth**, resolve windows by HWND (`pup_window_info`), never by title (`titleTag` is off by default, so `titleContains:"(session:"` matches nothing). Each rect at the standard 10px-inset work-area frame (10,10 on every machine; the exact width/height follows that- monitor). A maximized window reads slightly negative origin — that is maximized, not stranded.+ monitor). A maximized window reads slightly negative origin, that is maximized, not stranded. `-32000,-32000` = minimized, confirm with IsIconic before calling it lost; a HIDDEN or off-screen window is always pup's own bug (the 30s visibility sweep repairs both).-4. **Health** — `pup_eval` per window: `readyState`, `visibilityState`, a `1+1` drivability check.+4. **Health**, `pup_eval` per window: `readyState`, `visibilityState`, a `1+1` drivability check. `visibilityState:hidden` is NORMAL for a parked/minimized window; only **frontmost-yet-hidden** is the broken state. Note any crashed tabs.-5. **Log** — `bridge_log_read`, count `[crash] [heal] [health] [unstick] [disconnect] [reclaim]+5. **Log**, `bridge_log_read`, count `[crash] [heal] [health] [unstick] [disconnect] [reclaim] [tabs] park-failed Handler error`. Explicitly report **park-failed** and **[reclaim] adopted** lines: a park-failed window is sitting wherever Chrome left it, and an adoption means pup LOST a window and re-bound it (which produces duplicate sessions for one window).-6. **Chrome footprint** — procs + total MB, and **pup's share vs the user's own Chrome**. Never+6. **Chrome footprint**, procs + total MB, and **pup's share vs the user's own Chrome**. Never report a big total as if it were all pup's.-7. **Cleanup state** — leftover test windows, phantom sessions, stale session files.+7. **Cleanup state**, leftover test windows, phantom sessions, stale session files. **Report rules:** state what is WRONG first, name which findings are your own regressions, and never-present an API-reported success (badge applied, verb ok:true) as proof — prove it on glass or in the+present an API-reported success (badge applied, verb ok:true) as proof, prove it on glass or in the page. ## ⏱ Captions: NEVER longer than 2 seconds (John, standing rule)
package.json+2−2@@ -1,6 +1,6 @@ { "slug": "pup-bridge",- "version": "2.0.552",+ "version": "2.0.553", "type": "app", "description": "The pup SKILL PACK \u2014 the AI skills that teach a cloud/container AI how to drive the Adom Desktop Puppeteer (pup) bridge (browser_* verbs): the everyday `pup` skill plus focused sub-skills for windows/sessions/tabs, screenshots+recording, browsers+Chrome install, and driving the Adom wiki. Installs into ~/.claude/skills + ~/.codex/skills. DOCS ONLY \u2014 this is NOT the bridge runtime. The bridge itself (the Node program that drives the browser) runs on the DESKTOP: Adom Desktop bundles it and auto-installs/updates it, and it's published separately on the Releases tab as adom-bridge-puppeteer-v<ver>.zip. The pkg version is kept in lockstep with the Release version so the page's max version row always carries BOTH a pkg tarball (skills) and a runtime zip (bridge) \u2014 a skills-only or runtime-only bump on the top row breaks constraint installs. TL;DR: pkg install = the skills for your AI; the bridge = the Release zip, which Adom Desktop manages for you.", "keywords": [@@ -147,5 +147,5 @@ "prompt": "Download the puppeteer-bridge zip from the wiki and walk me through server.js" } ],- "changelog": "Two corrections. (1) adom/pup-bridge#108: pup_list_sessions now resolves to the same caller-scoped implementation as pup_my_windows (registered passive, so it never launches a browser) and carries a catalog entry that survives regeneration, instead of erroring not_in_rust_yet while naming no supported equivalent. (2) WITHDRAWN across the root skill, pup-why-cft, pup-browser-detection and pup-bot-walls: the claim that matching CfT to the installed Chrome is what makes vendor sites load. That comparison was uncontrolled, every denied trial was a raw hand-rolled Chrome launch without pup's flag set while every passing trial was pup. Re-measured with screenshots on 2026-09-14, analog.com renders in pup on CfT 152 on both a warm profile and a brand-new isolated jar, so neither the build nor the cookie jar is the established discriminator. Keep the pin near the installed Chrome as hygiene, not as an explanation."+ "changelog": "Three fixes. (1) The Action required bubble: Chrome's ExternalInstallManager gates the 'Another program on your computer added an extension' dialog per-extension on extensions.settings.<id>.ack_external, NOT on the extensions.alerts.initialized flag pup was setting, so the old suppression never applied to it (John hit it again on 2026-09-14 for Application Launcher For Drive). prepare_profile now pre-acknowledges every machine-wide registered id, profile-scoped; pup still never edits the machine's registry. The registry read lives behind a new defaulted Host::external_extension_ids implemented only in host-win. (2) pup_close_all_windows is served instead of erroring not_in_rust_yet with no equivalent named, the same defect class as #108; it aliases onto close_window's existing all:true path, so it closes only the caller's own windows. (3) Em-dashes removed from the four pup skill files, 156 of them, per John's standing style rule." }
page.json+2−2@@ -4,7 +4,7 @@ "slug": "pup-bridge", "title": "Pup - Puppeteer Bridge", "brief": "pup is the AI's own browser: a real, full Chrome on the user's desktop that the AI fully controls (a sandbox, not the user's signed-in browser). Rides Bridge; pup_* verbs open windows and tabs, navigate, screenshot, and eval JS.",- "version": "2.0.552",+ "version": "2.0.553", "tags": [ "skills", "pup",@@ -167,5 +167,5 @@ "pup-bridge verbs are invoked through the adom-bridge CLI, not through a binary this package installs, so prefix rules cannot reach them." ] },- "changelog": "Two corrections. (1) adom/pup-bridge#108: pup_list_sessions now resolves to the same caller-scoped implementation as pup_my_windows (registered passive, so it never launches a browser) and carries a catalog entry that survives regeneration, instead of erroring not_in_rust_yet while naming no supported equivalent. (2) WITHDRAWN across the root skill, pup-why-cft, pup-browser-detection and pup-bot-walls: the claim that matching CfT to the installed Chrome is what makes vendor sites load. That comparison was uncontrolled, every denied trial was a raw hand-rolled Chrome launch without pup's flag set while every passing trial was pup. Re-measured with screenshots on 2026-09-14, analog.com renders in pup on CfT 152 on both a warm profile and a brand-new isolated jar, so neither the build nor the cookie jar is the established discriminator. Keep the pin near the installed Chrome as hygiene, not as an explanation."+ "changelog": "Three fixes. (1) The Action required bubble: Chrome's ExternalInstallManager gates the 'Another program on your computer added an extension' dialog per-extension on extensions.settings.<id>.ack_external, NOT on the extensions.alerts.initialized flag pup was setting, so the old suppression never applied to it (John hit it again on 2026-09-14 for Application Launcher For Drive). prepare_profile now pre-acknowledges every machine-wide registered id, profile-scoped; pup still never edits the machine's registry. The registry read lives behind a new defaulted Host::external_extension_ids implemented only in host-win. (2) pup_close_all_windows is served instead of erroring not_in_rust_yet with no equivalent named, the same defect class as #108; it aliases onto close_window's existing all:true path, so it closes only the caller's own windows. (3) Em-dashes removed from the four pup skill files, 156 of them, per John's standing style rule." }
skills/pup-why-cft/SKILL.md+10−10@@ -1,12 +1,12 @@ --- name: pup-why-cft user-invocable: true-description: "Parent skill: pup. WHY pup drives Chrome for Testing instead of installed Chrome/Edge — the five dated reasons (automation banner, --load-extension removal being the forcing one, version determinism, none of the consumer chrome, binary isolation), the honest trade-offs (~160 MB first fetch, the window title, no Widevine), and what did NOT change (profiles/logins are pup-owned either way). The policy flipped twice; docs/WHY-CFT.md on the wiki page is the dated canonical record and this skill is its AI-discoverable form. Read before questioning the browser choice, proposing native-first, or explaining a 'Google Chrome for Testing' window title to a user. Trigger words: why chrome for testing, why CfT, why not real chrome, why not installed chrome, why not edge, chrome for testing reasons, automation banner, controlled by automated test software, load-extension removed, browser policy pup, native-first, cft vs chrome, what is chrome for testing, google chrome for testing in task manager, widevine pup, drm pup."+description: "Parent skill: pup. WHY pup drives Chrome for Testing instead of installed Chrome/Edge, the five dated reasons (automation banner, --load-extension removal being the forcing one, version determinism, none of the consumer chrome, binary isolation), the honest trade-offs (~160 MB first fetch, the window title, no Widevine), and what did NOT change (profiles/logins are pup-owned either way). The policy flipped twice; docs/WHY-CFT.md on the wiki page is the dated canonical record and this skill is its AI-discoverable form. Read before questioning the browser choice, proposing native-first, or explaining a 'Google Chrome for Testing' window title to a user. Trigger words: why chrome for testing, why CfT, why not real chrome, why not installed chrome, why not edge, chrome for testing reasons, automation banner, controlled by automated test software, load-extension removed, browser policy pup, native-first, cft vs chrome, what is chrome for testing, google chrome for testing in task manager, widevine pup, drm pup." --- # Why pup drives Chrome for Testing (the five reasons) -**Policy since 2026-08-10 (pup 2.0.53+), and it is final.** The policy flipped twice — early pup+**Policy since 2026-08-10 (pup 2.0.53+), and it is final.** The policy flipped twice, early pup was CfT-based, mid-2026 flipped to "drive the browser already on the machine" for instant first runs, and production experience flipped it back for good. The dated technical record is **`docs/WHY-CFT.md`** on the `adom/pup-bridge` wiki page (written so the reasons outlive the people@@ -16,21 +16,21 @@ re-breaks. ## 1. The automation banner you cannot turn off -The moment pup does real automation in branded Chrome — synthetic cursor, login fill, captcha-assist — Chrome asserts the infobar: **"Chrome is being controlled by automated test software."**+The moment pup does real automation in branded Chrome, synthetic cursor, login fill, captcha+assist, Chrome asserts the infobar: **"Chrome is being controlled by automated test software."** It blips in and out as automation starts and stops, reflowing the viewport each time, and it is deliberately hard to suppress in the branded build: stripping `--enable-automation` changes input and focus semantics, and newer branded builds re-assert automation indicators when DevTools attaches regardless. Users watched the banner flicker over their windows all day. CfT is the build Google ships FOR automation: no banner arms race. -## 2. Extensions — the FORCING reason+## 2. Extensions: the FORCING reason Branded Chrome 137 (mid-2025) **removed `--load-extension`** specifically to stop silent side-loading. Verified live on branded 148 (2026-08-10), every alternative route for the Adom Pup toolbar failed or was vetoed: -- CDP `Extensions.loadUnpacked` needs a `--remote-debugging-pipe` launch — incompatible with pup's+- CDP `Extensions.loadUnpacked` needs a `--remote-debugging-pipe` launch, incompatible with pup's detached-launch architecture (windows outlive the bridge and are re-adopted over the port), and the install is session-scoped anyway. - Driving `chrome://extensions` + the native folder dialog puts a file dialog on the user's@@ -85,14 +85,14 @@ A different binary means a different process tree: the user's real Chrome can up closed without touching a single pup window, and pup's taskbar identity never tangles with their browsing. Profiles were always pup-owned; CfT extends the separation to the binary itself. -## The trade-offs, stated honestly (do not hide these from a user)+## The trade-offs: stated honestly (do not hide these from a user) -- **First-run download** (~160 MB) on a box without CfT cached — `pup_prewarm` does it ahead of+- **First-run download** (~160 MB) on a box without CfT cached, `pup_prewarm` does it ahead of time, and installed Chrome/Edge serve as the spawn-verified fallback ladder meanwhile, so an open never blocks on the fetch. - The window title and Task Manager say **"Google Chrome for Testing"**. Expected; the rendered window is otherwise identical. Tell a user asking "what is this browser?" exactly that.-- **No Widevine DRM** — protected streaming video will not play in pup. That is an nb/nbe job+- **No Widevine DRM**, protected streaming video will not play in pup. That is an nb/nbe job anyway (the user's real browser, the user's real identity). - A user who explicitly pins branded Chrome (`pup_use {browser:"chrome"}`) keeps everything working EXCEPT the toolbar extension and infobar suppression; pup says so when it matters.@@ -105,7 +105,7 @@ binary-agnostic; the same profile simply renders under the automation build. ## How the policy is implemented (so nobody "fixes" the code backwards) -`launchCandidates()` in `src/chrome.js` still lists installed Chrome → Edge → CfT — **on purpose:+`launchCandidates()` in `src/chrome.js` still lists installed Chrome → Edge → CfT, **on purpose: that is the fallback ladder**, not the policy. The pinned/cached CfT default that `readDefault()` promotes to the front of the list is what implements CfT-first. A healthy box reports `browserKind: "chrome-for-testing"` in `pup_readiness`.