Repository navigation
refactor(agent-runtime): unify declarative tool approval policy - #18865
Conversation
…ity modules Extract environment.ts (env assembly + token-budget math), mcpCatalog.ts (server specs, tool metadata, bounded cache warm), pathContainment.ts (workspace containment helpers), and ClaudeCodeSessionStateService (the four session-keyed maps + MCP catalog subscription, now a lifecycle service per naming-conventions §5.2 with onStop/onDestroy cleanup). settingsBuilder keeps orchestration and re-exports the historical import surface, so driver/warmup consumers and their module mocks are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
…ator toolGuards.ts models the PreToolUse policy plane: rules declare what they match, their effect, and — via bypassBehavior — whether bypassPermissions lifts them, instead of each imperative hook deciding ad hoc. The evaluator folds all matching candidates deny > ask (mirroring the SDK's parallel severity fold) with deterministic reasons, and headless overrides apply independently of the bypass skip. guardRules.ts transcribes the ten policy hooks into rows; reasons are byte-identical to the hooks they will replace. Additive only — nothing consumes the table yet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
buildClaudeCodeHooks(ctx) now owns all 13 PreToolUse hooks and the PostToolUse timing hook, moved verbatim from buildToolPermissions with an explicit ClaudeCodeHookContext instead of closure capture. Shared name constants come from guardRules so the table and the hooks cannot drift. No behavior change; hook function names are preserved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
Replace the ten imperative policy hooks with one toolGuardHook that evaluates CLAUDE_TOOL_GUARD_RULES at fire time (live permission mode and disabled set from the session snapshot; interaction state per turn). canUseTool keeps its structure; its approval-required input now derives from guardRules' canonical list. Behavior change (approved): bypassPermissions no longer raises approval cards for the per-call approval list (kb_manage / generate_image / cli_install / mounted assistant tools) — it is the user's explicit opt-out of per-call approval. Headless denials are not approvals and survive bypass unchanged. The role asks (Assistant feedback, Support Bash) and the workspace-escape ask stop firing under bypass, which is net-equivalent: canUseTool's mode auto-approval already pierced those asks before this change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
Move the approvalRequiredTools check behind the bypassPermissions gate in decideToolCall so the mode is a real opt-out of per-call approval, matching the claude and pi runtimes. Outside bypass the list still beats auto-approval. Delegated subagents inherit the change: under bypass an approval-required call now runs instead of dead-ending in the ask-degrades-to-deny path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
…missions The guard protects the shared cross-agent environment (~/.bun, ~/.local/share/uv, …), not just the session, so it is a safety block — denied outright, never a prompt — and bypassPermissions must not lift it. This aligns pi with the claude and dsh runtimes. The pi-specific Full Access warning copy is retired: bypass now means the same thing on every runtime (approvals lifted; explicit safety blocks still apply), so the generic card copy is accurate. Locale catalogs synced; the 2026-08-13 pi permission-mode breaking-change entry updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
Signed-off-by: suyao <sy20010504@gmail.com>
Signed-off-by: suyao <sy20010504@gmail.com>
Signed-off-by: suyao <sy20010504@gmail.com>
Signed-off-by: suyao <sy20010504@gmail.com>
|
Code review complete. No actionable findings. The declarative guard table architecture correctly separates policy from implementation, with explicit bypass behavior declarations. Safety blocks (disabled tools, global installs, protected built-in destructive operations, non-bypassable cross-Session delegation) remain enforced under Full Access across all runtimes. The lifecycle-managed session state service properly cleans up resources. Test coverage is comprehensive. |
The fire-time no-responder denial had no bypassPermissions exemption, so it agreed with the guard table only because the SDK skips canUseTool under that mode. Derive it from the tool's `bypassApproval` and the snapshot's live mode, and reuse the same predicate for `requiresUserResponse`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
`askStrength` was never read: the snapshot's auto-allow exceptions come from the policy registry, not the table, so a 'hard' row without a matching `required` entry got none of the enforcement its doc comment promised. Also un-export `resolveRealOrNearestExistingPath` (no external consumer) and log a guard condition that throws instead of failing open silently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
The note claimed unattended turns still deny every tool needing a live responder. Since the headless bypass exemption landed, approval-only tools (kb_manage, image generation, CLI/skill installs, assistant tools) do run on channel and scheduled turns; only user-authored-answer tools, config mutation and the delegation ceiling still deny. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
…iltin flag `ToolGuardContext.isProtectedBuiltin` put "is this one of Cherry's two built-in agents" into the generic evaluator's context type. A plain `appliesTo.roles` list expresses the same scoping without the concrete-agent concept, and drops the field from the context, the hook context and settingsBuilder. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
… runtime The guard table meant to hold cross-cutting policy also held three rules that only Cherry Assistant / Support need (builtin-destructive, assistant-feedback, support-bash) plus their Lark/GitHub command detectors. They now live with the Agents that declare them, and the Claude runtime appends that table to its own. Putting them under agents/ required `ToolGuardRule` to be reachable without agents/ importing a runtime, so hoist the tool-approval layer to src/main/ai/toolApproval/ and move the guard language into it. That layer already imported nothing from runtime/ and was already consumed from agentSession/, so "runtime-neutral" was only true of its contents, not its path. Dependencies now point one way: runtime -> agents -> toolApproval. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
…of branching on role The built-in Agents' reach — host MCP tools, all-knowledge-base access, skill and plugin sources, filesystem settings — was re-derived as `builtin_role ===` branches in 13 places across three runtimes, and had already drifted: Cherry Support got host tools on Claude but not on Pi or DSH. Move it to one table the Agents own (`builtinAgentCapabilities`) and have the runtimes read capabilities instead of asking which Agent this is. The Pi/DSH divergence is preserved verbatim as an explicit `hostTools.runtimes` field rather than silently unified; behavior is unchanged on every runtime. Assistant tool names move to `@shared/ai/assistantTools` so the MCP server, the approval registry and the capability table name the same tools without any new cross-layer import — `agents/` must not depend on `mcp/` or `runtime/`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
…not an agent name `BuiltinToolAvailability = 'always' | 'assistant'` gave a runtime-neutral registry an enum member named after one Agent family, and forced an `assistantMcpEnabled` boolean through nine files including the generic guard context. An entry's availability was already implied by its `serverName`: it applies exactly when its MCP server is mounted. Query by the mounted server set instead. `resolveMountedMcpServers` derives that set once from the Agent's capabilities, so the servers a session builds and the policy derived from them cannot disagree — this also drops the special-cased `mcp__skills__search_skills` filter, which existed because the registry claimed the skills tools were always available while Support never mounts that server. The capability interface collapses with it: `skillSource`, `skillDiscovery`, `filesystemSettings` and `pluginSources` had exactly two combinations across every role — one bit spelled four ways. They become `environment: open|sealed`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
Two conflicts, both where main moved code this branch had already rewritten:
- settingsBuilder `autoAllowRuntimeNames`: kept this branch's registry query
(`listBuiltinToolPolicies({ approval: 'auto', mountedServers })`) over main's
`CHERRY_BUILTIN_AUTO_APPROVED_TOOL_NAMES` name lists, and took main's comment,
which drops the reference to the now-deleted CHANNEL_SECURITY_PROMPT.
- PiRuntimeConnection.test: dropped the CHANNEL_SECURITY_PROMPT import main
removed, kept this branch's hoisted `@main/ai/toolApproval` path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
|
Code review complete. No actionable findings. The declarative guard table architecture correctly separates policy from implementation, with explicit bypass behavior declarations. Safety blocks (disabled tools, global installs, protected built-in destructive operations, non-bypassable cross-Session delegation) remain enforced under Full Access across all runtimes. The lifecycle-managed session state service properly cleans up resources. Test coverage is comprehensive. |
Two conflicts: - AgentSessionRuntimeService imports: both sides added a different line. Kept this branch's hoisted `../toolApproval/ToolApprovalRegistry` and main's widened `../types` import (ApprovalRequestedEvent). - ProviderSpecific/__tests__/CherryInOauth.test.tsx (modify/delete): took main's deletion. #18895 removed this duplicate suite because it synchronized on the IPC call before the balance effect had run; the surviving suite in ProviderSettings/__tests__ now waits on the rendered balance and covers the top-up refresh. That supersedes 26758a0, which was patching the same flake in the copy main deleted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
| if (!rule.match.when) return {} | ||
| try { | ||
| return await rule.match.when(ctx) | ||
| } catch (error) { |
There was a problem hiding this comment.
Security Observation: Fail-Open on Condition Exceptions
This catch block treats exceptions from guard conditions as "no match", which could bypass security rules if a condition throws unexpectedly.
Failure scenario:
- A malformed input, filesystem error, or bug in a condition detector (e.g.,
detectGlobalInstall,isPathWithinAllowedRoots) causes an exception - The rule is skipped, potentially allowing a
denyrule likeglobal-installorworkspace-escapeto be bypassed - The tool proceeds with the runtime's default permission-mode semantics instead of the guard's intended protection
Impact:
- Deny rules (
global-install,builtin-destructive) could be bypassed if their condition functions throw - Ask rules could be skipped, allowing tools without prompts
Suggested consideration:
- For deny rules, consider whether failing closed (returning
{}to match) would be safer - At minimum, consider logging at error level rather than warn to ensure visibility of potential bypasses
| } catch (error) { | |
| } catch (error) { | |
| logger.error('Guard condition threw — treating as no match', { ruleId: rule.id, toolName: ctx.toolName, error }) | |
| return null | |
| } |
Note: The current conditions appear to internalize their own failure postures (e.g., treating unresolvable paths as outside the workspace), but this fail-open behavior is still worth explicit consideration for defense in depth.
| } | ||
| }, | ||
| { | ||
| // Installing third-party skill code needs a responder — except under bypassPermissions, the |
There was a problem hiding this comment.
Code Clarity: Meaningless bypassBehavior
This rule has bypassBehavior: 'skipInteractiveEffect' but no effect field, making the bypassBehavior declaration meaningless.
Analysis:
bypassBehavior: 'skipInteractiveEffect'only affects rules with an interactiveeffect(deny/ask)- This rule has no
effectfield—it only has aheadlessoverride - The
skipHeadlessDenyInBypass: trueon line 128 already handles the bypass case for the headless denial
Behavior verification (from tests):
- Interactive turn: no decision (undefined) — canUseTool handles it elsewhere
- Headless turn, no bypass: deny
- Headless turn, with bypass: undefined (allowed via
skipHeadlessDenyInBypass)
The rule works correctly, but bypassBehavior is dead code here.
Suggested fix:
Either remove bypassBehavior or document this pattern if it's intentional for consistency with other rules.
| // Installing third-party skill code needs a responder — except under bypassPermissions, the | |
| { | |
| // Installing third-party skill code needs a responder — except under bypassPermissions, the | |
| // user's explicit opt-in to unattended installation. | |
| id: 'skill-install', | |
| bypassBehavior: 'enforce', // No interactive effect to skip; headless deny is explicitly lifted below | |
| match: { tool: 'mcp__skills__install_skill' }, | |
| headless: { | |
| predicate: 'turn-headless', | |
| reason: | |
| 'This channel or scheduled turn cannot approve a skill installation. Use bypassPermissions for unattended installation, or install it from an interactive turn.', | |
| skipHeadlessDenyInBypass: true | |
| } | |
| }, |
Or simply remove bypassBehavior since it has no effect on headless-only rules.
There was a problem hiding this comment.
This review was translated automatically.
Found 1 blocking cross-runtime security contract gap and 1 shared-layer architecture boundary issue. The current non-bypassable built-in Agent guard only integrates with Claude, while Pi and DSH's Full Access can still bypass similar restrictions, which is inconsistent with the PR documentation and the product contract in #18898. Suggest REQUEST_CHANGES: first integrate the hard limits into the evaluator shared by all three runtimes, and move tool names exclusively for Main use out of src/shared/.
Original Content
发现 1 个阻断性跨运行时安全契约缺口和 1 个 shared-layer 架构边界问题。当前不可绕过的 built-in Agent guard 只接入 Claude,Pi 与 DSH 的 Full Access 仍可绕过同类限制,与 PR 文档及 #18898 的产品契约不一致。建议 REQUEST_CHANGES:先将硬限制接入三个 runtime 共用的 evaluator,并将仅供 Main 使用的工具名称移出 src/shared/。
… placement - Raise the fail-open guard-condition catch from warn to error (@claude): it is the only signal that a deny rule was skipped by a throwing detector. - Make `bypassBehavior` part of the effect-bearing arms of `ToolGuardRule` (@claude). It was dead on the three rules whose only decision is a headless denial, and the compiler now rejects declaring it there. The suggested `bypassBehavior: 'enforce'` was not viable — `validateToolGuardRules` rejects `enforce` combined with `skipHeadlessDenyInBypass`. - Move assistant tool names out of `@shared` (@vaayne): all consumers are in `src/main`, so the shared layer's cross-process rule did not admit them. They live in the approval layer, which already owns every Cherry tool's canonical identity, as an import-free leaf so `mcp/servers/assistant.ts` cannot close a cycle back through the MCP layer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
… runtime Review of #18865 established that three of the limits the note claimed "on every runtime" are enforced by the Claude guard table alone: built-in Agent destructive operations, unattended agent-configuration changes, and the Assistant/Support external-submission commands. Verified rather than assumed. Pi's authorizer runs disabled -> global-install -> `if (bypass) return`, and only reaches `detectDestructiveCommand` inside `requiresApproval` under `mode === 'auto'` — two steps after Full Access has returned. dsh has no equivalent guard, and neither runtime supplies built-in role context, so the role-scoped rules cannot fire at all. `cherryConfig` is `approval: 'auto'`, so unattended Pi/dsh turns can still mutate agent config. The gap predates this PR; the false guarantee did not. Split the bullet list into cross-runtime limits and Claude-only ones, and point both the note and `builtinAgentGuardRules` at #18898, which already proposes the shared evaluator that would close it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
There was a problem hiding this comment.
This review was translated automatically.
LGTM. Re-review confirmed that original code findings have been closed, and no new correctness, security, or architecture issues were found in the incremental changes; cross-runtime permission unification continues to be tracked by #18898.
Original Content
LGTM. 复审确认原有代码 findings 已闭环,增量未发现新的 correctness、security 或 architecture 问题;跨 runtime 权限统一继续由 #18898 跟进。
There was a problem hiding this comment.
This review was translated automatically.
Non-blocking: The Breaking changes in the PR description still claims that protected built-in destructive-operation blocks and unattended responder requirements remain in effect under Full Access for all runtimes; the updated breaking-change document has clarified these restrictions as Claude-only, and left cross-runtime unification for #18898. Please sync the PR description to avoid making overly broad guarantees for Pi and DSH in the release notes. The code review conclusion remains approved.
Original Content
非阻断:PR 描述的 Breaking changes 仍声称 protected built-in destructive-operation blocks 和 unattended responder requirements 在所有 runtime 的 Full Access 下保持生效;更新后的 breaking-change 文档已经将这些限制明确为 Claude-only,并把跨 runtime 统一留给 #18898。请同步 PR 描述,避免 release note 对 Pi 和 DSH 作出过宽保证。代码复审结论仍为通过。
Signed-off-by: suyao <sy20010504@gmail.com>
Review SummaryThis is a well-structured refactoring that unifies tool approval policy across runtimes. After reviewing the policy registry, guard table evaluation logic, runtime consistency, and session state management, I found no high-confidence correctness bugs, security issues, or contract violations. Key verification points:
The documented behavioral change (Full Access lifting per-call approvals) matches the implementation, and the known gap about guard coverage (#18898) is properly acknowledged. |
zhangjiadi225
left a comment
There was a problem hiding this comment.
LGTM. The implementation centralizes Cherry-owned tool approval metadata, preserves the explicit non-bypassable limits, and provides a sound transition toward the shared evaluator tracked in #18898. One non-blocking documentation consistency note remains.
| "Full Access" on a Fast (Pi) agent now lifts every approval prompt: tools that used to keep asking even there | ||
| (knowledge-base edits, image generation, CLI installs, assistant settings tools) run without a prompt. Two | ||
| explicit safety blocks still apply — disabled tools, and shell commands that install into the shared global | ||
| environment (`npm install -g`, `pip install --user`, …), which are denied outright rather than prompted. |
There was a problem hiding this comment.
[A1] Non-blocking: this paragraph says Full Access lifts every approval prompt and that only disabled tools or global installs remain restricted, but session_create and session_send still require live approval in every mode. Please mention that cross-session delegation remains the explicit exception, matching the newer 2026-08-18 breaking-change note.
<!-- Template from https://github.com/kubevirt/kubevirt/blob/main/.github/PULL_REQUEST_TEMPLATE.md?--> <!-- Thanks for sending a pull request! Here are some tips for you: 1. Consider creating this PR as draft: https://github.com/CherryHQ/cherry-studio/blob/main/CONTRIBUTING.md --> > ### Branch strategy > > - Active development targets `main`. ### What this PR does Before this PR: Cherry Studio remains at version 2.0.8, and the bundled release notes, stable release history, and generated product manifest still identify 2.0.8. After this PR: Cherry Studio is prepared for the stable 2.0.9 release with bilingual user-facing release notes, a matching stable-history entry, and a regenerated built-in product manifest. <!-- (optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*: --> Fixes # N/A ### Why we need it and why it was done in this way This release packages the user-visible features, fixes, improvements, and performance work merged since v2.0.8. Release notes were generated from commit-level `release-note` blocks first, with targeted inspection for user-visible commits without one. Internal refactors, CI, tests, documentation-only changes, dependency maintenance, and commits marked `NONE` were excluded. The following tradeoffs were made: - Keep one user-facing line per included commit, which makes this large release comprehensive. - Preserve the exact bilingual notes in both `electron-builder.yml` and `release-history.json`. - Regenerate the product manifest from its source instead of editing it manually. The following alternatives were considered: - A prerelease was not used because 2.0.9 is being prepared as a stable patch release. - A shorter thematic summary was not used because it would omit user-visible commit-level changes required by the release workflow. Links to places where the discussion took place: None. ### Breaking changes This release includes user actions or behavior changes that reviewers should verify: - GitHub Models has been retired; affected users must configure another provider. - Full Access now skips per-call approval for additional Agent actions; users who still want prompts should choose Approve for Me or Ask Before Acting. - Existing flomo users must authorize the migrated MCP connection once more. - Windows users whose `CLAUDE_CODE_GIT_BASH_PATH` points to `git-bash.exe` must change it to Git Bash's `bin/bash.exe`. - Users with anonymous data collection enabled must review the August 2026 privacy policy before analytics resumes. ### Special notes for your reviewer Release preparation checklist: - [ ] Review generated release notes in `electron-builder.yml` - [ ] Verify stable release notes are preserved in `resources/cherry-studio/release-history.json` - [ ] Verify version bump in `package.json` - [ ] Verify generated product manifest uses the release version - [ ] CI passes - [ ] Merge to trigger release build <details> <summary>Included commits since v2.0.8 (147)</summary> - fix(provider-model): reconcile ambiguous delete failures (#19301) - refactor(code): retire CLI config read IO, broadcast managed-tool status (#18671) - perf(dsh-runtime): bundle packaged dependencies (#19191) - feat(data): support JSON Merge Patch for provider settings (#19280) - fix(screenshot): keep overlays on the active macOS Space (#19281) - fix(composer): show first picker attachment on unused assistant (#19293) - perf(painting): parallelize history file hydration (#19283) - fix(agent-runtime): serialize stop and retry teardown (#19252) - fix(chat-shell): keep sidebar resize handle aligned after settings navigation (#19123) - fix(code-editor): resolve editor height and language extension lookups (#18951) - feat(wechat-channel): support native iLink media (#19278) - feat(agent-dialogs): refine create and edit experience (#19013) - feat(pi-icon): use official Pi mark (#19277) - fix(painting-size): keep selected chip outline visible (#18982) - fix(settings): default settings entries to model services (#19137) - fix(provider): add defaultChatEndpoint to lmstudio preset so Pi/DSH endpoint resolution succeeds (#19006) - fix(api-gateway): restore Gemini 3 thought signatures on replayed tool calls (#19047) - fix(provider-registry): distinguish integer image ranges (#19256) - feat(settings): clarify Global Memory scope and add memory overview (#19229) - fix(agent): add back navigation to subagent flows (#19266) - fix(chat-markdown): animate streamed inline code (#19156) - perf(home-composer): skip unchanged streaming renders (#18468) - feat(skills): restore file browser and translation (#18993) - feat(skills): open installed skill folders (#18929) - fix(agent-search): exclude hidden reasoning from search (#17661) - fix(composer): isolate referenced conversation instructions (#19257) - fix(agent-creation): show go-to-chat action after tool creation (#19195) - fix(quick-panel): preserve labels while aligning descriptions (#19199) - fix(composer): preserve pasted screenshots with text flavors (#19260) - fix(agent): discover workspace .agents skills (#19258) - fix: remove unsupported 'includeServerSideToolInvocations' field from Gemini tool config (#15098) - fix(agents): delete an agent's task schedules with the agent (#19215) - refactor(i18n): flatten locale catalogs to dotted keys (#19143) - fix(agent-migration): tolerate unreadable legacy database (#18912) - docs(development): remove empty Development section heading (#19237) - feat(appearance): surface the chat and work list positions in settings (#19200) - fix(popups): stop IME candidate Enter from resolving PromptPopup with raw pinyin (#19224) - refactor(observability): remove orphaned IPC trace patch (#19246) - fix(stream-manager): ignore stale terminal callbacks (#17997) - docs(references): verify stale paths fixed for #18850 (#19227) - feat(sidebar): support middle-click and context menu to open in new tab (#19202) - fix(settings): avoid router-hook crash when ModelSettings renders off… (#19205) - feat(provider-registry): add DeepSeek V4 Flash Vision Exp with image input (#19236) - fix(markdown): copy tables from local source blocks (#19193) - perf(screenshot): make the capture overlay appear immediately (#18933) - fix(deepseek-harness): heal stale managed keys at top-level of versioned credentials (#19162) - feat(export): convert markdown alerts to notion callouts (#18611) - fix(filesystem-mcp): parse ripgrep JSON output (#17598) - feat(artifact-pane): add copy path context menu to workspace file tree (#18776) - fix(prompt-interpolation): preserve literal replacement text (#17574) - fix(artifact-pane): clear the elevated composer in a maximized pane (#19034) - feat(launchpad): refresh built-in app icons (#19059) - fix(quick-panel): prevent hidden action hitboxes after tab switches (#19187) - feat(prompt-bindings): add contextual prompt visibility (#18378) - fix(prompt-editor): align prompt action button styles (#19190) - chore(gh-create-issue): add restricted engineering task template (#19173) - perf(mini-app): stabilize pool callbacks (#19169) - fix(models): hydrate custom model registry metadata (#19174) - fix(binary-manager): make BabelDOC installable from mainland China (#19098) - fix(binary-manager): retry pipx installs across PyPI mirrors in China (#19102) - ci(provider-registry): author catalog sync commits through GraphQL (#19181) - fix(model-drawer): drop native number spinners from model settings inputs (#19125) - fix(composer): preview Markdown attachments (#19154) - fix(models): hydrate custom model registry limits (#19155) - fix(screenshot): show the IME candidate window while annotating text (#18934) - test(agents-migration): fix flaky high-fan-out copy test on Windows CI (#19132) - fix(api-gateway): map chat reasoning_effort to Responses (#18854) - fix(quick-panel): keep skill names readable (#19142) - fix(screenshot): let Escape dismiss an overlay before any selection (#18932) - fix(model-selector): distinguish duplicate model names (#19018) - fix(api-gateway): fold inline system messages for openai-chat-completions (#19145) - fix(anthropic-provider): accept thinking blocks without a signature (#19146) - fix(screenshot-overlay): keep the toolbar on screen for a full-screen selection (#19150) - feat(mcp): add built-in install_mcp_server tool for one-click MCP registration (#18609) - chore(download-binaries): share downloaded tools across git worktrees (#19126) - fix(app-startup): apply launch-on-boot preference to system (#18225) - fix(agent-runtime): default missing model context window to 256K (#18847) - fix(model-list): read the Ollama context window from /api/show (#19046) - fix(deepseek-harness): write the versioned dsh credentials layout (#19131) - fix(provider): align Responses behavior and provider state ownership (#18449) - docs(claude-md): replace broken pnpm test <path> advice with working targeted commands (#19135) - fix(quick-assistant): use the configured quick model (#19080) - fix(mcp): collect prompt arguments in a dialog before insert (#18946) - fix(mini-app): relay guest keydown so renderer shortcuts work in webviews (#19076) - fix(api-gateway): convert max_tokens to max_completion_tokens for OpenAI reasoning models (#18768) - fix(composer): prevent editable toggle from restoring sent draft text (#19113) - fix(composer): surface full url on truncated link chips and speed up og hover (#19107) - fix(provider): remove retired GitHub Models integration (#19072) - fix(pi-agent): resolve citations for pi's tool_call wrapper (#19071) - fix(message-locate): bind global-search locate requests to their conversation (#19094) - fix(data-migration): skip Dexie records with unreadable large values (#19086) - fix(binary-manager): validate installs by the bins a recipe exposes (#19087) - test(trace-viewer): drop the virtualizer scroll debounce before jsdom teardown (#19101) - fix(keyboard): make message scroll containers focusable for arrow-key navigation (#19070) (#19099) - perf(chat): remove unused flow navigation listener (#19085) - fix(html-artifact): prevent bare-text height jitter in inline preview (#18998) - feat(usage): redesign usage analytics with ECharts (#19084) - fix(chat): prevent arrow-key scroll jitter in message list (#18769) - fix(topic-rename): wait for persistence before showing success (#19005) - fix(citations): clear sources when switching conversations (#18996) - fix(ovms): list all configured OVMS models regardless of loading state (#18826) - fix(chat): restore clear-messages shortcut after event removal (#19091) - fix(message-list): isolate message targeting by tab (#18997) - fix(composer): preserve drafts when stream open is blocked (#18999) - fix(conversation-routing): prevent stale session actions (#18970) - fix(topic-list): clear messages for non-active topics (#19004) - fix(right-pane): animate maximize and restore instead of committing at the boundary (#19083) - fix(paths): divert dev logs directory away from packaged installs (#18068) - fix(user-data-relocation): copy symlink targets during migration (#19033) - fix(agents): lazy-load AgentSessionImageCaptureHost in Sessions (#19078) - fix(notes): preserve navigation after renaming (#19020) - fix(agent-composer): show full AskUserQuestion titles (#19016) - fix(code-style): isolate theme catalog updates (#19069) - fix(command-execution): restore Bash output on Windows (#17682) - fix(topics): prevent old title flash after rename (#18760) - feat(mini-app): split the mini app view into two panes (#18425) - feat(sidebar): pin assistants and agents to the sidebar (#18345) - fix(agent-session): update lastActivityAt when reusing placeholder session (#19036) - test(job-scheduler): fix flaky nextRun assertion on natural interval fire (#19057) - feat(local-model): enable inference hardware acceleration by default (#19055) - refactor(external-app): unify external open targets (#19027) - fix(migration): preserve agent identity files when v1 source workspace is removed (#19053) - fix(pdf-translation): show resource download progress (#19051) - refactor(mcp): move builtin server knowledge out of the MCP runtime (#18630) - fix(api-gateway): downgrade inline system messages for Ollama (#19042) - fix(job-scheduler): persist automatic next runs (#19017) - fix(code-mate): ignore stale provider selections (#19015) - fix(permission-mode): improve option legibility (#19035) - fix(topic-navigation): recover deleted active topics (#18995) - fix(message-locate): cancel stale requests on conversation switch (#18994) - fix(ai-messages): drop unanswered approvals from replayed history (#18066) - fix(code-viewer): highlight the first frame without the debounce delay (#18950) - feat(assistant-settings): add model settings navigation (#19030) - fix(api-gateway): keep Agent SDK system messages out of the assistant turn (#18697) - fix(skills): only block skills on provable dependency gaps, and split SkillService (#18549) - feat(shortcuts): restore Ctrl+L clear messages shortcut (#19021) - ci(claude-review): keep review failures advisory (#19031) - feat(privacy): publish August 2026 policy update (#19022) - docs(code-review): require dual sources for network downloads (#18953) - fix(api-gateway): skip Anthropic tools without an input_schema (#18780) - fix(pi-agent): resolve citations for third-party MCP tool outputs (#18829) - perf(chat): lazy-load disabled message outline (#18971) - fix(tabs): prevent pinning transient mini-apps (#18904) - fix(provider-registry): send enable_thinking for Qwen on openai-compatible providers (#18772) - refactor(agent-runtime): unify declarative tool approval policy (#18865) - feat(provider-registry): remote catalog hot-update without an app release (#16857) - fix(skills): support valid GitHub SKILL.md links (#18907) </details> ### Checklist This checklist is not enforcing, but it's a reminder of items that could be relevant to every PR. Approvers are expected to review this list. - [x] Branch: This PR targets `main` - [x] PR: The PR description is expressive enough and will help future contributors - [x] Code: [Write code that humans can understand](https://en.wikiquote.org/wiki/Martin_Fowler#code-for-humans) and [Keep it simple](https://en.wikipedia.org/wiki/KISS_principle) - [ ] Refactor: You have [left the code cleaner than you found it (Boy Scout Rule)](https://learning.oreilly.com/library/view/97-things-every/9780596809515/ch08.html) - [x] Upgrade: Impact of this change on upgrade flows was considered and addressed if required - [x] Documentation: A [user-guide update](https://docs.cherry-ai.com) was considered and is present (link) or not required. Check this only when the PR introduces or changes a user-facing feature or behavior. - [x] Self-review: I have reviewed my own code (e.g., via [`/gh-pr-review`](/.claude/skills/gh-pr-review/SKILL.md), `gh pr diff`, or GitHub UI) before requesting review from others ### Release note <!-- Write your release note: 1. Enter your extended release note in the below block. If the PR requires additional action from users switching to the new release, include the string "action required". 2. If no release note is required, just write "NONE". 3. Only include user-facing changes (new features, bug fixes visible to users, UI changes, behavior changes). For CI, maintenance, internal refactoring, build tooling, or other non-user-facing work, write "NONE". --> ```release-note Cherry Studio 2.0.9 - Features, Fixes, and Experience Improvements ✨ New Features - [WeChat] Agents can now send documents and videos through WeChat channels, while incoming video and voice messages are saved to the Agent workspace. - [Skills] Installed Skill details once again show the source file tree and content preview, with optional one-click Chinese translation for Markdown files. - [Skills] Installed Skill details now include an action to open the managed skill folder. - [Agent] Agent sessions now automatically discover workspace skills stored under .agents/skills. - [Appearance] Chat conversation list and work task list positions can now be changed from Settings → Appearance. - [Sidebar] Sidebar items can now be opened in a new tab with middle-click or the context menu. - [Models] Added DeepSeek V4 Flash Vision Exp with image input support. - [Export] Notion export now converts GitHub-style Markdown alerts into native Notion callout blocks. - [Files] The Agent workspace file tree now offers a context-menu action to copy a file path. - [Prompts] Prompts can now be global or limited to selected contexts, with context-specific ordering and management in Settings. - [MCP] Agents can now register an MCP server in one step; new servers remain inactive and untrusted until enabled unless activation was explicitly requested. - [Usage] Redesigned usage analytics with a clearer overview, independent request sorting, and interactive charts. - [Mini Apps] Mini apps can now be shown side by side in a split view. - [Sidebar] Assistants and Agents can now be pinned to the sidebar for quick access to their latest conversation. - [Providers] Provider and model catalog updates can now be delivered without waiting for an app release. 🐛 Bug Fixes - [Providers] Model deletion now keeps the model list synchronized when a request reports failure after the model was already removed. - [Providers] Updating provider settings now preserves untouched nested options and correctly removes explicitly cleared values. - [Screenshot] Capture overlays now remain on the active macOS Space. - [Composer] Fixed the first paperclip attachment not appearing immediately after switching to an unused assistant. - [Agent] Fixed an immediate retry after stopping a session returning an empty or stalled response or losing Claude Code resume state. - [UI] Fixed the left sidebar resize handle becoming misaligned after returning from Settings. - [Code Editor] Fixed artifact file previews not filling or scrolling correctly and restored syntax highlighting for Markdown, TSX, TOML, Vue, PHP, and other file types. - [Painting] Painting size selections now keep their full outline visible. - [Settings] Generic Settings entry points now consistently open Model Services. - [LM Studio] Fixed LM Studio models not appearing in Pi and DeepSeek Harness Agent model pickers. - [Gemini] Fixed Gemini 3 latest aliases failing after the first tool call because replayed calls lacked thought signatures. - [Painting] Image count controls now reject fractional values while continuous controls retain decimal precision. - [Markdown] Fixed streamed inline code appearing abruptly and occasionally causing a rendering error. - [Agent] Global search no longer exposes hidden model reasoning, and existing search data is cleaned automatically. - [Agent] Referenced conversations are now treated as historical context and cannot authorize Agent actions. - [Composer] Fixed pasted Windows screenshots being dropped when the clipboard also contains text. - [Gemini] Fixed Gemini 3.x requests failing with HTTP 400 when web search and MCP tools are used together. - [Agent] Deleting an Agent now also removes its scheduled tasks instead of leaving them running and failing in the background. - [Migration] An unreadable legacy Agent database no longer blocks v2 migration; it is preserved and reported while remaining data continues migrating. - [Input] Confirming a Chinese, Japanese, or Korean IME candidate no longer saves raw composition text when renaming a conversation. - [Onboarding] Fixed a fresh installation crashing on the model-selection step. - [Markdown] Copying a table now returns the original Markdown table instead of unrelated message content. - [CodeMate] Fixed DeepSeek Harness failing to launch when old managed API-key entries remain in its credentials file. - [MCP] Fixed workspace file search failing to parse ripgrep results correctly. - [Prompts] Prompt and translation placeholders now preserve literal replacement text, including special replacement characters. - [Artifact Pane] Fixed the elevated composer remaining visible after maximizing the artifact pane. - [QuickPanel] Fixed hidden actions remaining clickable after switching back to a tab. - [Models] Recognized custom models now inherit missing registry descriptions and input/output capabilities. - [PDF] Fixed BabelDOC installation failing in mainland China by using accessible Python, font, and model mirrors. - [Tools] Python-based tool installation in mainland China now retries alternate package mirrors when the default mirror is not ready. - [Composer] Markdown attachments can once again be opened in the file preview from the Composer or sent messages. - [Models] Existing custom models now inherit context limits and pricing when a registry update recognizes them. - [Screenshot] The IME candidate window now remains visible while annotating screenshots with text. - [API Gateway] Fixed reasoning effort being lost when Chat Completions requests are converted to OpenAI Responses requests. - [Screenshot] Escape now dismisses a capture overlay before a selection is made, including on another hovered display. - [API Gateway] Fixed Agent sessions failing on OpenAI-compatible endpoints that require system messages to appear first. - [Providers] Fixed Anthropic-compatible providers failing when thinking blocks omit a signature. - [Screenshot] Fixed the annotation toolbar appearing off-screen after a full-screen capture. - [Startup] Launch on Startup now registers with the operating system and reapplies the saved preference when Cherry Studio starts. - [Agent] Pi and DeepSeek Harness Agents can now use models without a configured context window by falling back to 256K. - [Ollama] Local Ollama models now use their reported context window, preventing tool-based Agent sessions from being truncated and failing. - [CodeMate] Fixed DeepSeek Harness 0.1.1 and newer rejecting the credentials layout written by Cherry Studio. - [Providers] Fixed Volcengine Ark multi-turn Responses and web search, Hugging Face reasoning and tool use, compatible third-party reasoning settings, OpenAI reasoning summaries, and Fast mode for supported Doubao models. - [Quick Assistant] Quick Assistant now uses the configured Quick Model instead of silently falling back to the default model. - [MCP] MCP prompts with arguments now collect values in a form before inserting the rendered prompt. - [Mini Apps] Keyboard shortcuts such as Ctrl+Tab now work while focus is inside a Mini App. - [API Gateway] Fixed Agent requests to OpenAI reasoning models failing because compatible providers sent max_tokens instead of max_completion_tokens. - [Composer] Fixed the input box restoring an already-sent message after sending. - [Agent] Pi Agent citations now render as citation badges with a source list instead of literal markers. - [Search] Opening another conversation while message location is in progress no longer loads the entire history into the newly opened conversation. - [Migration] V1-to-v2 migration now skips IndexedDB records whose large backing value can no longer be read. - [Tools] Dependency installs now validate the executables a recipe actually provides, fixing false “installed but not runnable” errors. - [Artifacts] Fixed inline HTML previews continuously jumping between two heights when content ends with bare text. - [Chat] Fixed arrow-key scrolling jitter in message lists. - [Chat] Topic rename success now appears only after the title is saved, and automatic rename failures are reported. - [Citations] Switching conversations now closes the citations panel so sources from the previous conversation are not shown. - [OVMS] Downloaded OVMS models now remain visible in Model Manager even when server-side loading fails. - [Shortcuts] The clear-messages shortcut once again clears the active conversation. - [Shortcuts] Restored Ctrl+L as the clear-messages shortcut, with a confirmation step. - [Tabs] Message navigation and selection now target the correct tab when the same conversation is open more than once. - [Composer] Drafts, attachments, and structured tokens are preserved when the main process blocks a send request. - [Navigation] Actions no longer target the previous assistant or Agent conversation while a newly selected conversation is loading. - [Chat] Clearing messages from a non-active conversation now affects the selected conversation instead of the active one. - [Migration] Data-directory migration on Windows now copies symbolic-link targets as ordinary files and directories. - [Notes] The first click after renaming a note now switches to the selected note correctly. - [Windows] Restored Bash command output; if CLAUDE_CODE_GIT_BASH_PATH points to git-bash.exe, update it to Git Bash's bin/bash.exe. - [Chat] Conversation and Agent session titles no longer briefly revert after an inline rename. - [Agent] Reused empty Agent sessions now appear in the correct recent date group. - [Agent] V2 Agent identity and memory files are preserved when their old v1 source workspace no longer exists. - [Ollama] Fixed Claude Agent SDK system updates being dropped by some Ollama model templates. - [Tasks] Scheduled tasks now report accurate next-run times after pause, resume, manual runs, and restarts. - [CodeMate] Fixed stale provider selections interfering with the currently selected Code Mate configuration. - [Navigation] A chat window no longer remains on an assistant conversation that was deleted elsewhere. - [Search] Message-search navigation now stops loading old history after switching conversations. - [Agent] Continuing after an unanswered tool approval no longer fails with “No tool output found for tool call”. - [Code] Code blocks no longer flash as unhighlighted text before syntax highlighting appears. - [API Gateway] Agent sessions on non-Anthropic models no longer receive internal Agent SDK context as an assistant reply. - [Skills] Missing subagent dependencies now show a clear error, and unresolved CLI executables show a warning instead of silently producing unrelated output. - [API Gateway] Anthropic requests containing server tools without an input schema no longer fail the whole request on providers that cannot run those tools. - [Agent] Pi Agent citations now resolve for web-search results returned by third-party MCP tools. - [Tabs] Transient Mini App tabs no longer offer an unsupported Pin Tab action. - [Qwen] Fixed the thinking toggle for locally deployed Qwen models on OpenAI-compatible providers. - [Skills] Fixed GitHub Skill installation from raw links, repository-root descriptors, lowercase skill.md files, and explicit branches or tags. 💄 Improvements - [Agent] Improved Agent creation and editing with consistent navigation, clearer runtime choices, integrated avatar editing, and reliable permission defaults. - [Agent] Pi Agent now uses its official icon. - [Memory] Added a memory overview and clarified the roles of Agent File Memory, Knowledge Base, and MCP Memory after removal of v1 Global Memory. - [Agent] Added a back button to subagent flow details. - [Agent] Agent creation tools now refresh the Agent list and offer a Go to chat action for the newly created Agent. - [QuickPanel] Improved row layout so command and Skill names remain readable while descriptions use the available space. - [Launchpad] Refreshed built-in apps with dedicated multicolor icons and consistent sizing. - [Prompts] Aligned prompt editor action buttons for consistent sizing, shape, color, and icon scale. - [Models] Removed native number spinners from numeric model-setting fields. - [QuickPanel] Improved Skill-name readability. - [Models] Model selectors now identify duplicate names by provider and model ID. - [Composer] Truncated pasted links now show an ellipsis, reveal the full URL on hover, and display preview cards faster. - [Artifact Pane] Maximizing and restoring the right pane now animates smoothly without content jumps, overlap, or dark-mode transparency. - [Files] File-opening menus now show bounded application choices and remember the preferred app for each file type. - [PDF] PDF translation now shows accurate progress while required resources are downloaded. - [Agent] Agent questions now show their full title instead of truncating it to one line. - [Permissions] Improved permission-mode readability and restored warning tooltips. - [Assistant] Selection Assistant and Quick Assistant settings now show the active model and link directly to its global configuration. - [Providers] Action required: the retired GitHub Models provider has been removed; configure another provider if you previously relied on it. - [MCP] Built-in flomo and Nowledge Mem servers now use their real Streamable HTTP endpoints; existing servers migrate automatically, but flomo users must authorize once more. - [Privacy] The August 2026 privacy policy adds model-relay and third-party billing disclosures; users with anonymous data collection enabled must review it before analytics resumes. - [Permissions] Full Access now skips per-call approval for knowledge-base edits, image generation, CLI installation, and Assistant settings/file tools; choose Approve for Me or Ask Before Acting if these actions should still prompt. ⚡ Performance - [Installation] Reduced unpacked DeepSeek Harness runtime files to improve Cherry Studio installation speed. - [Painting] Painting history now loads independent file metadata concurrently. - [Screenshot] Capture overlays now appear immediately. - [Local Models] Hardware acceleration is now enabled by default for local inference. ``` Signed-off-by: kangfenmao <kangfenmao@qq.com>
What this PR does
Before this PR: Claude settings construction mixed environment, MCP, lifecycle state, hooks, and permission policy; fixed approvals were maintained as parallel tool-name lists, and
bypassPermissionsstill prompted inconsistently across runtimes.After this PR: settings construction is split by responsibility, Claude conduct rules use one declarative evaluator, Cherry-owned MCP tools declare structured approval entries, and Full Access bypasses per-call approvals consistently across Claude, Pi, and DSH while explicit safety blocks remain enforced.
Fixes # N/A
Why we need it and why it was done in this way
The following tradeoffs were made: SDK and bridge arrays/sets remain as derived boundary formats, while the structured tool policy registry is the only maintained source; Assistant-only entries are filtered by actual MCP availability.
The following alternatives were considered: a mutable GoF Builder, a hook-per-rule directory, and attaching cross-runtime policy to the AI-SDK-only
ToolEntrywere rejected because they preserve the wrong ownership or synchronization boundary.Links to places where the discussion took place: N/A
Breaking changes
Full Access (
bypassPermissions) no longer shows per-call approval cards for knowledge-base edits, image generation, CLI installation, or Assistant settings/file tools on any Agent runtime. Users who relied on those prompts should switch the Agent to Approve for Me or Ask Before Acting; disabled tools, shared-global installation blocks, protected built-in destructive-operation blocks, and unattended responder requirements remain enforced.Special notes for your reviewer
The seven commits intentionally separate mechanical responsibility extraction from the policy behavior change. Validation covered
pnpm lint,pnpm test:lint, formatting, type checks, i18n, documentation links, all main/renderer/package tests, and the DSH bridge's independent test/build; transient local disk pressure and two unrelated timing/teardown flakes passed on rerun.Checklist
This checklist is not enforcing, but it's a reminder of items that could be relevant to every PR.
Approvers are expected to review this list.
main/gh-pr-review,gh pr diff, or GitHub UI) before requesting review from othersRelease note