feat(hub,desktop): hub-relayed desktop UI context (D5) - #489
Conversation
7fd13a0 to
d89b004
Compare
The second traffic class over the W3 reverse tunnel: not the embedded browser tabs but the desktop's own UI. An agent anywhere in the team asks a desktop what its user is looking at (ui_get_focus), or — per-call approved, always — for a frame of it (ui_screenshot). desktop_ui_invoke packages the call as a Kind="desktop.invoke" envelope, exactly as browser_invoke does with "browser.invoke". The structural machinery is now shared (one grant store, one approval helper, one router, two kind constants), so a third class costs ~zero. The interesting parts are the three places the classes deliberately DIFFER: - Grants are keyed by KIND; revocation is not. W3's store keyed team|host|agent with no kind dimension, so generalizing without one would let a browser-driving grant silence a desktop screenshot card — two different sentences, one consent. But "Revoke" in Settings → Remote driving means this agent no longer touches this desktop, so revoke clears every kind: the same asymmetry the desktop's own revoked-set already applies to reads. - ui_screenshot consults no grant at all, and the approval helper enforces that rather than trusting the client to hide the button — a class with no grant namespace ignores option_id:"session" entirely. The card carries session_grant:false so a renderer states the consent shape instead of inferring it from the kind. - The desktop re-checks the envelope kind. The hub gates BY CLASS, so a ui_screenshot routed as browser.invoke would be a capture nobody approved. dispatchHubInvoke refuses a tool that arrived under the wrong kind: the desktop is the authority for its own pixels, and it checks rather than trusts. The capability key `desktop_ui` tracks the sharing toggle and is re-posted when it flips, so hosts_list answers truthfully instead of making the agent discover the refusal on call. Bridge toggle + Remote-driving toggle still gate the relay itself — all three consents stand. The §3.5 driver work replaced THREE drop sites, not one. The plan named pane/stdio and image:false ACP; gemini's exec-per-turn argv was a third with the same defect. All three now materialize the image into <workdir>/.termipod/annotations/ (0600) and name the path in the text, which every one of them can read. Two consequences worth stating: an image-only input used to be rejected by the gemini driver and is now a valid turn whose body is the path (the user pointed at something and said nothing — a legitimate message); and a FAILED write keeps the historical drop-and-warn with the reason in the event, because "notes the drop rather than failing silently" is the rule. Tests: 11 new Go (hub class differences + the grant-store asymmetry) + 7 (materialization, including partial-write and nowhere-to-write) + 6 electron (kind↔tool enforcement). go build/vet/test ./... green, 299 electron node --test pass, all 12 repo lints clean. The live tunnel is unexercised here — it needs two machines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
92dabaa to
b9802e3
Compare
|
Review of D5 (verified against head Major — raw-pane agents never run in the workdir their annotations materialize into
p, err = launchCmdWithEnv(ctx, a.Launcher, sp, cmd, secretEnv)— no So a pane agent on the raw Fix options: wrap the raw pane launch in the same Minor
Test gapsApproval timeout → auto-resolve → deny is untested (needs a clock seam); the via-hub provider re-check wiring ( |
… the right class Review fixes for #489 (D5): - Major: the raw PaneDriver fallback launched its command bare while deriving Workdir 'same as the M1/M2 launchers' — the pane ran in the tmux server's cwd and annotation crops materialized into a directory the agent was never in. The launch now MkdirAll+cd-wraps into the derived workdir exactly like every specialized launcher, and Workdir is set only when that cd actually happened (else the materialization drop-with-warns honestly). Pinned by a launchOne test. - Audit mislabel: the pre-route audit summary hardcoded 'desktop action awaiting approval' for every class — now derived from the attention kind, so browser_action requests audit as browser actions again. - attention-kinds.md: the payload is documented per leg — the hub-raised remote card is sight-unseen ({host_id, host_name, tool, args, …}), the richer surfaces/url shape is desktop-raised only; header credit corrected (D3 introduced the kind, D5 adds the hub leg). - Materialized crops are capped per workdir (newest 32 kept) — they accumulated for the workdir's lifetime; prune is best-effort and test-pinned. - Gemini's drop warning regains the fallback reason the pane/ACP drivers carry (WHY the workdir fallback failed, not just that it did). - Stale comments swept: desktopui.ts D1-local-only header, desktopui.test.ts 'until D5', server.go bridgeGrants now names both tunnel classes. - Tests: browser_invoke rejects desktop-UI tools hub-side (the mirror of the existing cross-class check), raw-pane workdir contract, annotation-dir prune. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Second review pass (agent): the major confirmed against code and fixed on the branch — commit Major (fixed, the cd-wrap variant): the raw PaneDriver fallback launched its command bare while every specialized launcher ( Minors, all fixed:
Test gaps: added the hub-side Verified: 🤖 Generated with Claude Code |
…490) The stacked PRs squash-merged into their base branches (the GitHub stack cascade), so main received only D3 (#487); this merge brings the cascaded j8-d5-remote-desktop-ui head — element-resolved pointing (D4), hub-relayed desktop UI context (D5), and agent pointing (D6), each including its review-fix commit — onto main. Conflicts (main's D3-squash regions vs the branch's D5/D6-extended versions of the same code) resolved by taking the branch side, which is a strict superset; the plan's status block updated to all-shipped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
D5 of
docs/plans/desktop-ui-context-and-pointing.md(§3.5/§3.6). Stacked on #488 (D4) → #487 (D3).The second traffic class over the W3 reverse tunnel: not the embedded browser tabs, but the desktop's own UI. An agent anywhere in the team asks a desktop what its user is looking at (
ui_get_focus), or — per-call approved, always — for a frame of it (ui_screenshot).desktop_ui_invokepackages the call as aKind="desktop.invoke"envelope, exactly asbrowser_invokedoes with"browser.invoke".The structural machinery is now shared (one grant store, one approval helper, one router, two kind constants), so a third class costs ~zero — which is what §3.6 asked for. The reviewable part is the three places the classes deliberately differ.
1. Grants are keyed by kind; revocation is not
W3's store keyed
team|host|agentwith no kind dimension, so a naive "one helper, two kind constants" generalization would let a browser-driving session grant silence a desktop screenshot card — two different sentences, one consent. Granting is now kind-scoped.Revoking is not, and that asymmetry is deliberate: "Revoke" in Settings → Remote driving means this agent no longer touches this desktop, so it clears every kind. (The desktop's own revoked-set already applies the same rule to reads.)
2.
ui_screenshotconsults no grant at allEnforced in the approval helper, not by trusting the client to hide the button: a class with no grant namespace ignores
option_id: "session"entirely. The card also carriessession_grant: false, so a renderer states the consent shape instead of inferring it from the kind.3. The desktop re-checks the envelope kind
The hub gates by class —
browser_invokenever raises adesktop_actioncard — so aui_screenshotrouted asbrowser.invokewould be a capture nobody approved.dispatchHubInvokerefuses a tool that arrived under the wrong kind. The desktop is the authority for its own pixels: it checks rather than trusts.The capability key
desktop_uitracks the sharing toggle and is re-posted when it flips, sohosts_listanswers truthfully instead of making the agent discover the refusal on call. Bridge toggle + Remote-driving toggle still gate the relay itself — all three consents stand.The driver work replaced three drop sites, not one
The plan named pane/stdio and
image:falseACP; gemini's exec-per-turn argv was a third with the same defect. All three now materialize the image into<workdir>/.termipod/annotations/(0600) and name the path in the text, which every one of them can read.Two consequences worth flagging in review:
Verification
go build ./...,go vet ./...,go test ./...: all greenmcp_desktop_ui_test.go(class differences + the grant-store asymmetry), 7 inannotation_materialize_test.go(incl. partial-write and nowhere-to-write)tunnelclass.test.ts) — 299 passlint-docs,lint-glossary,lint-arb,lint-design-tokens,lint-desktop-tokens,lint-vocab,lint-templates,lint-governed-actions,lint-doc-anchors,lint-hardcoded-strings, …)reference/attention-kinds.mdgains thedesktop_actionsectionStack: D3 (#487) → D4 (#488) → D5 → D6.
🤖 Generated with Claude Code