Co-authored-by: jdzhang <625013594@qq.com> Co-authored-by: gujiaming <52187003+AtomsH4@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: kangfenmao <kangfenmao@qq.com> Co-authored-by: jd <59188306+zhangjiadi225@users.noreply.github.com> Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com> Signed-off-by: gujiaming <52187003+AtomsH4@users.noreply.github.com> Signed-off-by: suyao <sy20010504@gmail.com> Signed-off-by: kangfenmao <kangfenmao@qq.com> Signed-off-by: jdzhang <625013594@qq.com> Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
12 KiB
Declarative Claude Code Tool Registry
Design doc. Status: approved, implementation in progress (PR-by-PR).
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.ts—claudeCodeBuiltinTools(13 tools; policy + edit-dialog catalog). Stale (lists removedMultiEdit/NotebookRead, omitsAgent/BashOutput/…).src/renderer/.../tools/agent/types.ts—AgentToolsType(~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 inToolHeader.tsx.src/shared/ai/claudecode/constants.ts—GLOBALLY_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(defined but wired nowhere).
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
- Hand-authored registry + CI drift guard (pure import from
sdk-tools.d.tsimpossible — type-only). - Upgrade SDK
0.3.145→0.3.168. Workflow stays available (the motivating feature). - Declarative
exposureper tool:user(shown, toggleable) ·internal(always on, hidden) ·conditional(on iff a runtime predicate holds, hidden) ·disabled(always blocked). PluspairGroupfor atomic pair toggling. - Built-in toggle = real enable/disable via SDK
disallowedTools(hard block). Per-tool approval removed — approval governed solely bypermission_modecards. - App in-process tools fold into the built-in tab by
category. - Single opt-out
disabledToolsJSON column (empty = all enabled).allowedToolsretired. - Categories:
shell | file | search | orchestration | media | context.context= tools that feed the agent external/persistent context: web, knowledge, memory, skills, notes(future).searchkeeps only localGlob/Grep. - Drop soul-mode tool gating; classify at tool granularity. Soul mode's non-tool effects (prompt personality) are out of scope.
- Final classifications:
NotebookEdit→ disabled (not needed).TodoWrite→ disabled (superseded byTask*).Agent+ agent-teams (SendMessage/TeamCreate/TeamDelete) → internal.Workflow→ user.ScheduleWakeup/RemoteTrigger/Monitor/PushNotification→ disabled (CLI-oriented).EnterWorktree/ExitWorktree→ conditional (workspace-has-git), pair-grouped.- claw
cron→ user;agent-memorymemory→ user;skills→ internal (and newly wired); clawnotify+config→ conditional (agent-has-channel).
Architecture — 3 layers
Layer 1 — shared descriptor (new src/shared/ai/claudecode/toolRegistry.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.tssource descriptors from the registry.settingsBuilder.buildToolPermissions()→resolveDisallowedTools(agent, ctx).settingsBuilder.buildMcpServers()injects an in-process server iff ≥1 of its tools is effectively enabled (replaces theif (soulEnabled)gates; wiresskills).
Enable/disable model
- New Drizzle column
disabledTools: text({mode:'json'}).$type<string[]>().notNull().default('[]')inagent.ts; add toAgentBaseSchema+AGENT_MUTABLE_FIELDS. resolveDisallowedTools(agent, ctx)intoolRegistry.ts, wherectx = { workspaceHasGit: boolean; agentHasChannel: boolean }(resolved in main:.gitstat on cwd;channelService.listChannels):exposure:'disabled'→ always disallowed (subsumesGLOBALLY_DISALLOWED_TOOLS; delete that constant).exposure:'internal'→ never added.exposure:'conditional'→ disallowed iff itsconditionpredicate is false inctx.exposure:'user'→ disallowed iff its name or anypairGroupsibling is inagent.disabledTools.- pairGroup atomicity — re-expand groups (a disabled/condition-false member disables the whole group, incl. internal siblings).
kind:'mcp'disabled tools also drop fromadjustAllowedToolsForMcp's ensure-list + addmcp__server__wireto disallowed.
SOUL_MODE_DISALLOWED_TOOLS+soul_enabledserver gates removed, replaced by per-tool exposure. Assistant-modeAskUserQuestiondisable stays as a thin overlay.allowedToolsretired (UI stops writing; backend passes empty → SDK permits all, gated bycanUseTool+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 +
_sdkCoverageassertion perkind:'sdk' && sdkTypedkey). - Additions →
scripts/check-claude-tools.ts(mirrorsscripts/check-i18n.ts): parseToolInputSchemas/ToolOutputSchemasmember identifiers via TS compiler API, stripInput/Output, apply SDK→name alias map, diff vs registrykind:'sdk' && sdkTypedkeys → 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')
| Tool | category | exposure | pair | note |
|---|---|---|---|---|
| Bash | shell | user | bash | |
| BashOutput | shell | internal | bash | |
| Read / Edit / Write | file | user | ||
| NotebookEdit | file | disabled | ||
| Glob / Grep | search | user | ||
| Agent | orchestration | internal | subagent spawn | |
| SendMessage / TeamCreate / TeamDelete | orchestration | internal | sdkTyped:false (teams, runtime flag) |
|
| Task | orchestration | internal | legacy render-only alias | |
| TaskCreate/Get/Update/List/Stop/Output | orchestration | internal | taskCluster | |
| TodoWrite | orchestration | disabled | ||
| ExitPlanMode / EnterPlanMode | orchestration | internal | planMode | |
| EnterWorktree / ExitWorktree | orchestration | conditional: workspace-has-git | worktree | |
| AskUserQuestion | orchestration | internal | assistant overlay disables | |
| ToolSearch | orchestration | internal | ||
| ListMcpResources / ReadMcpResource | orchestration | internal | mcpResource | |
| Workflow | orchestration | user | ||
| WebSearch / WebFetch (native) | context | disabled | replaced by cherry | |
| REPL | shell | disabled | ||
| CronCreate / CronDelete / CronList | orchestration | disabled | ||
| ScheduleWakeup / RemoteTrigger / Monitor / PushNotification | orchestration | disabled |
In-process MCP (kind:'mcp')
| Tool (wire) | server | category | exposure | note |
|---|---|---|---|---|
| web_search / web_fetch | cherry-tools | context | user | replaces native Web* |
| kb_search | cherry-tools | context | user | |
| kb_list | cherry-tools | context | internal | |
| memory | agent-memory | context | user | cross-session FACT.md/JOURNAL |
| skills | skills | context | internal | newly wired (currently injected nowhere) |
| cron | claw | orchestration | user | app scheduler (≠ SDK Cron*) |
| notify | claw | orchestration | conditional: agent-has-channel | IM channel push |
| config | claw | orchestration | conditional: agent-has-channel | agent 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 totypes.ts. Verify:pnpm typecheck, app boots, agent runs Bash+Read. - PR-2 — shared registry + policy + CI guard (static exposures only).
toolRegistry.ts,resolveDisallowedTools(treatsconditionalasinternalfor now — noctxyet),scripts/check-claude-tools.ts. RewireagentTools.ts/useAgentTools.ts. Snapshot-assert disallowed set == today for non-soul agents. - PR-3 —
disabledToolscolumn + schema + migration. Existing agents unaffected (empty set). - PR-4 — edit-dialog UI: real enable/disable + category sections + cherry fold-in. Toggle writes
disabledTools; group bycategory; one switch perpairGroup; show onlyexposure==='user'; drop per-tool approve. - PR-5 — i18n.
agent.tools.<Key>.label/.descriptionin en/zh-cn/zh-tw; migrategetAgentToolLabel. - PR-6 — render-registry unification + cleanup.
TOOL_UI_BINDINGS+ registry-drivenToolHeader; deletebuiltinTools.ts. - PR-7 — conditional exposure + in-process MCP de-soul-gating. Introduce the
ctxpredicates (workspace-has-gitvia.gitstat;agent-has-channelviachannelService); enforceconditional. De-soul-gate claw/agent-memory; wireskills; inject servers per-tool-enabled; removeSOUL_MODE_DISALLOWED_TOOLS+ soul server gates. 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/skills/worktree. Must be explicit + tested; existing soul agents keep their tools; conditional predicates must be cheap (cache
.gitstat / channel count per session build). skillsnewly wired: confirm marketplace install/authoring is safe to expose (internal = on for every agent).- Teams outside typed union:
SendMessage/Team*can't be union-guarded; need explicitinternalentries + bindings or they fall toUnknownToolRenderer. - Warm-query staleness (low):
disabledTools/ctxre-key the warm signature; applies next session, not mid-session.