← Commit history

Broaden the AD ask: expose ALL AD verbs to bridges via the direct-API (not just notify), plus cross-AD target routing + a spawn-time capability token for the gated verbs

John Lauer ·4543b213a2 · ·parent 31245c3
1 file changed +71−65
HANDOFF-notify-for-bridges.md+71−65
@@ -1,72 +1,78 @@-# Handoff to Adom Desktop: let bridges notify the user (and reach the attended device)+# Handoff to Adom Desktop: expose ALL AD verbs to bridges via the direct-API  **From:** pup (adom-desktop-puppeteer-bridge)-**Why:** pup can now install real Google Chrome on a box that needs admin, but on a-locked-down machine (VM / managed device) that requires a **Windows UAC**. To be usable,-pup must (a) tell the user "click YES on the prompt" and (b) re-nudge if it expires — all-**without an AI turn**. A bridge's only channel back to AD is the loopback direct-API-(`127.0.0.1:47200`), and today that API does **not** expose `notify_user`. So the bridge-cannot notify at all. This is the blocker.+**TL;DR:** A bridge should be able to call **any** AD verb the CLI can, through the+loopback direct-API (`127.0.0.1:47200`). Today it can only reach a **subset**+(`desktop_*` + other bridges' verbs). That's why pup can't fire `notify_user` to walk a+user through a Chrome UAC — but the fix should be general, not a one-off for notify. -## What's broken (evidence, AD 1.9.79 on ADOMBASELINE)+## The two directions (so we're precise) -`POST 127.0.0.1:47200/command` with every plausible namespace:+- **Inbound (AD → bridge):** AD routes verbs to a bridge by its declared prefixes+  (`browser_*` for pup). Works fine; not the issue.+- **Outbound (bridge → AD):** a bridge's only callback channel is the direct-API. AD's own+  `GET /commands` defines "reachable verbs" for a bridge as **`desktop_*` + `app:"dynamic"`+  bridge verbs only.** Top-level/lifecycle verbs are absent. So a bridge has a *partial*+  view of AD, not the full CLI surface.++## Evidence (AD 1.9.79, ADOMBASELINE)++`POST 127.0.0.1:47200/command`:  | body | result | |---|---|-| `{app:"desktop", command:"notify_user", ...}` | `error: "Unknown desktop command: notify_user"` |-| `{app:"core"\|"adom"\|"cli", command:"notify_user"}` | `error: "Unknown app: <x>"` |-| `{app:"dynamic", command:"notify_user"}` | `errorCode: "unknown_verb" — no installed bridge claims this prefix` |-| `{command:"notify_user"}` (no app) | HTTP 400 |--`GET /commands` confirms it: the `desktop` app enumerates only the **`desktop_*`** verbs-(`list_windows, screenshot_*, ui_click, read_file, registry_*, process_*, launch_process,-…`). `notify_user` / `notify_response` / `ping` / `server_*` are **not in any namespace**.-(The `adom-desktop` skill docs claim `notify_user` is "safe to send directly" — that's-aspirational; the running build doesn't dispatch it.)--## Ask 1 — expose `notify_user` + `notify_response` on the direct-API--Route these two top-level verbs through `POST /command` (same dispatcher the CLI/WS path-uses, same rich payload: `buttons`, `id`, `scenario:"reminder"`, `level`, `progress`).-Simplest: add them to the `desktop` app's command table (so `{app:"desktop",-command:"notify_user"}` works), and list them in `GET /commands` so callers can-capability-probe. pup **already probes `/commands`** and will light up automatically the-moment they appear — no pup release needed for the local case.--Loopback-only, so no new external surface. `notify_response` must stay pollable by the-same process that fired the `id`.--## Ask 2 — deliver the notification to the ATTENDED device--The harder, important half. Real scenario: pup runs on **box A** (a hypervm), the UAC is-on **box A** (you can only approve it there), but the user is sitting at **box B** (their-laptop, a different connected AD). A toast on box A's own desktop is invisible to them.-The user must be **pinged on box B** — "Chrome needs approval on **A** — go click YES" —-while the actual UAC stays on A.--A bridge can only reach its *local* AD; cross-device routing is AD/relay-level. Options-(your call which):--- **`target` on `notify_user`**: `{command:"notify_user", args:{…, target:"attended"|"<clientName>"|"all"}}`. The local AD relays it over the relay to that AD, which renders the toast. `"attended"` = the AD AD judges the user is present at (most-recently-focused / input-active), falling back to broadcast.-- **Include the origin box** in the payload/label automatically (`"…on ADOMBASELINE"`) so a cross-device toast is actionable.-- **Cross-device `notify_response` round-trip**: the user taps **Approve now** on box B; that click must travel back so the **bridge on box A** learns of it (it polls its local AD; the relay carries the correlation A↔B). Then the bridge re-raises the UAC **on A**. Without this, the one-tap-retry can't work across devices — only the "go look at A" ping does.--If cross-device round-trip is too big for a first cut, **Ask 1 + a one-way `target`-ping (Ask 2 minus the round-trip)** already makes this usable: pup pings the attended-device to send the user to box A, and box A shows the UAC. The re-arm-on-tap is a-follow-up.--## What pup already does (so you know the bridge side is ready)--- Probes `GET /commands` for `notify_user`; caches capability (`chromeStableNotifyChannel`).-- **If reachable:** notify-FIRST — fires a sticky `reminder` toast with an **Approve now**-  button and **waits for the tap before raising the UAC** (raising first puts up the-  secure desktop, which hides the toast). Re-notifies + re-waits on expiry. Autonomous.-- **If NOT reachable (today):** raises the UAC **once**, sets `awaiting_uac` +-  `chromeStableNotifyChannel:false` + errorCode `chrome_stable_needs_approval_uncced`,-  and returns a hint telling the AI to relay "go click YES on box A". **No blind-  re-raise** (that was a 1.8.12 bug that spammed the UAC).-- The moment AD ships Ask 1, pup's local-case notify works with zero pup changes. Ask 2-  (attended-device `target`) will need pup to pass `target` — a tiny follow-up once the-  contract is decided.+| `{app:"desktop", command:"notify_user"}` | `"Unknown desktop command: notify_user"` |+| `{app:"core"\|"adom"\|"cli", command:"notify_user"}` | `"Unknown app"` |+| `{app:"dynamic", command:"notify_user"}` | `"no installed bridge claims this prefix"` |+| `{command:"notify_user"}` | HTTP 400 |++`GET /commands` returns `{ desktop:[…desktop_* verbs…], bridges:[…], cliRequired:[…] }` —+no `notify_user`, `notify_response`, `ping`, `targets`, `server_*`, `bridge_*`, `hd_*`,+`updater_*`. Any bridge that needs one of those hits the same wall pup did.++## The ask — make the full verb set reachable by bridges++**1. Route every dispatchable verb through the direct-API**, not just `desktop_*`. A bridge+calling `POST /command {command:"<any verb>", args:{…}}` should hit the same dispatcher the+CLI/WS path uses and get the same JSON back (incl. `_hint`/`errorCode`/`statusVerb`). List+the full set in `GET /commands` so bridges can capability-probe (pup already does this and+will light up automatically).++**2. The `cliRequired` verbs** (`shell_execute`, `run_script`, `pull_file`, `send_files`,+`hd_send_files`) need the CLI's streaming/approval flow — so "all verbs" doesn't mean+"bypass approval." Keep their gates: make them **reachable** but still run the approval /+streaming path (or return a precise "call the streaming variant" error), rather than a bare+404. Don't silently drop them from a bridge's reach.++**3. Preserve the security posture, deliberately.** The direct-API is loopback-only, but+exposing the full surface (incl. `shell_execute`) to any local process is a real widening.+Recommended: AD hands each bridge a **spawn-time capability token** (env var) that the+direct-API requires for the sensitive/`cliRequired` verbs, so AD can attribute and authorize+bridge calls — and the per-verb approval prompts still fire. Bridges are AD-spawned, so AD+can mint the token cleanly.++**4. Cross-AD addressing (the attended-device problem).** A bridge reaches only its *local*+AD, but the user may be at a *different* connected AD (pup runs on a hypervm; the user is on+their laptop). Honor a **`target`** arg on direct-API calls — `"attended" | "<clientName>" |+"all"` — mirroring the CLI's global `--target`, so a bridge can route a verb (a+`notify_user`, a screenshot, anything) to the right box. `"attended"` = the AD the user is+present at (most-recently-focused / input-active). This makes the whole fleet addressable+from a bridge, and specifically lets pup ping the user wherever they actually are while the+UAC stays on the box that needs it.++## Why general, not per-verb++pup's immediate need is `notify_user` + `notify_response` (walk the user through a Chrome+UAC without an AI turn). But the kicad/fusion/native-browser bridges — and every future one+— will each want *some* verb outside the `desktop_*` slice (`notify_user`, `targets`,+`updater_*`, `bridge_list`…). Exposing the whole surface once removes the wall for all of+them. If you want to stage it: **(1) all non-`cliRequired` verbs + `target`** unblocks pup+today; **(2) `cliRequired` behind the capability token** follows.++## pup is already ready++pup probes `GET /commands` for `notify_user` and caches the result+(`readiness.chromeStableNotifyChannel`). The moment the verb appears, pup's notify-first+UAC flow activates with **zero pup release** for the local case. Cross-AD `target` is a+tiny pup follow-up once the arg is finalized. Full context:+`adom/adom-desktop-puppeteer-bridge` repo.