refactor: serve permission requests on the experimental ACP v2 surface (#1234)

First part of step 5 of opt-in ACP v2 support (after #1231, #1232): the
v2 surface now sends the agent's permission requests. Nothing changes on
ACP v1: no v1 golden file changes.

## What differs in v2

A v1 request asks about a tool call, and clients show the tool call's
title as the prompt. A v2 request gives the prompt a required `title`
and an optional `description` of its own, apart from the tool call. It
asks about a `subject`, which can be a tool call or a command.

## Change

- **Prompt text.** The agent already computes the prompt's heading and
explanation, but only AIR gets them, in
`_meta.jetbrains.air.permission`. The agent's request type,
`AcpPermissionRequest`, now carries them as `title` and `description`
for a v2 client. It's the same rule #1232 uses for diffs: build what v1
can't express only for a v2 client. So v1 requests are byte-identical,
and the v1 scenario golden files, which record what the agent hands
`requestPermission`, don't change. The v2 surface fails on a request
without a title, so a missed site fails loudly.
- **Subject.** Always `{ type: "tool_call", toolCall }`, where
`toolCall` is the same patch a `tool_call_update` carries
(`v2ToolCallPatch`). The `command` subject would suit Bash once display
terminals exist, since its `terminalId` refers to one.
- **Approval preview.** The exact patch before an Edit or Write runs is
now built in v2 form for a v2 client, and the request shows it. This
fixed a real gap: the agent pins the preview as content the client
holds, but only AIR's request shape included it. Without the fix, a v2
client saw no diff at all, because the hook's identical patch was then
dropped as unchanged.
- **Answer.** v2's outcomes map back unchanged. An outcome v1 doesn't
know, which the draft says is no approval, becomes `cancelled`, and the
agent reads that as an aborted tool use.
- **`requires_action`** while a request is open was already in place
since #1226.

## Scenario suite

All 12 scenarios that stopped at a permission request now pass: Bash,
Edit (approved and rejected), Write, NotebookEdit, WebFetch, MCP,
network, ExitPlanMode (approved and rejected), and two subagent
scenarios. Each recording shows `requires_action`, the request, then
`running`. Now only replay and the two plan scenarios remain.

## Verification

- **End to end:** a new test drives the agent's real `canUseTool`
through a v2 client. Allow, reject, and an unknown outcome become allow,
deny, and an aborted tool use.
- **Unit tests:** the prompt's title is separate from the tool call's
(ExitPlanMode: "Ready to code?" vs "Approve Plan"), the preview shows in
the request, the v2 preview fallback works, and the title guard rejects
a missing title.
- **Mutations:** each rule fails a test when removed. Sending
`title`/`description` to every client fails 37 tests, including v1
golden files.
- Build, lint and all 2380 tests pass.
This commit is contained in:
Ben Brandt
2026-10-02 09:15:39 -07:00
committed by GitHub
parent c1c8c0377d
commit b9a6ec00a6
24 changed files with 608 additions and 57 deletions

View File

@@ -0,0 +1,28 @@
{"kind":"response","method":"initialize","payload":{"_meta":{"steering":{"supported":true}},"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp"}],"sessionUpdate":"available_commands_update"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolName":"Agent"}},"content":[],"kind":"think","name":"Agent","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Task","toolCallId":"toolu_native"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolName":"Agent"}},"rawInput":{"description":"Fix the build"},"sessionUpdate":"tool_call_update","title":"Fix the build","toolCallId":"toolu_native"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolName":"Agent"}},"rawInput":{"description":"Fix the build","prompt":"Make the build pass"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_native"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolName":"Agent"}},"content":[{"content":{"text":"Make the build pass","type":"text"},"type":"content"}],"rawInput":{"description":"Fix the build","prompt":"Make the build pass","subagent_type":"general-purpose"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_native"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"content":{"text":"Running t","type":"text"},"messageId":"msg_native_sub_1","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"content":{"text":"he build.","type":"text"},"messageId":"msg_native_sub_1","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native","toolName":"Grep"}},"content":[],"kind":"search","name":"Grep","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"grep","toolCallId":"toolu_native_grep"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native","toolName":"Grep"}},"rawInput":{"pattern":"TODO"},"sessionUpdate":"tool_call_update","title":"grep \"TODO\"","toolCallId":"toolu_native_grep"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native","toolName":"Grep"}},"rawInput":{"path":"/src","pattern":"TODO"},"sessionUpdate":"tool_call_update","title":"grep \"TODO\" /src","toolCallId":"toolu_native_grep"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native","toolName":"Grep"}},"content":[{"content":{"text":"src/a.ts","type":"text"},"type":"content"}],"rawOutput":"src/a.ts","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_native_grep"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native","toolName":"Bash"}},"content":[],"kind":"execute","name":"Bash","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Terminal","toolCallId":"toolu_native_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native","toolName":"Bash"}},"rawInput":{"command":"make"},"sessionUpdate":"tool_call_update","title":"make","toolCallId":"toolu_native_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native","toolName":"Bash"}},"content":[{"content":{"text":"Build","type":"text"},"type":"content"}],"rawInput":{"command":"make","description":"Build"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_native_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native","toolName":"Bash"}},"content":[{"content":{"text":"Build","type":"text"},"type":"content"}],"kind":"execute","name":"Bash","rawInput":{"command":"make","description":"Build"},"status":"pending","title":"make","toolCallId":"toolu_native_bash"},"type":"tool_call"},"title":"make"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native","toolName":"Bash"}},"content":[{"content":{"text":"```\nmake: *** Error 2\n```","type":"text"},"type":"content"}],"rawOutput":"make: *** Error 2","sessionUpdate":"tool_call_update","status":"failed","toolCallId":"toolu_native_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolName":"Agent"}},"content":[{"content":{"text":"```\nThe build still fails.\n```","type":"text"},"type":"content"}],"rawOutput":[{"text":"The build still fails.","type":"text"}],"sessionUpdate":"tool_call_update","status":"failed","toolCallId":"toolu_native"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}