main
Kyle Bergstedt Publish 3.0.0 259e308 13d ago

name: pup-credentials description: "How pup signs the user into sites AS THEM on macOS: pup's Keychain-backed credential vault (auto-capture on login + silent autofill on return, matched across a site's whole registrable domain), Google sign-in (pup_google_signin), syncing the vault with the user's other desktops (pup_vault_sync), HTTP Basic auth from the vault, and what to do when a saved password is stale. Importing the browser's own saved passwords is NOT available on macOS (pup_import_browser_logins answers unsupported_platform); logins grow by the user signing in once in a pup window. Trigger words: log me in, sign me in, saved password, pup_login, credential_set, pup_credentials, vault, stale password, google sign in in pup."

pup credentials — sign the user in AS THEM

⛔ THE RULE: serve the login from the vault, NEVER ask the human for a password

pup's adom-you vault EXISTS so the AI never has to ask the user for a username or password. If you find yourself about to ask "what's your Google password?" — STOP. You have verbs for this:

  1. It's probably already saved. pup_credentials {} lists every host+username pup holds. To sign a window in, pup_login {submit:true, submitReason} — pup fills and submits from the vault. To hand the actual secret to a LOCAL desktop assistant, pup_credential_reveal {host, reason} (one consent click).
  2. Not saved on THIS Mac? Ask the user to sign in ONCE in the pup window. pup captures the login as they submit it, and serves it from then on. (Importing the browser's own saved passwords is not available on macOS: pup_import_browser_logins answers unsupported_platform.)
  3. Saved on ANOTHER of the user's desktops? Sync it. pup_vault_sync {peer, reason} merges this box's adom-you logins with a peer desktop's, BOTH directions, so both boxes hold the union (newest-wins on a conflict). ONE consent toast on this box; passwords move pup-to-pup over ab's cross-desktop egress and never touch an AI. targets names the peers. This is the answer when one box has learned some logins and another box built up others.
  4. Only if a stored password is STALE do you fall to section 3 below. That is the LAST resort, not the first move.

Asking the human to paste a password in chat is almost always the wrong move: it exposes the secret, it does not grow the shared adom-you profile, and pup already has a cleaner path. Grow the vault (capture on login, sync) and serve from it.

pup is the user's doppelganger, so it needs to log in the way the user does. This skill is the whole credential story: the vault, syncing it, and what to do when a stored password has gone stale. Verbs live in the pup bridge; call them with adom-bridge-cli.

0. Google: pup_google_signin (never pup_login)

Google asks for the email on one page and the password on the next, so pup_login cannot do it and says so (no_login_form). Use the Google verbs (2.0.596):

adom-bridge pup_google_accounts {}                       # who this user is on Google, cached a day
adom-bridge pup_google_signin {"account":"[email protected]","submitReason":"the task is the work calendar"}

Most users have several Google accounts, and pup never guesses which one. The first time it is asked, pup searches everything it can reach on the Mac: the user's browser profiles as nb reports them (with the label the user gave each, like "John Personal"), pup's own vault, and which accounts pup's profile is already signed into. Each account comes back as work or personal (from its domain) and likelyStale when it lives only in pup's vault and in no browser (those are usually years old and Google demands a second step for them).

Call pup_google_signin with no account and more than one account exists, and the answer is google_account_choice_required with the list. Decide, in order:

  1. The task. Company mail, calendars, docs, vendor portals: the work account. Family, banking, school, personal purchases: the personal one.
  2. Your own skills. The user's work and personal skills usually name their accounts. Search them.
  3. Ask the user. Then pass account as the email. "work" or "personal" also works when exactly one account of that kind exists (a user with two work accounts gets the list again).

What the verb does, and what each answer means:

Answer Meaning Do
signedIn:true, alreadySignedIn:true pup's profile was already signed in; no password sent drive the window
signedIn:true email and saved password went through drive the window
google_password_missing not signed in and no saved password for that account the user signs in once in the pup window (pup captures it)
google_verification_required + options Google wants a second step pup_google_signin {window, account, challenge:"text message"}
google_verification_required (code) Google sent a code pup_google_signin {window, account, code:"123456"}
google_verification_required (other device) the user must tap Yes on their phone tell them; pup already waited 45 s
google_password_rejected the saved password is out of date section 3; never retry the same one
google_signin_unconfirmed Gmail opened as a different account call again; do not assume

Traps measured on John's laptop (2026-09-16):

  • Landing on Gmail proves nothing. mail.google.com/mail/u/?authuser=<email> for an account that is not signed in silently opens the signed-in account's inbox. pup confirms the account by name.
  • Never restart a sign-in stuck on a verification step. That resends the password and a new code. Finish it with {window, code} or {window, challenge}.
  • A text code can finish hands-free when your thread has the user's standing permission to read their texts (a personal texting skill). Otherwise ask the user for it.
  • Google does not block pup's default window. A window opened with lane:"cdp" carries a debug port and can draw "This browser or app may not be secure".
  • The vault holds one login per site AND username since 2.0.596. Before that a second Google account overwrote the first.

TOAST THE USER, AND PUT THE WINDOW ON THE TOAST (John, 2026-09-17)

"i didn't even realize you were waiting on me. why did you not ab toast me?" ... "when i just clicked that ab toast, i would have expected its action when i clicked the toast to open your pup window. i believe ab lets you attach those actions now." ... "whenever you need the users attention for a password, you should do it that way"

The moment a flow parks on the human, TOAST, and make the toast open the thing that is waiting:

adom-bridge notify_user {"title":"pup needs you to sign in once",
  "body":"Sign in as [email protected] in the pup window (click here to open it).",
  "level":"warning", "focus":{"hwnd": <the pup window's hwnd>}}

focus is what makes the click useful: {hwnd} raises that exact pup window, {host:true} reveals the app Bridge runs under (use it for a Bridge consent, which is a notification inside Hydrogen, not a window). A toast with no focus is the one the user clicks and nothing happens, which is worse than none.

pup does this itself from 2.0.603 on every no-password answer (google_password_missing, google_verification_required, google_password_rejected, no_credential), so an AI that just relays the answer is already covered. Do it yourself too whenever you are about to sit and wait: the user cannot see your thread.

Capture works on BOTH lanes (2.0.602, and why it did not)

Capture is what makes "sign in once" true: type a password into any pup window and pup stores it. It lived ONLY on the CDP lane until 2.0.602, so every login typed into a normal (extension-lane) window was thrown away and pup asked again next time. John typed his Google password into a pup window, pup kept nothing, and the explanation "your browser never saved it" was about the wrong thing. If you are on a build older than 2.0.602, a typed password is NOT stored on the default lane. Verify a capture instead of assuming it: pup_credentials {} must list the host right after the sign-in.

1. The vault (how pup remembers logins)

  • Auto-capture: when the user (or pup) signs into a site, pup grabs the credential and stores it in the macOS Keychain. Passwords are NEVER echoed back — pup_credentials lists host + username only.
  • Autofill: on a return visit pup fills the saved login in the background (no Chrome dropdown, no foregrounding). Submitting is reason-gated (pup_login {submit:true, submitReason}) and confirms the live domain first (anti-phishing).
  • Registrable-domain matching: a login saved on ms.example.com is offered on example.com and its siblings, like a browser password manager. So the host a credential was saved under does not have to equal the host of the page you are filling.
  • Durable: the vault index lives in ~/.adom (not the bridge cache), so it survives pup updates.
  • Store one explicitly: credential_set {host, username, password}. List: pup_credentials {}.

2b. Sync the vault across the user's desktops (a real union, not a push)

The user runs several desktops; each pup learns different logins over time (one signs into Gusto on the laptop, another into a vendor portal on the office box). pup_vault_sync {peer, reason} makes both complete:

adom-bridge-cli pup_vault_sync {"peer":"arav-rog","reason":"both my desktops should have all my logins"}
  • Bidirectional. It pushes THIS box's logins to the peer AND pulls the peer's back, so afterwards both boxes hold the UNION. A one-way copy would leave each box missing what the other learned — which is exactly the failure this verb avoids.
  • Newest-wins. If the same host has different passwords on the two boxes, the more recently updated one wins on both. Identical entries are skipped.
  • No AI sees a password. The bytes go pup→ab→peer-ab→peer-pup over the account's cross-desktop egress. pup_vault_export / pup_vault_import (the internal halves) are refused for cloud/relay callers — only same-account desktops sync.
  • One consent. The initiating box shows one toast ("Sync your saved logins with ?"); the user clicks Allow. Like every consent verb, waiting on the click is not a timeout — re-call with the same args to re-attach.
  • Run it again whenever a box has learned new logins; it is cheap and idempotent.

3. A saved password is STALE

The tell: pup fills + submits (filledUsername/filledPassword/submitted all true) but the site says "Fail to authenticate" / "incorrect password". The stored secret is out of date, OR the username itself is wrong (the same site can be saved under different usernames).

On a Mac the clean fix is the user: ask them to sign in once in the pup window (toast with the window attached, see above). pup's capture rail stores the current login, with the right username, and pup_login works from then on. If the user gives you the current details directly, store them with credential_set {host, username, password} and retry. If another of their desktops holds a fresher copy, pup_vault_sync {peer, hostFilter} brings it over. Registrable-domain matching (section 1) handles a login saved on ms.<site> and used on <site>.

Privacy rules (non-negotiable)

  • Only ever handle a login for the site the user asked about, on the user's own machine, at their request.
  • A password you are given is used once to store it. Store it and move on — never print it, never repeat it in a later message, never log it.
  • Prefer, when the user would rather pup never see the plaintext: have them log in once manually (pup's capture rail grabs it fresh), or use a "Sign in with Google/Microsoft" SSO path (pup-vendor-login) that types no password at all.

When NOT to need any of this

Manufacturer-direct sites (TI, ST, ams, …) and datasheets need no login at all — see ee-parts-sites.md. Reach for credential recovery only when a site truly gates what the user wants behind their own account.

Read landed before you say a word about signing in (pup 2.0.642)

Every pup_open_window and pup_navigate response carries landed:{url,title,loginPage,signedIn,_hint}. The shared adom-you profile keeps sessions (claude.ai, chatgpt.com, the wiki, vendor portals) for weeks, so most opens land already signed in. An AI toasted "Sign into claude.ai in the pup window once" on 2026-09-19 while the window was already on the user's chats. Never do that: read landed; signedIn:true means say nothing; loginPage:true means try the vault, Google sign-in and the site's recipe in login-recipes/ first, and toast the user only when pup itself cannot finish. Recipes: login-recipes/claude.ai.md, login-recipes/chatgpt.com.md.

HTTP Basic/Digest auth (the browser's native "Sign in" dialog) is answered from the vault on every lane (2.0.644)

Some downloads (SamacSys / componentsearchengine.com CAD models, vendor file servers) answer with a 401 and Chrome pops its native "Sign in" dialog, which no page verb can see. pup answers it from the vault on both lanes now, matched on the registrable domain (a componentsearchengine.com entry serves st., ms. and nxp.componentsearchengine.com). With no entry, pup CANCELS the challenge so the dialog never blocks the window, and the next pup_navigate / pup_fetch_url returns errorCode: no_credential naming the host, with the routes: the user signs in once, pup_vault_sync {peer, hostFilter}, or credential_set {host:"<domain>", username, password}. A stored credential the server rejects is tried once, then reported the same way (stale password: see login-recipes/recover-a-current-password.md). pup_fetch_url works on the extension lane too, so a gated file can be fetched as bodyBase64 without a download dialog.

Where secrets live

Every password is a macOS Keychain item under the service adom-bridge-pup (keyed by host, or host|username for a second account on the same host), indexed by ~/.adom/pup-credentials.index.json (host + username only). pup's own browser runs with an in-memory cookie key (--use-mock-keychain), so it never asks the user for Keychain access.