Files
CherryHQ-cherry-studio/docs/references/ai/tool-approval.md
SuYao 5706307451 refactor(ai-service): consolidate AI runtime to main process (#14911)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com>
Signed-off-by: suyao <sy20010504@gmail.com>
2026-06-05 00:06:51 +08:00

3.7 KiB

Tool Approval

Model

Main is the single writer of approval state. The renderer surfaces an approval-requested ToolUIPart, takes the user's decision, and posts it to Main. Main applies the decision to the DB-authoritative anchor parts, persists, and resumes the stream.

End-to-end flow

  1. Tool needs approval — at execute time, the wrapper checks tool.needsApproval and the assistant's auto-approve policy. If approval is required, the wrapper writes an approval-requested part and resolves the tool's promise into a held state (Claude-Agent: holds canUseTool; MCP: stream pauses on the approval part).

  2. Stream pausesAiStreamManager transitions the topic to awaiting-approval. The topic.stream.statuses.<topicId> shared-cache entry carries the status; every renderer window reading that key sees the pause atomically.

  3. User decides — the approval card renders from the part. On click, useToolApprovalBridge (src/renderer/hooks/useToolApprovalBridge.ts) calls window.api.ai.toolApproval.respond(...) with approvalId, approved, optional reason / updatedInput, topicId, anchorId.

  4. Main appliesAiService's Ai_ToolApproval_Respond handler branches on transport before touching the DB:

    • Claude-Agent fast-path (AiService.ts:191-197): hands the decision to AgentSessionRuntimeService.respondToolApproval, which resolves the live canUseTool promise so the existing stream proceeds. When a live registry entry handles it, the handler early-returns — no DB read happens (and topicId / anchorId are not required).
    • MCP path (reached only when no live entry matched; requires topicId + anchorId): reads the anchor message's current parts from DB, applies the decision, and writes only when the target approval-requested part is present on the DB row — guarding the overlay-only case (approval received before the part has persisted). When all approvals on the turn are decided it dispatches a synthetic continue-conversation request through dispatchStreamRequest; the provider applies the decision when it reads parts.
  5. Awaiting-approval clears — the moment the continue stream broadcasts pending, the shared-cache entry flips back. Every window sees the approval card disappear in the same tick.

Persistent decisions

useToolApproval (src/renderer/pages/home/Messages/Tools/hooks/useToolApproval.ts) exposes an autoApprove action only for MCP tools — when an mcpTool descriptor is passed. It persists the opt-out by PATCHing the server's disabledAutoApproveTools, so the MCP settings page reflects it and subsequent calls of that tool skip the approval card. There is no generic per-tool default for non-MCP (e.g. Claude-Agent) tools.

Why this design

  • No renderer writes — the renderer cannot PATCH approval state. If it did, it would race Main's authoritative re-read and cause the approval card to reappear on every click.
  • Cross-window consistency — the shared-cache awaiting-approval status is the single source of truth for "this topic is paused".
  • Overlay/persist gap — the renderer sometimes sees the approval-requested part via overlay before it lands in the DB row. Writing unconditionally would clobber the (concurrent) Main-side persistence; the conditional write + continue-dispatch covers that case.

Where to read more

  • Main IPC handler: src/main/ai/AiService.ts (Ai_ToolApproval_Respond)
  • Renderer bridge: src/renderer/hooks/useToolApprovalBridge.ts
  • Persistent decisions: src/renderer/pages/home/Messages/Tools/hooks/useToolApproval.ts
  • Status broadcast: Stream Manager