Skip to content

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 套工具定义来源:

  1. CLI 工具 contractpackages/cli/src/runtime/coding-agent-contract.js
  2. Desktop core coding-agent tools — 手写静态 schema
  3. Desktop unified tool registry descriptor — 自定义字段
  4. MCP 工具序列化结果 — 直接透传第三方 shape
  5. Renderer — 混读 parameters / inputSchema / 临时字段

导致的问题:

  • function-calling schema 漂移:CLI 与 Desktop 同一工具字段不同
  • risk 与 permission 语义不一致:Plan Mode 判断分叉
  • Plan Mode 行为分叉:同一工具在 CLI 与 Desktop 的审批策略不同
  • MCP 工具展示不稳定MCPSettings.vue 难以生成统一测试表单
  • 新增工具需要多处同步:工程成本高,容易漏改

5. Canonical Shape

ts
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 store

6.2 数据流(读路径)

所有消费方遵循统一的回退链:

js
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 GateisReadOnly / riskLevel / requiresPlanApproval不用工具名白名单
Renderer Store消费 canonical 字段不猜字段
MCPSettings.vueinputSchema 生成测试表单不自定义字段映射

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)

  1. ✅ CLI coding-agent contract 成为 core 工具元数据与 schema 的唯一真源
  2. ✅ CLI agent-core 不再手写静态工具 schema
  3. ✅ Desktop coding-agent-tool-adapter 从共享 contract 派生 core tools
  4. agent-core 不再依赖旧 runtime descriptor 常量处理 shell/git/mcp
  5. ✅ Desktop managed tool 与 MCP tool 补齐 canonical 字段
  6. unified-tool-registry 统一规范 builtin / MCP / skill-linked tools,对外 clone
  7. context-engineering 优先读 inputSchema
  8. ✅ Renderer unified-tools store 消费 canonical 字段
  9. mcp:list-tools 返回 canonical-ish 字段
  10. MCPSettings.vueinputSchema 生成测试表单
  11. function-caller 统一 inputSchemaparameters

8.2 已完成 (P1 / P2)

  1. tool-masking canonical 化:getAllToolDefinitions() / getAvailableToolDefinitions() 输出 canonical schema,通过 _projectCanonical() 投影 inputSchema 与 canonical 字段,不再只吐旧 parameters
  2. function-caller 通过 buildMaskingPayload() 把 canonical 字段 (title / riskLevel / isReadOnly / availableInPlanMode / requiresPlanApproval / telemetry …) 注入到 masking system,并在 getAvailableTools() 输出中保留
  3. computer-use-tools 通过 canonicalizeComputerUseTool() 产出 canonical shape:read-only 工具 (browser_screenshot / desktop_screenshot / analyze_page) 标记为 isReadOnly: true + riskLevel: "medium" + availableInPlanMode: true;其余工具默认 riskLevel: "high" + requiresPlanApproval: true
  4. getOpenAITools() / getClaudeTools()inputSchema 作为真源输出 function-calling schema

8.3 已完成 (P3 — 兼容层收敛)

  1. ✅ 全仓扫描 parameters 写入点:确认不存在手工维护的 inputSchema + parameters 双写位置 —— 所有 parameters: ... 要么是 normalizer 的镜像赋值(tool-masking.js:66/348unified-tool-registry.js:103mcp-ipc.js:34computer-use-tools.js:34function-caller.js:74),要么是 FunctionCaller 工具注册端的 legacy parameters 字面量(会被 normalizer 自动镜像成 canonical shape)
  2. unified-tool-registry.js:588 的 MCP 工具 FC 回填路径改为 canonical 读序:fcTool.inputSchema → fcTool.parameters → fcTool.function?.parameters → fcTool.input_schema
  3. ✅ 读路径抽查:context-engineering.jsmcp-ipc.jsMCPSettings.vueunified-tools store、computer-use-tools.getOpenAITools/getClaudeTools 已全部以 inputSchema 为首选、parameters 仅作 fallback
  4. CLAUDE-patterns.md 新增《Canonical Tool Descriptor 规范》章节,固化 canonical shape、镜像方向、读序与禁止项

8.4 执行顺序(历史记录)

  1. 完成 tool-masking 的 canonical 化 ✅ (P1)
  2. 清理剩余 main-process schema 消费方 (function-caller / computer-use-tools) ✅ (P2)
  3. 让 Renderer 与 MCP 展示层优先读 canonical 字段 ✅ (P3)
  4. 读路径稳定观察一段时间后,再决定 parameters 是否长期保留为兼容别名 (持续)

9. 验收标准

  1. 单一维护点:新增工具只需维护一处 canonical schema 来源
  2. 语义一致:core tool / managed tool / MCP tool / skill-linked tool 具备一致 descriptor 语义
  3. 权限统一:Plan Mode 与 Permission Gate 在所有链路读取同一组字段
  4. 读路径收敛:Renderer 不再 ad-hoc 猜字段,最多保留 inputSchema → parameters 标准回退链
  5. 兼容层只读:旧 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 Gatedesktop-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 storedesktop-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 callerdesktop-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.cjs
  • packages/cli/src/runtime/coding-agent-contract.js
  • packages/cli/src/lib/agent-core.js
  • packages/cli/src/tools/legacy-agent-tools.js

Desktop Main

  • desktop-app-vue/src/main/ai-engine/code-agent/coding-agent-tool-adapter.js
  • desktop-app-vue/src/main/ai-engine/code-agent/coding-agent-permission-gate.js
  • desktop-app-vue/src/main/ai-engine/unified-tool-registry.js
  • desktop-app-vue/src/main/ai-engine/unified-tools-ipc.js
  • desktop-app-vue/src/main/ai-engine/function-caller.js
  • desktop-app-vue/src/main/llm/context-engineering.js
  • desktop-app-vue/src/main/mcp/mcp-ipc.js

Desktop Renderer

  • desktop-app-vue/src/renderer/stores/unified-tools.ts
  • desktop-app-vue/src/renderer/components/MCPSettings.vue

12. 风险与缓解

风险缓解
兼容层长期存在导致"两个真源"漂移registry 层强制 parametersinputSchema 派生,禁止手工双写
新接入 MCP 服务器未标注 riskLevel默认按 high 处理,强制在 code review 阶段补齐
Renderer 缓存被污染registry 对外必 clone,单测覆盖
Plan Mode 漏拦缺省 availableInPlanMode 视为 false,默认收紧
工具数量增长导致 contract 文件膨胀按 category 拆分子模块,保持 contract 文件可维护

13. 演进方向

  • 工具能力元数据:在 canonical shape 中扩展 capability 字段(如 supportsStreamingmaxInputBytes
  • 运行时动态注册:允许 MCP 工具在运行时注册并产出 canonical descriptor
  • 跨仓库复用:把 canonical shape 提取为独立 npm 包,供其他 ChainlessChain 组件复用

14. 技能域过滤(Skill-scoped Tool Exposure, v5.0.2.9)

随着内置技能数量增长到 138+,每次 LLM 请求全量暴露所有 canonical tools 会造成"工具爆炸"问题:token 开销大、工具选择难。

APIUnifiedToolRegistry.getToolsForLLM(options) 新增两个可选参数,向后完全兼容。

js
// 旧行为:全部可用工具
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.jsbuildOptimizedPrompt 里消费

测试

  • 单元测试: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 回退链

基于 MIT 许可发布