Repository navigation
Surface ACP permission requests to the desktop app, first answer wins - #897
Conversation
An ACP agent's permission request went only to the server's web card; the desktop app's local broker fed only the Claude/Codex hooks, so a Copilot permission never appeared there. Wrap the interaction delegate to also register a permission on the broker and race the two surfaces — the desktop allow/deny maps back to an offered option by kind so the daemon resolves the id as a web pick would. Elicitations stay server-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PR Summary by QodoSurface ACP permission prompts to desktop with first-answer wins
AI Description
Diagram
High-Level Assessment
Files changed (4)
|
Code Review by Qodo
1.
|
A generic desktop allow must never resolve to an allow_always option: it grants more than the card showed, so an allow with no once-scoped option fails closed. The card reuses the bounded pending builder — an over-cap frame poisons every subscription — pairs the server id onto itself so a client seeing both lanes coalesces them, and audits every settlement. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the desktop answered first the server await was cancelled, which resolved the interaction as cancel — server history then disagreed with the allow/deny the agent received. Submit the mapped decision back so the server records it, first-writer-wins keeping a web answer that already landed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AI-2197
What & why
An ACP agent's permission request (Copilot, Cursor, Gemini, Kiro) went only to the server's web card; the desktop app's local permission broker was fed only by the Claude Code / Codex HTTP hooks, so a hosted ACP agent's permission never appeared in the desktop app. This wraps the server-facing interaction delegate the ACP bridge already calls: for a permission it registers a card on the local
PermissionPromptBroker(which the app subscribes to over local control IPC) alongside the server and races the two — first answer wins, the loser is dismissed. The desktop card answers allow/deny, which maps back to one of the agent's own offered options by kind, soMapPermissionDecisionresolves the option id exactly as a server-side pick would. Elicitations stay server-only.Where to look
AcpPermissionSurfaceis the whole race + mapping. It is wired at the five ACP factory registrations inDaemonRunnerand used byAcpHostedAgentRuntimeFactoryonly when a broker is present — null keeps a launch server-only, so every existing test and the review-flow path are unchanged. The broker is the same DI singleton the desktop app'sPermissionIpcreads and the Claude/CodexLocalPermissionBridgealready writes.Safety properties the surface holds, mirroring
LocalPermissionBridge:allow_alwaysoption, honouring the click would grant more than was shown, so it fails closed to a deny rather than escalating.PermissionPromptBroker.TryCorrelate, so a client that sees both the server and the local lane coalesces them into one card instead of showing two.permission-decisions.jsonl, so a locally-answered ACP decision is in the audit log like a Claude/Codex one.When the desktop answers first, the same mapped decision is submitted back to the still-open server interaction, so server history records that allow/deny and the server's first-writer-wins tracker keeps a web answer that already landed. A visible web card may linger in a browser until refreshed, but the interaction is resolved and the agent is answered exactly once.
Verification
AcpPermissionSurfaceTests(8): a desktop allow maps to the allow option's id; a deny to a deny outcome; a generic allow with only a standing grant offered fails closed; the server id is correlated onto the local card; a settlement is written to the audit log; an oversized tool name skips the local card and stays server-only; a server answer is returned and dismisses the desktop card; an elicitation never registers a card.AcpHostedAgentRuntimeFactoryTestsandAcpHostedAgentRuntimePermissionTestsstay green; daemon AOT publish is IL-clean.