Back to Cherry Studio

Declarative Claude Code Tool Registry

v2-refactor-temp/docs/ai/declarative-tool-registry.md

2.0.014.0 KB
Original Source

Declarative Claude Code Tool Registry

Design doc. Status: approved, implementation in progress (PR-by-PR).

Update (PR #16726): soul mode and its tool gating are gone — soul_enabled, SOUL_MODE_DISALLOWED_TOOLS, and the PR-7 de-gating items below have landed. The claw server was renamed and merged into cherry-tools; the autonomy tools' final names are mcp__cherry-tools__cron/notify/config. References to claw / soul gating below are historical context, not current state.

Update (skill-install-visibility-sync): the skills MCP server is now wired into every session by buildMcpServers(), exposing two tools — search_skills (read-only marketplace search, auto-approved) and install_skill (clones + installs exactly one skill via SkillService.install, from an install_source returned by search_skills in the same MCP server session). install_skill is not in the explicit auto-approve list: the Claude Agent SDK applies the configured permission mode, so default / acceptEdits can request approval and bypassPermissions runs directly. A PreToolUse hook only denies headless turns whose mode still requires an interactive responder. Skill authoring is not a tool — skill-creator writes files that reconcileSkills catalogs. The "defined but unmounted / not yet wired" notes below are historical.

Context

The Claude Code agent tool set is hand-written and duplicated across 3+ drifting places, assembled from disconnected mechanisms:

  • src/shared/ai/claudecode/builtinTools.tsclaudeCodeBuiltinTools (13 tools; policy + edit-dialog catalog). Stale (lists removed MultiEdit/NotebookRead, omits Agent/BashOutput/…).
  • src/renderer/.../tools/agent/types.tsAgentToolsType (~29 names) + per-tool input/output types (type-only import from @anthropic-ai/claude-agent-sdk/sdk-tools).
  • src/renderer/.../tools/agent/toolRendererRegistry.tsx + icon/label switches in ToolHeader.tsx.
  • src/shared/ai/claudecode/constants.tsGLOBALLY_DISALLOWED_TOOLS, SOUL_MODE_DISALLOWED_TOOLS (mode-gated, not tool-granular).
  • In-process MCP servers, each gated differently in settingsBuilder.buildMcpServers(): cherry-tools (always), claw (cron/notify/config — soul only), agent-memory (memory — soul only), assistant (assistant only), skills (wired for every sessionsearch_skills + install_skill; see the update at the top).

Goal: one declarative registry = single source of truth for policy (main), catalog UI (renderer), and chat rendering (renderer), covering both SDK-native and in-process MCP tools, classified at tool granularity (no all-or-nothing soul gating; some tools gated by runtime conditions). Plus: upgrade the SDK for the Workflow tool, make the toggle a real enable/disable, categorize + i18n the UI.

Decisions

  1. Hand-authored registry + CI drift guard (pure import from sdk-tools.d.ts impossible — type-only).
  2. Upgrade SDK 0.3.1450.3.168. Workflow stays available (the motivating feature).
  3. Declarative exposure per tool: user (shown, toggleable) · internal (always on, hidden) · conditional (on iff a runtime predicate holds, hidden) · disabled (always blocked). Plus pairGroup for atomic pair toggling.
  4. Built-in toggle = real enable/disable via SDK disallowedTools (hard block). Per-tool approval removed — approval governed solely by permission_mode cards.
  5. App in-process tools fold into the built-in tab by category.
  6. Single opt-out disabledTools JSON column (empty = all enabled). allowedTools retired.
  7. Categories: shell | file | search | orchestration | media | context. context = tools that feed the agent external/persistent context: web, knowledge, memory, skills, notes(future). search keeps only local Glob/Grep.
  8. Drop soul-mode tool gating; classify at tool granularity. Soul mode's non-tool effects (prompt personality) are out of scope.
  9. Final classifications:
    • NotebookEditdisabled (not needed). TodoWritedisabled (superseded by Task*).
    • Agent + agent-teams (SendMessage/TeamCreate/TeamDelete) → internal. Workflowuser.
    • ScheduleWakeup/RemoteTrigger/Monitor/PushNotificationdisabled (CLI-oriented).
    • EnterWorktree/ExitWorktreeconditional (workspace-has-git), pair-grouped.
    • claw cronuser; agent-memory memoryuser; skills search_skills/install_skillinternal, wired for every session (install_skill follows the SDK permission mode, with only no-responder headless turns denied); claw notify + configuser (no channel gate; notify self-degrades at call time when no channel is connected).

Architecture — 3 layers

Layer 1 — shared descriptor (new src/shared/ai/claudecode/toolRegistry.ts)

ts
export type ClaudeToolCategory = 'shell' | 'file' | 'search' | 'orchestration' | 'media' | 'context'
export type ClaudeToolExposure = 'user' | 'internal' | 'conditional' | 'disabled'
export type ClaudeToolCondition = 'workspace-has-git' | 'agent-has-channel'
export type ClaudeToolPairGroup = 'worktree' | 'planMode' | 'bash' | 'taskCluster' | 'mcpResource'

interface ClaudeToolDescriptorDef {
  name: string                 // runtime tool name == write-back id (SDK bare name, or mcp__server__wire)
  category: ClaudeToolCategory
  exposure: ClaudeToolExposure
  condition?: ClaudeToolCondition          // required iff exposure==='conditional'
  pairGroup?: ClaudeToolPairGroup
  labelKey: string; descriptionKey: string // i18n keys
  kind: 'sdk' | 'mcp'
  sdkTyped?: boolean           // kind:'sdk'; false = runtime/experimental, absent from ToolInputSchemas (teams); guard skips
  mcpServer?: 'cherry-tools' | 'claw' | 'agent-memory' | 'skills'  // kind:'mcp'
  mcpWireName?: string
}
export const CLAUDE_TOOL_REGISTRY = { /* … */ } as const satisfies Record<string, ClaudeToolDescriptorDef>
export type ClaudeToolKey = keyof typeof CLAUDE_TOOL_REGISTRY

Approval metadata dropped (decision #4). AgentToolsType derived from the registry.

Layer 2 — renderer binding (new agent/toolBinding.tsx)

TOOL_UI_BINDINGS satisfies Record<ClaudeToolKey, { icon; render? }> — compile-time coverage guard. Replaces toolRenderers + ToolHeader switches; UnknownToolRenderer stays as fallback.

Layer 3 — policy & MCP injection (main)

  • useAgentTools.ts + agentTools.ts source descriptors from the registry.
  • settingsBuilder.buildToolPermissions()resolveDisallowedTools(agent, ctx).
  • settingsBuilder.buildMcpServers() injects cherry-tools and agent-memory for every session; per-tool policy is enforced by disallowedTools/hooks, not by mounting/unmounting the server. skills is injected for every session too (search_skills auto-approved; install_skill governed by the SDK permission mode, with a no-responder headless guard).

Enable/disable model

  • New Drizzle column disabledTools: text({mode:'json'}).$type<string[]>().notNull().default('[]') in agent.ts; add to AgentBaseSchema + AGENT_MUTABLE_FIELDS.
  • resolveDisallowedTools(agent, ctx) in toolRegistry.ts, where ctx = { workspaceHasGit: boolean; agentHasChannel: boolean } (resolved in main: .git stat on cwd; channelService.listChannels):
    1. exposure:'disabled' → always disallowed (subsumes GLOBALLY_DISALLOWED_TOOLS; delete that constant).
    2. exposure:'internal' → never added.
    3. exposure:'conditional' → disallowed iff its condition predicate is false in ctx.
    4. exposure:'user' → disallowed iff its name or any pairGroup sibling is in agent.disabledTools.
    5. pairGroup atomicity — re-expand groups (a disabled/condition-false member disables the whole group, incl. internal siblings).
    6. kind:'mcp' disabled tools also drop from adjustAllowedToolsForMcp's ensure-list + add mcp__server__wire to disallowed.
  • SOUL_MODE_DISALLOWED_TOOLS + soul_enabled server gates removed, replaced by per-tool exposure. Assistant-mode AskUserQuestion disable stays as a thin overlay.
  • allowedTools retired (UI stops writing; backend passes empty → SDK permits all, gated by canUseTool + disallowedTools).

Approval & ToolApprovalRegistry

Approval = permission_mode cards + read-only default-safe set. Round-trip unchanged. A disabled/condition-false tool is in disallowedTools → removed from context → never reaches canUseTool (cover with a test).

CI drift guard

  • Removals → typecheck failure (named SDK-type imports + _sdkCoverage assertion per kind:'sdk' && sdkTyped key).
  • Additionsscripts/check-claude-tools.ts (mirrors scripts/check-i18n.ts): parse ToolInputSchemas/ToolOutputSchemas member identifiers via TS compiler API, strip Input/Output, apply SDK→name alias map, diff vs registry kind:'sdk' && sdkTyped keys → CI fails on any unmapped new SDK builtin.
  • sdkTyped:false (teams) + kind:'mcp' tools are hand-tracked, exempt from the union guard.

Final classification

SDK-native (kind:'sdk')

Toolcategoryexposurepairnote
Bashshelluserbash
BashOutputshellinternalbash
Read / Edit / Writefileuser
NotebookEditfiledisabled
Glob / Grepsearchuser
Agentorchestrationinternalsubagent spawn
SendMessage / TeamCreate / TeamDeleteorchestrationinternalsdkTyped:false (teams, runtime flag)
Taskorchestrationinternallegacy render-only alias
TaskCreate/Get/Update/List/Stop/OutputorchestrationinternaltaskCluster
TodoWriteorchestrationdisabled
ExitPlanMode / EnterPlanModeorchestrationinternalplanMode
EnterWorktree / ExitWorktreeorchestrationconditional: workspace-has-gitworktree
AskUserQuestionorchestrationinternalassistant overlay disables
ToolSearchorchestrationinternal
ListMcpResources / ReadMcpResourceorchestrationinternalmcpResource
Workfloworchestrationuser
WebSearch / WebFetch (native)contextdisabledreplaced by cherry
REPLshelldisabled
CronCreate / CronDelete / CronListorchestrationdisabled
ScheduleWakeup / RemoteTrigger / Monitor / PushNotificationorchestrationdisabled

In-process MCP (kind:'mcp')

Tool (wire)servercategoryexposurenote
web_search / web_fetchcherry-toolscontextuserreplaces native Web*
kb_searchcherry-toolscontextuser
kb_listcherry-toolscontextinternal
memoryagent-memorycontextusercross-session FACT.md/JOURNAL
search_skillsskillscontextinternalread-only marketplace search (auto-approved)
install_skillskillscontextinternalinstalls one marketplace skill; follows SDK permission mode + no-responder headless guard
croncherry-toolsorchestrationuserapp scheduler (≠ SDK Cron*)
notifycherry-toolsorchestrationuserIM channel push; self-degrades at call time if no channel
configcherry-toolsorchestrationuseragent self-config (rename/channels)

Future: media tools (image gen, audio/video) → category:'media'; notes → category:'context'.

PR sequence

  • PR-1 — SDK upgrade 0.3.145 → 0.3.168. Bump root + 8 native optional deps + verify asarUnpack; add new union members' type aliases to types.ts. Verify: pnpm typecheck, app boots, agent runs Bash+Read.
  • PR-2 — shared registry + policy + CI guard (static exposures only). toolRegistry.ts, resolveDisallowedTools (treats conditional as internal for now — no ctx yet), scripts/check-claude-tools.ts. Rewire agentTools.ts/useAgentTools.ts. Snapshot-assert disallowed set == today for non-soul agents.
  • PR-3 — disabledTools column + schema + migration. Existing agents unaffected (empty set).
  • PR-4 — edit-dialog UI: real enable/disable + category sections + cherry fold-in. Toggle writes disabledTools; group by category; one switch per pairGroup; show only exposure==='user'; drop per-tool approve.
  • PR-5 — i18n. agent.tools.<Key>.label/.description in en/zh-cn/zh-tw; migrate getAgentToolLabel.
  • PR-6 — render-registry unification + cleanup. TOOL_UI_BINDINGS + registry-driven ToolHeader; delete builtinTools.ts.
  • PR-7 — conditional exposure + in-process MCP de-soul-gating. Introduce the ctx predicates (workspace-has-git via .git stat; agent-has-channel via channelService); enforce conditional. De-soul-gate cherry-tools/agent-memory by injecting them for every session and enforcing per-tool policy through disallowedTools/hooks; remove SOUL_MODE_DISALLOWED_TOOLS + soul server gates. (skills is now wired — see the skill-install-visibility-sync update at the top.) Behavior change — explicit before/after, tested.

Risks

  • exposure:'disabled' correctness (high): WebSearch/WebFetch hard-disabled today; mis-encoding re-grants them. PR-2 snapshot assertion is the safety net.
  • De-soul-gating + conditional (high): PR-7 changes which agents see claw/memory/worktree. Must be explicit + tested; existing soul agents keep their tools; conditional predicates must be cheap (cache .git stat / channel count per session build).
  • skills wiring (done): wired in skill-install-visibility-sync. search_skills is read-only (auto-approved); install_skill follows the SDK permission mode, while a PreToolUse hook denies only headless turns that still require an interactive responder. Install resolves the exact repo directory and refuses to overwrite builtin/system/local/other-source skills.
  • Teams outside typed union: SendMessage/Team* can't be union-guarded; need explicit internal entries + bindings or they fall to UnknownToolRenderer.
  • Warm-query staleness (low): disabledTools/ctx re-key the warm signature; applies next session, not mid-session.