Closed feature request

Route bridge consent prompts (notify_user wait+buttons: pup_credential_reveal / pup_fill_card / pup_vault_sync) into ah like ab's own permission prompts, for boxes that block toasts (Ray)

John Lauer · 2mo ago ·closed by John Lauer

John, 2026-09-05: Ray's box blocks all ab (Windows) toasts, so ab was changed to send its own permission prompts over to ah (Hydrogen) where Ray sees them. Bridges use the same toast rail for their consent gates and will hit the same wall. pup's gated verbs all block on a notify_user {wait:true, buttons:[...], timeoutSec} decision:

  • pup_credential_reveal (buttons Reveal / Deny)
  • pup_fill_card (buttons Pay / Deny)
  • pup_vault_sync (buttons Allow / Deny) - new, 2.0.415+
  • pup_assisted_click and the foreground raise also toast before acting

On a box where the toast is blocked the user never sees the prompt, the verb answers blocked_on_user until the 120 s window expires, and the AI is stuck. Nothing tells the user a decision was waiting.

What we think the fix is (extending what exists)

Treat a bridge-originated notify_user that carries wait:true + buttons as a permission prompt and push it through the SAME ab->ah channel the core permission prompts use. Everything ah needs is already on the call: title, body, the buttons[].label list, the caller's ai-thread (X-Adom-Caller-Thread / args.caller.aiThread), the delegate bridge (X-Adom-Caller-Delegate: puppeteer), and a structured reason (pup is adding an explicit reason field to every consent toast in 2.0.419 so ah can show it verbatim; the body already carries "Reason: ..."). The button the user presses in ah should resolve the pending notify_user exactly as a toast click does ({action:"<label>"}), so the bridge side needs no change.

Nice-to-have: when ah is not connected, fall back to the toast; when both are live, show both and resolve on the first press.

3 Replies

John Lauer · 2mo ago

Shipped in 2.1.48 (insiders), exactly the shape you described: a notify_user with wait:true + buttons is treated as a decision prompt and pushed through the same channel as ab's own permission prompts. No bridge-side change.

What happens now, per waiting buttoned notify:

  • The toast still shows (where toasts render).
  • The prompt is ALSO pushed onto ab's own in-window permission stack (a "DECISION" card with the title/body, the requester line from caller / X-Adom-Caller-*, "The AI says: " when a reason string is on the call, and your button labels as the buttons). So on Ray's box the ab window itself can answer it.
  • When embedded, it is mirrored to Hydrogen via POST /permission/request with type:"notify-decision" and choices:[{id:<label>, label:<label>}] (hydrogen #63 asks ah to render that type with the given choices; if ah already renders unknown types from choices, it works today).
  • Every same-account mesh peer gets the card too (ON chip), so you can answer from the machine you are sitting at.
  • First press anywhere wins: the decision lands in the notify response store exactly like a toast click, the waiting verb returns {action:"<label>"} (your Reveal/Pay/Allow/Deny verbatim, matched case-insensitively), and every other copy is dismissed (/permission/dismiss {id, decision} to ah and the peers, the toast removed by tag, the ab card dropped). On timeout the same dismiss goes out with decision:"timeout".
  • Nothing is granted by the mirror: a label is just an answer to your prompt, never a shell grant, and the id-routed resolve refuses anything that is not one of your labels (unknown_decision).

Contract for pup: keep sending {wait:true, buttons, timeoutSec} as today; add reason (a string on the notify call) so the cards show it verbatim, and keep forwarding the caller headers so the requester line names the thread. delegatedBy is surfaced as the delegate on the mirrored card.

Leaving open until you or Ray confirm it on the toast-blocked box; the Hydrogen dialog half is tracked in hydrogen #63.

John Lauer · 2mo ago

Closing: shipped in 2.1.48 and verified in code today. A notify_user with wait:true + buttons is registered as a decision (notifications::register_notify_decision), mirrored through permission_mirror::request to Hydrogen (POST /permission/request type:notify-decision) when embedded AND to every same-account mesh peer, and pushed onto ab's own in-window DECISION card. The button pressed in Hydrogen resolves it via /permission/resolve (direct_api resolve_notify_decision, source "hydrogen"), a mesh press via the mirror source, an ab-window press via answer_notify_decision - first press anywhere wins and the waiting verb returns {action:""}. The toast still shows where toasts render, so the nice-to-have (both surfaces live, resolve on first press) is covered. No bridge-side change needed.

John Lauer closed this · 2mo ago
John Lauer · 2mo ago

Related, shipped 2.1.59/2.1.60: a bridge's notify_user {wait, buttons} decision (the pup_credential_reveal / pup_fill_card / pup_vault_sync class this issue is about) now also follows the user to their OTHER boxes over the hc relay, not only via the mesh and Hydrogen. The relay is outbound-only, so this works with the mesh off (the default) and never raises the firewall dialog; the user answers from whichever box they are sitting at and the bridge's waiting verb returns {action: label} exactly as a toast click would. Verified end to end: a decision raised on winvm from the gallia CLI rendered on AdomLapper (mesh off) within a second. A mirrored decision carries a one-shot nonce, so an AI on the relay cannot answer its own prompt (perm_mirror_resolve without it is refused, human_only). 2.1.60 fixed the case where a CLI-raised decision carried an empty caller identity and the receiving box silently refused the mirror.

Log in to reply.