83. 工具描述规范统一 (Canonical Tool Descriptor v1.0)
状态: 🟢 P0/P1/P2/P3 全部完成,进入稳态观察期 日期: 2026-04-09 作用范围:
packages/cli+desktop-app-vue关联文档: 79. Coding Agent 系统 · 用户文档
1. 概述
Canonical Tool Descriptor 是 ChainlessChain Coding Agent 体系中跨 CLI、Electron Main、Renderer、MCP 四端的统一工具描述契约。它把原本分散在 CLI contract、Desktop core tools、unified tool registry、MCP 序列化结果、Renderer 消费层的 5 套工具定义,收敛为一份 shape:
一次定义 → 全端消费 ·
inputSchema为真源 ·parameters为只读兼容镜像
本项目不是新增工具能力,而是收口:让 Plan Mode、Permission Gate、function-calling schema、Renderer UI、MCP 展示页全部读取同一组字段,消除协议分叉。
2. 设计目标
| 目标 | 描述 |
|---|---|
| 单一真源 | CLI coding-agent-contract 为所有 core tool schema 的唯一来源 |
| Shape 统一 | core / managed / MCP / skill-linked tool 共享 descriptor 结构 |
| 语义固定 | risk、read-only、permission、plan 字段语义全端一致 |
| 兼容无损 | 旧 parameters 字段退化为 inputSchema 的镜像,不破坏旧消费方 |
| 读路径收敛 | 所有消费方遵循 inputSchema → parameters 回退链 |
| 可测性 | contract ↔ adapter、registry、IPC、Store、UI 全链路单测覆盖 |
3. 设计原则
3.1 内核定义,宿主派生
CLI runtime 是唯一的 schema 真源。Desktop 的 coding-agent-tool-adapter 只做派生,不重写字段;Desktop managed tool / MCP tool 只补齐 host 专有字段(如 telemetry、approval flow)。
3.2 inputSchema 优先,parameters 镜像
JSON Schema 是行业标准,inputSchema 是真源;parameters 作为兼容镜像,由 registry 自动从 inputSchema 派生,禁止手工双写。
3.3 权限语义必须随 descriptor 走
isReadOnly / riskLevel / availableInPlanMode / requiresPlanApproval 是 Permission Gate 的唯一输入。禁止在宿主侧用工具名硬编码白名单。
3.4 对外必 clone
unified-tool-registry 对外返回时必须 clone,避免 Renderer 或 IPC 消费方修改缓存中的共享对象。
3.5 渐进迁移,收敛兼容
先让所有写入方产出 canonical shape,再让读取方切到 inputSchema → parameters 顺序,最后收敛 parameters 为只读镜像。不搞"大爆炸"式重构。
4. 问题背景
在这次收口之前,仓库同时存在 5 套工具定义来源:
- CLI 工具 contract —
packages/cli/src/runtime/coding-agent-contract.js - Desktop core coding-agent tools — 手写静态 schema
- Desktop unified tool registry descriptor — 自定义字段
- MCP 工具序列化结果 — 直接透传第三方 shape
- Renderer — 混读
parameters/inputSchema/ 临时字段
导致的问题:
- function-calling schema 漂移:CLI 与 Desktop 同一工具字段不同
- risk 与 permission 语义不一致:Plan Mode 判断分叉
- Plan Mode 行为分叉:同一工具在 CLI 与 Desktop 的审批策略不同
- MCP 工具展示不稳定:
MCPSettings.vue难以生成统一测试表单 - 新增工具需要多处同步:工程成本高,容易漏改
5. Canonical Shape
interface CanonicalToolDescriptor {
// 身份
name: string; // 全局唯一 id
title?: string; // 人类可读名称
description?: string;
kind?: "builtin" | "mcp" | "skill" | string;
source?: string; // "cli-contract" | "mcp:<server>" | ...
category?: string; // filesystem / shell / git / network ...
// Schema
inputSchema: Record<string, unknown>; // JSON Schema — 真源
parameters: Record<string, unknown>; // 兼容镜像,由 registry 从 inputSchema 派生
// 权限 & Plan Mode
isReadOnly?: boolean;
riskLevel?: "low" | "medium" | "high";
permissions?: Record<string, unknown>;
availableInPlanMode?: boolean;
requiresPlanApproval?: boolean;
requiresConfirmation?: boolean;
approvalFlow?: string;
// Telemetry
telemetry?: Record<string, unknown>;
}关键约束:
name/inputSchema必填,其余可选parameters必须始终 =inputSchema(registry 层强制)- 缺省
availableInPlanMode视为false(默认收紧) - 缺省
riskLevel视为low(仅限isReadOnly=true的工具)
6. 系统架构
┌────────────────────────────────────────────────────────────┐
│ Renderer (Vue) │
│ unified-tools store · MCPSettings.vue │
│ read order: inputSchema → parameters │
└──────────────────────────┬─────────────────────────────────┘
│ IPC (unified-tools:list / mcp:list-tools)
┌──────────────────────────▼─────────────────────────────────┐
│ Electron Main (Host) │
│ unified-tool-registry (clone on read) │
│ unified-tools-ipc · mcp-ipc · function-caller │
│ context-engineering · coding-agent-tool-adapter │
│ coding-agent-permission-gate │
└──────────────────────────┬─────────────────────────────────┘
│ 派生 core tools
┌──────────────────────────▼─────────────────────────────────┐
│ packages/cli Runtime (Source of Truth) │
│ coding-agent-contract-shared.cjs │
│ coding-agent-contract.js · agent-core.js │
└────────────────────────────────────────────────────────────┘6.1 数据流(写路径)
Contract 注册 ──▶ Adapter 派生 ──▶ Registry 规范化 + 镜像 parameters ──▶ IPC 序列化 ──▶ Renderer store6.2 数据流(读路径)
所有消费方遵循统一的回退链:
const schema = tool.inputSchema ?? tool.parameters ?? {};6.3 分层职责
| 层 | 职责 | 不做的事 |
|---|---|---|
| CLI Contract | 定义真源 schema | 不感知 host |
| Desktop Adapter | 从 contract 派生 core tools,补齐 host 字段 | 不重写 schema |
| Unified Registry | 规范化、镜像 parameters、clone on read | 不执行工具 |
| IPC 层 | 序列化 canonical 字段 | 不重组字段 |
| Context Engineering | 组装 LLM prompt 工具列表 | 不绕过 registry |
| Permission Gate | 读 isReadOnly / riskLevel / requiresPlanApproval | 不用工具名白名单 |
| Renderer Store | 消费 canonical 字段 | 不猜字段 |
| MCPSettings.vue | 用 inputSchema 生成测试表单 | 不自定义字段映射 |
7. 关键设计决策
7.1 为什么保留 parameters
- 历史消费方(旧 MCP 服务器、第三方集成)仍依赖
parameters字段 - 一次性删除会破坏外部兼容性
- 改为由 registry 自动从
inputSchema镜像生成,成本低,渐进式可收敛
7.2 为什么不在 Desktop 重写 schema
- 重写会出现与 CLI 的漂移
- 派生方式让 CLI contract 成为唯一修改入口
- 减少新工具接入成本
7.3 为什么对外必须 clone
- Registry 内部用 Map 缓存 descriptor
- Renderer 与 IPC 直接引用会导致"远程修改缓存"
- clone on read 让缓存不可变,问题容易定位
7.4 为什么默认 Plan Mode 收紧
- 未标注
availableInPlanMode的工具默认按需要审批处理 - 宁可多拦不可漏拦,防止新工具漏审
- 只有在 contract 中显式声明
availableInPlanMode: true才会跳过审批
8. 实施进度
8.1 已完成 (P0)
- ✅ CLI coding-agent contract 成为 core 工具元数据与 schema 的唯一真源
- ✅ CLI
agent-core不再手写静态工具 schema - ✅ Desktop
coding-agent-tool-adapter从共享 contract 派生 core tools - ✅
agent-core不再依赖旧 runtime descriptor 常量处理 shell/git/mcp - ✅ Desktop managed tool 与 MCP tool 补齐 canonical 字段
- ✅
unified-tool-registry统一规范 builtin / MCP / skill-linked tools,对外 clone - ✅
context-engineering优先读inputSchema - ✅ Renderer
unified-toolsstore 消费 canonical 字段 - ✅
mcp:list-tools返回 canonical-ish 字段 - ✅
MCPSettings.vue用inputSchema生成测试表单 - ✅
function-caller统一inputSchema与parameters
8.2 已完成 (P1 / P2)
- ✅
tool-maskingcanonical 化:getAllToolDefinitions()/getAvailableToolDefinitions()输出 canonical schema,通过_projectCanonical()投影inputSchema与 canonical 字段,不再只吐旧parameters - ✅
function-caller通过buildMaskingPayload()把 canonical 字段 (title / riskLevel / isReadOnly / availableInPlanMode / requiresPlanApproval / telemetry …) 注入到 masking system,并在getAvailableTools()输出中保留 - ✅
computer-use-tools通过canonicalizeComputerUseTool()产出 canonical shape:read-only 工具 (browser_screenshot/desktop_screenshot/analyze_page) 标记为isReadOnly: true+riskLevel: "medium"+availableInPlanMode: true;其余工具默认riskLevel: "high"+requiresPlanApproval: true - ✅
getOpenAITools()/getClaudeTools()以inputSchema作为真源输出 function-calling schema
8.3 已完成 (P3 — 兼容层收敛)
- ✅ 全仓扫描
parameters写入点:确认不存在手工维护的inputSchema + parameters双写位置 —— 所有parameters: ...要么是 normalizer 的镜像赋值(tool-masking.js:66/348、unified-tool-registry.js:103、mcp-ipc.js:34、computer-use-tools.js:34、function-caller.js:74),要么是 FunctionCaller 工具注册端的 legacyparameters字面量(会被 normalizer 自动镜像成 canonical shape) - ✅
unified-tool-registry.js:588的 MCP 工具 FC 回填路径改为 canonical 读序:fcTool.inputSchema → fcTool.parameters → fcTool.function?.parameters → fcTool.input_schema - ✅ 读路径抽查:
context-engineering.js、mcp-ipc.js、MCPSettings.vue、unified-toolsstore、computer-use-tools.getOpenAITools/getClaudeTools已全部以inputSchema为首选、parameters仅作 fallback - ✅
CLAUDE-patterns.md新增《Canonical Tool Descriptor 规范》章节,固化 canonical shape、镜像方向、读序与禁止项
8.4 执行顺序(历史记录)
完成✅ (P1)tool-masking的 canonical 化清理剩余 main-process schema 消费方 (function-caller / computer-use-tools)✅ (P2)让 Renderer 与 MCP 展示层优先读 canonical 字段✅ (P3)- 读路径稳定观察一段时间后,再决定
parameters是否长期保留为兼容别名 (持续)
9. 验收标准
- 单一维护点:新增工具只需维护一处 canonical schema 来源
- 语义一致:core tool / managed tool / MCP tool / skill-linked tool 具备一致 descriptor 语义
- 权限统一:Plan Mode 与 Permission Gate 在所有链路读取同一组字段
- 读路径收敛:Renderer 不再 ad-hoc 猜字段,最多保留
inputSchema → parameters标准回退链 - 兼容层只读:旧
parameters仅作兼容镜像存在,不再作为第二个真源
10. 测试策略
| 链路 | 测试文件 |
|---|---|
| CLI contract 基线 | packages/cli/__tests__/unit/coding-agent-contract.test.js |
| CLI agent-core 派生 | packages/cli/__tests__/unit/agent-core.test.js |
| Desktop adapter 一致性 | desktop-app-vue/src/main/ai-engine/code-agent/__tests__/coding-agent-tool-adapter.test.js |
| Permission Gate | desktop-app-vue/src/main/ai-engine/code-agent/__tests__/coding-agent-permission-gate.test.js |
| Registry 归一化 | desktop-app-vue/src/main/ai-engine/__tests__/unified-tool-registry.test.js |
| Context engineering 序列化 | desktop-app-vue/src/main/llm/__tests__/context-engineering.test.js |
| Renderer store | desktop-app-vue/src/renderer/stores/__tests__/unified-tools.test.ts |
| Registry (tests 目录) | desktop-app-vue/tests/unit/ai-engine/unified-tool-registry.test.js |
| Function caller | desktop-app-vue/tests/unit/ai-engine/function-caller.test.js |
| MCP IPC 序列化 | desktop-app-vue/tests/unit/mcp/mcp-ipc.test.js |
| MCPSettings 组件 | desktop-app-vue/tests/unit/components/MCPSettings.test.js |
11. 关键文件
CLI (真源)
packages/cli/src/runtime/coding-agent-contract-shared.cjspackages/cli/src/runtime/coding-agent-contract.jspackages/cli/src/lib/agent-core.jspackages/cli/src/tools/legacy-agent-tools.js
Desktop Main
desktop-app-vue/src/main/ai-engine/code-agent/coding-agent-tool-adapter.jsdesktop-app-vue/src/main/ai-engine/code-agent/coding-agent-permission-gate.jsdesktop-app-vue/src/main/ai-engine/unified-tool-registry.jsdesktop-app-vue/src/main/ai-engine/unified-tools-ipc.jsdesktop-app-vue/src/main/ai-engine/function-caller.jsdesktop-app-vue/src/main/llm/context-engineering.jsdesktop-app-vue/src/main/mcp/mcp-ipc.js
Desktop Renderer
desktop-app-vue/src/renderer/stores/unified-tools.tsdesktop-app-vue/src/renderer/components/MCPSettings.vue
12. 风险与缓解
| 风险 | 缓解 |
|---|---|
| 兼容层长期存在导致"两个真源"漂移 | registry 层强制 parameters 由 inputSchema 派生,禁止手工双写 |
新接入 MCP 服务器未标注 riskLevel | 默认按 high 处理,强制在 code review 阶段补齐 |
| Renderer 缓存被污染 | registry 对外必 clone,单测覆盖 |
| Plan Mode 漏拦 | 缺省 availableInPlanMode 视为 false,默认收紧 |
| 工具数量增长导致 contract 文件膨胀 | 按 category 拆分子模块,保持 contract 文件可维护 |
13. 演进方向
- 工具能力元数据:在 canonical shape 中扩展 capability 字段(如
supportsStreaming、maxInputBytes) - 运行时动态注册:允许 MCP 工具在运行时注册并产出 canonical descriptor
- 跨仓库复用:把 canonical shape 提取为独立 npm 包,供其他 ChainlessChain 组件复用
14. 技能域过滤(Skill-scoped Tool Exposure, v5.0.2.9)
随着内置技能数量增长到 138+,每次 LLM 请求全量暴露所有 canonical tools 会造成"工具爆炸"问题:token 开销大、工具选择难。
API:UnifiedToolRegistry.getToolsForLLM(options) 新增两个可选参数,向后完全兼容。
// 旧行为:全部可用工具
registry.getToolsForLLM();
// 按激活的技能过滤
registry.getToolsForLLM({
activeSkillNames: "web-skill", // 单个或数组
alwaysAvailable: ["file_read", "file_write"], // 白名单
});语义:
activeSkillNames省略/为 null → 保留旧行为,返回所有available工具- 提供时 → 仅返回
skill.toolNames中声明的工具;alwaysAvailable中的工具总是保留(核心文件/读操作等) - 未知技能名静默忽略(不报错),便于调用方安全降级
设计取舍:
- 过滤在
UnifiedToolRegistry层实施,保证mcp-skill-generator自动为每个 MCP server 生成的 skill-shaped 分组也直接可被过滤 - "哪个技能当前激活" 的 runtime 追踪仍未在 agent loop 中落地,目前由调用方显式传入;下一步会在
context-engineering.js的buildOptimizedPrompt里消费
测试:
- 单元测试:
src/main/ai-engine/__tests__/unified-tool-registry.test.js(4 新增断言) - 集成测试:
tests/integration/canonical-tool-descriptor.integration.test.js(端到端 FC→Registry→LLM 投影验证) - 自动化迁移工具:扫描旧
parameters硬编码点,自动改写为inputSchema → parameters回退链
