v2-refactor-temp/docs/ai/declarative-tool-registry.md
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. Theclawserver was renamed and merged intocherry-tools; the autonomy tools' final names aremcp__cherry-tools__cron/notify/config. References toclaw/ soul gating below are historical context, not current state.Update (skill-install-visibility-sync): the
skillsMCP server is now wired into every session bybuildMcpServers(), exposing two tools —search_skills(read-only marketplace search, auto-approved) andinstall_skill(clones + installs exactly one skill viaSkillService.install, from aninstall_sourcereturned bysearch_skillsin the same MCP server session).install_skillis not in the explicit auto-approve list: the Claude Agent SDK applies the configured permission mode, so default /acceptEditscan request approval andbypassPermissionsruns 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 thatreconcileSkillscatalogs. The "defined but unmounted / not yet wired" notes below are historical.
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 removed MultiEdit/NotebookRead, omits Agent/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 in ToolHeader.tsx.src/shared/ai/claudecode/constants.ts — GLOBALLY_DISALLOWED_TOOLS, SOUL_MODE_DISALLOWED_TOOLS (mode-gated, not tool-granular).settingsBuilder.buildMcpServers(): cherry-tools (always), claw (cron/notify/config — soul only), agent-memory (memory — soul only), assistant (assistant only), skills (wired for every session — search_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.
sdk-tools.d.ts impossible — type-only).0.3.145 → 0.3.168. Workflow stays available (the motivating feature).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.disallowedTools (hard block). Per-tool approval removed — approval governed solely by permission_mode cards.category.disabledTools JSON column (empty = all enabled). allowedTools retired.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.NotebookEdit → disabled (not needed). TodoWrite → disabled (superseded by Task*).Agent + agent-teams (SendMessage/TeamCreate/TeamDelete) → internal. Workflow → user.ScheduleWakeup/RemoteTrigger/Monitor/PushNotification → disabled (CLI-oriented).EnterWorktree/ExitWorktree → conditional (workspace-has-git), pair-grouped.cron → user; agent-memory memory → user; skills search_skills/install_skill → internal, wired for every session (install_skill follows the SDK permission mode, with only no-responder headless turns denied); claw notify + config → user (no channel gate; notify self-degrades at call time when no channel is connected).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.
agent/toolBinding.tsx)TOOL_UI_BINDINGS satisfies Record<ClaudeToolKey, { icon; render? }> — compile-time coverage guard. Replaces toolRenderers + ToolHeader switches; UnknownToolRenderer stays as fallback.
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).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):
exposure:'disabled' → always disallowed (subsumes GLOBALLY_DISALLOWED_TOOLS; delete that constant).exposure:'internal' → never added.exposure:'conditional' → disallowed iff its condition predicate is false in ctx.exposure:'user' → disallowed iff its name or any pairGroup sibling is in agent.disabledTools.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 = 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).
_sdkCoverage assertion per kind:'sdk' && sdkTyped key).scripts/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.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 |
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 |
| search_skills | skills | context | internal | read-only marketplace search (auto-approved) |
| install_skill | skills | context | internal | installs one marketplace skill; follows SDK permission mode + no-responder headless guard |
| cron | cherry-tools | orchestration | user | app scheduler (≠ SDK Cron*) |
| notify | cherry-tools | orchestration | user | IM channel push; self-degrades at call time if no channel |
| config | cherry-tools | orchestration | user | agent self-config (rename/channels) |
Future: media tools (image gen, audio/video) → category:'media'; notes → category:'context'.
asarUnpack; add new union members' type aliases to types.ts. Verify: pnpm typecheck, app boots, agent runs Bash+Read.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.disabledTools column + schema + migration. Existing agents unaffected (empty set).disabledTools; group by category; one switch per pairGroup; show only exposure==='user'; drop per-tool approve.agent.tools.<Key>.label/.description in en/zh-cn/zh-tw; migrate getAgentToolLabel.TOOL_UI_BINDINGS + registry-driven ToolHeader; delete builtinTools.ts.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.exposure:'disabled' correctness (high): WebSearch/WebFetch hard-disabled today; mis-encoding re-grants them. PR-2 snapshot assertion is the safety net..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.SendMessage/Team* can't be union-guarded; need explicit internal entries + bindings or they fall to UnknownToolRenderer.disabledTools/ctx re-key the warm signature; applies next session, not mid-session.