Agent CLI深水区 - 命令技能插件系统 · AI Agent

Agent CLI 深水区 09:命令、技能与插件系统

三者区别

Agent CLI 里经常有 command、skill、plugin,容易混。

名称本质谁执行
Command确定性 CLI 命令程序
Skill可复用任务说明书模型 + 工具
Plugin扩展工具/命令/资源程序加载

例子:

输入类型
/helpCommand
/mcp listCommand
“review 这次改动”触发代码审查流程Skill
新增一个 Jira 工具包Plugin

Command 系统

Command 不应该交给模型。

代码块TS · 16 行收起展开
// Command = 一个 slash command 的定义。
// Command 是程序确定性执行的控制面,不应该让模型自由发挥。
type Command = {
  name: string;        // 命令名,例如 /help、/cost、/mcp。
  description: string; // 给 /help 展示的人类说明。
  usage: string;       // 用法示例,例如 "/mcp list"。
  run(args: string[], ctx: CommandContext): Promise<void>; // 命令真正执行的函数。
};

// CommandContext = 命令执行时能看到的运行时信息。
// 不要把整个 AppRuntime 暴露给命令,避免命令乱改内部状态。
type CommandContext = {
  cwd: string;       // 当前工作目录。
  sessionId: string; // 当前会话 ID,用于查询成本、历史等。
  config: AppConfig; // 已加载配置。
};

注册:

代码块TS · 27 行收起展开
// commands = 命令注册表。
// key 是命令名,value 是命令处理器。
const commands = new Map<string, Command>();

// registerCommand = 注册一个 slash command。
// 真实系统里这里还要检查重名,尤其不能让插件覆盖内置命令。
export function registerCommand(command: Command) {
  commands.set(command.name, command);
}

// runCommand = 执行用户输入的 slash command。
export async function runCommand(line: string, ctx: CommandContext) {
  // 简化解析:按空白切分。真实系统要支持 flags、引号、子命令。
  const [name, ...args] = line.trim().split(/\s+/);

  // 根据命令名查注册表。
  const command = commands.get(name);

  if (!command) {
    // 命令不存在时直接给确定性错误,不要交给模型编答案。
    console.log(`Unknown command: ${name}`);
    return;
  }

  // 执行命令。这里不经过模型。
  await command.run(args, ctx);
}

Command 例子:/cost

代码块TS · 14 行收起展开
// 注册一个 /cost 命令。
// 它只是读取当前 session 的 token/cost 统计,不需要模型参与。
registerCommand({
  name: "/cost",
  description: "Show token usage and estimated cost.",
  usage: "/cost",
  async run(_args, ctx) {
    // 根据 sessionId 找到这次会话的成本统计。
    const report = await loadCostReport(ctx.sessionId);

    // 格式化后直接打印到终端。
    console.log(formatCostReport(report));
  }
});

/cost 是确定性查询,不需要模型。

Skill 系统

Skill 是一份 Markdown 说明,告诉 Agent 遇到某类任务时怎么做。

代码块TS · 9 行收起展开
// Skill = 一份可复用任务规程。
// Skill 不直接执行动作,而是被注入给模型,影响模型怎么做事。
type Skill = {
  name: string;         // 稳定名字,例如 code-review。
  description: string;  // 说明适用场景,用于匹配和展示。
  triggers: string[];   // 关键词触发器,例如 review、code review。
  instructions: string; // 真正注入给模型的流程规则。
  sourcePath: string;   // skill 文件路径,方便调试和溯源。
};

示例 skill 文件:

代码块MARKDOWN · 13 行收起展开
---
name: code-review
description: Review code changes for bugs and regressions.
triggers:
  - review
  - code review
---

When reviewing code:
1. Inspect git diff first.
2. Focus on bugs, regressions, missing tests.
3. Report findings with file and line references.
4. Do not summarize before findings.

Skill 加载

代码块TS · 23 行收起展开
// loadSkills = 从目录中加载 skill markdown 文件。
// 这里读的是可信 skill 目录,不应该从项目任意文件里加载。
export async function loadSkills(root: string): Promise<Skill[]> {
  // 找到 skills 目录下所有 markdown。
  const files = await glob("skills/**/*.md", { cwd: root });

  return Promise.all(files.map(async file => {
    // 读取 skill 文件正文。
    const text = await readFile(resolve(root, file), "utf8");

    // 拆 frontmatter 和正文。
    // frontmatter 放 name/description/triggers,body 放执行规程。
    const { frontmatter, body } = parseMarkdownWithFrontmatter(text);

    return {
      name: frontmatter.name,               // skill 名。
      description: frontmatter.description, // 适用场景。
      triggers: frontmatter.triggers ?? [], // 没写 triggers 就给空数组。
      instructions: body,                   // 规则正文。
      sourcePath: file                      // 来源路径。
    };
  }));
}

Skill 匹配

代码块TS · 11 行收起展开
// selectSkills = 根据用户输入选择要注入的 skill。
// 这是入门版:只做关键词匹配。
export function selectSkills(skills: Skill[], userInput: string) {
  // 统一小写,做大小写不敏感匹配。
  const lower = userInput.toLowerCase();

  return skills.filter(skill =>
    // 只要任一 trigger 出现在用户输入中,就选择该 skill。
    skill.triggers.some(trigger => lower.includes(trigger.toLowerCase()))
  );
}

更高级可以用 embedding 匹配,但关键词触发足够入门。

Skill 注入上下文

代码块TS · 16 行收起展开
// formatSkills = 把选中的 skill 转成 system prompt 片段。
// 用 XML-like 标签包起来,是为了让模型看清不同 skill 的边界。
function formatSkills(skills: Skill[]) {
  return skills.map(skill => `
<skill name="${skill.name}">
${skill.instructions}
</skill>
`).join("\n");
}

// 把 skill 作为 system message 注入,而不是作为普通 user message。
// 这样它更像行为规则,而不是用户随口说的一段话。
messages.push({
  role: "system",
  content: `Relevant skills:\n${formatSkills(selectedSkills)}`
});

注意:

  • Skill 是规则输入,不是工具。
  • Skill 不应该直接执行危险动作。
  • Skill 过多也会撑爆上下文。

Plugin 系统

Plugin 是程序扩展机制,可以提供:

  • 新工具。
  • 新命令。
  • 新 MCP server 配置。
  • 新 skill。
代码块TS · 15 行收起展开
// Plugin = 一个插件包暴露出来的对象。
// 插件是程序加载的代码,能注册工具、命令、skill,因此风险比 skill 高。
type Plugin = {
  name: string;                         // 插件名,例如 github-tools。
  version: string;                      // 插件版本,用于兼容和排错。
  register(api: PluginApi): Promise<void>; // 插件入口:向 CLI 注册能力。
};

// PluginApi = CLI 暴露给插件的最小注册接口。
// 这里只允许“注册能力”,不直接给插件内部全局对象。
type PluginApi = {
  registerTool(tool: Tool<any, any>): void; // 注册新工具,但执行时仍要过权限系统。
  registerCommand(command: Command): void;  // 注册新 slash command。
  registerSkill(skill: Skill): void;        // 注册新 skill。
};

加载:

代码块TS · 15 行收起展开
// loadPlugin = 加载一个插件入口文件。
// 注意:import 插件代码本身就有风险,所以真实系统要先检查 manifest 和权限。
export async function loadPlugin(path: string, api: PluginApi) {
  // 动态加载插件模块。
  const mod = await import(path);

  // 约定插件 default export 是 Plugin 对象。
  const plugin: Plugin = mod.default;

  // 校验插件结构、版本、名称、能力声明。
  validatePlugin(plugin);

  // 让插件注册工具/命令/skill。
  await plugin.register(api);
}

插件安全

插件是代码,风险很高。

必须考虑:

风险处理
插件注册危险工具工具仍走权限系统
插件读取本地隐私插件来源要可信
插件覆盖内置命令禁止重名或要求显式 override
插件执行初始化副作用加载前提示用户

如果只是给个人学习用,可以先不做插件执行,只做“工具目录扫描”。

Command / Skill / Tool 的关系

Command / Skill / Tool 的关系 User Input Slash? Command Skill Context Tool Action

Command 是确定动作。Skill 是影响模型行为。Tool 是模型可调用能力。

设计原则

原则说明
Command 不调模型能确定就代码执行
Skill 不执行动作只提供流程和规则
Tool 执行动作但必须过权限
Plugin 扩展能力但不能绕过权限
命名要稳定模型依赖工具名

常见坑

后果
/help 交给模型输出不稳定
skill 太长挤占上下文
skill 互相冲突模型行为混乱
plugin 能覆盖内置工具安全风险
command 和 tool 混在一起权限边界不清

读源码时看这些点

  1. Slash command 在哪里路由?
  2. Command 是否绕过模型?
  3. Skill 从哪里加载?
  4. Skill 如何触发?
  5. Skill 是否注入 system prompt?
  6. Plugin 是否能注册工具?
  7. Plugin 工具是否仍走权限?
  8. 是否有命名冲突处理?

命令、技能、插件决定 Agent CLI 能不能从“一个工具”长成“一个平台”。

三层边界

如果只记一个版本:

代码块TEXT · 3 行收起展开
Command = 程序自己的确定性操作
Skill   = 给模型看的任务流程说明
Plugin  = 给程序加载的扩展包

它们的边界非常重要。

是否让模型决定是否执行副作用是否需要权限
Command可能视操作而定
Skill是,影响模型推理不应直接执行不直接需要,但会影响工具调用
Tool是,模型可调用必须
Plugin否,程序加载可能注册副作用能力必须受能力限制

最危险的混乱是:让 Skill 直接像插件一样执行代码,或者让 Plugin 注册的工具绕过权限系统。

Command Router 应该在模型之前

Slash command 是 CLI 的控制面,不是对话内容。

Slash Command 控制流 Raw Input Slash? Parse Known? Handler Help/Error Agent Turn

为什么命令要在模型之前?

  • /help 不需要模型,模型输出还可能不稳定。
  • /cost/status/mcp list 是本地事实查询。
  • /clear/compact 是会话控制,不能让模型猜。
  • /permissions 是安全控制面,必须 deterministic。

命令解析最好支持子命令:

代码块TS · 8 行收起展开
// ParsedCommand = 更完整的命令解析结果。
// 适合 /mcp add github --transport stdio 这种带子命令和 flag 的命令。
type ParsedCommand = {
  name: string;                            // 主命令,例如 /mcp。
  subcommands: string[];                   // 子命令链,例如 ["add", "github"]。
  flags: Record<string, string | boolean>; // --transport stdio 解析成 { transport: "stdio" }。
  args: string[];                          // 普通位置参数。
};

例如:

代码块TEXT · 4 行收起展开
/mcp list
/mcp add github --transport stdio
/permissions allow shell --scope session
/cost --since last-compact

Command 的错误体验

命令错误不要进入模型,也不要只输出 unknown。

代码块TS · 16 行收起展开
// formatCommandError = 命令错误提示。
// 目标:用户输错时给可恢复建议,而不是让模型猜。
function formatCommandError(input: string, registry: CommandRegistry) {
  // 根据编辑距离找最接近的命令,例如 /hlep -> /help。
  const nearest = findNearestCommand(input, registry.names());

  return [
    `Unknown command: ${input}`,
    nearest ? `Did you mean: ${nearest}?` : "",
    `Run /help to list commands.`
  ]
    // 过滤掉空字符串。
    .filter(Boolean)
    // 每条提示单独一行。
    .join("\n");
}

对 CLI 来说,命令错误是产品体验,不是模型推理问题。

Skill 的本质:可复用上下文策略

Skill 不是“插件”,也不是“prompt 小作文”。
它是一段在特定任务下临时注入的操作规程。

一个好的 skill 至少包含:

字段作用
name稳定标识
description触发判断
triggers明确关键词
instructions具体流程
constraints禁止事项
verification完成前检查

示例:

代码块MARKDOWN · 13 行收起展开
---
name: code-review
description: Review code changes for bugs, regressions, and missing tests.
triggers:
  - review
  - code review
---

When reviewing:
1. Inspect git diff first.
2. Prioritize correctness bugs over style.
3. Report findings first with file and line.
4. If no findings, say so and mention residual risk.

好的 skill 是“行为约束”,不是背景资料堆叠。

Skill 触发不能只靠 includes

最简单触发:

代码块TS · 1 行收起展开
input.includes(trigger)

问题:

  • 误触发:用户提到 “不要 review”。
  • 漏触发:用户说 “帮我看下这次改动有没有问题”。
  • 冲突:多个 skill 同时命中。
  • 注入:项目文件里出现触发词,不应该激活 skill。

更稳的做法是分层:

代码块TS · 39 行收起展开
// SkillMatch = 一次 skill 匹配的评分结果。
// 不只返回是否命中,还记录为什么命中,方便 trace 和调试。
type SkillMatch = {
  skill: Skill;      // 被评估的 skill。
  score: number;     // 匹配分数,越高越应该注入。
  reasons: string[]; // 命中原因,例如 explicit trigger、semantic match。
};

// matchSkill = 比 includes 更稳的 skill 选择函数。
function matchSkill(skill: Skill, turn: TurnInput): SkillMatch {
  let score = 0;
  const reasons: string[] = [];

  // 明确关键词命中,给较高分。
  if (matchesExplicitTrigger(skill, turn.userText)) {
    score += 10;
    reasons.push("explicit trigger");
  }

  // 语义相近但没直接命中关键词,给较低分。
  if (matchesDescriptionEmbedding(skill, turn.userText)) {
    score += 4;
    reasons.push("semantic match");
  }

  // 用户显式写 $skillName,说明一定要用这个 skill。
  if (turn.userText.includes(`$${skill.name}`)) {
    score += 100;
    reasons.push("explicit skill mention");
  }

  // 如果用户说“不要 review”,触发词附近有否定词,要降分。
  if (hasNegationAroundTrigger(skill, turn.userText)) {
    score -= 20;
    reasons.push("negated trigger");
  }

  return { skill, score, reasons };
}

显式点名 skill 的优先级最高。
模糊语义匹配可以辅助,但不要让它随便激活一堆长 skill。

Skill 冲突处理

多个 skill 同时命中时,不能无脑全部塞进 system prompt。

冲突例子:

  • brainstorming 要先问问题。
  • test-driven-development 要先写测试。
  • 用户明确说“直接改,不要问”。

需要优先级:

代码块TS · 7 行收起展开
// SkillPolicy = skill 的选择策略。
// 用来处理多个 skill 同时命中时的优先级和互斥关系。
type SkillPolicy = {
  priority: number;        // 优先级,安全/流程类通常更高。
  exclusiveGroup?: string; // 互斥组,同组只选一个,例如多种 planning 流程。
  maxConcurrent?: number;  // 最多允许同时注入几个同类 skill。
};

处理原则:

  1. 用户显式指令优先。
  2. 安全类规则优先。
  3. 过程类 skill 先于领域类 skill。
  4. 同一 exclusiveGroup 只选最高分。
  5. 注入前记录选择原因。

格式:

代码块TEXT · 6 行收起展开
Selected skills:
- code-review: explicit user requested "review"
- verification-before-completion: completion requires verification

Skipped:
- brainstorming: user asked for review, not design work

这让行为可解释,也方便调试。

Skill 注入要有预算

Skill 会吃上下文。大型 skill 可能几千 token。

可以设计:

代码块TS · 7 行收起展开
// SkillInjection = 最终注入模型的 skill 形态。
// 因为上下文有限,有时不能注入全文,只能注入摘要或引用。
type SkillInjection = {
  skillName: string;                      // skill 名。
  mode: "full" | "summary" | "reference"; // 注入全文、摘要,还是只放引用。
  tokenCost: number;                      // 估算 token 成本。
};

注入策略:

情况注入方式
用户显式点名full
高置信触发full 或 summary
低置信相关reference
多个 skill优先 full 一个,其余 summary
上下文紧张只注入关键 checklist

不要把所有 skill 长期放进系统提示。那会让模型行为变慢、变贵、变混乱。

Plugin Manifest

Plugin 必须有清单文件,不能随便 import 任意代码。

代码块JSONC · 18 行收起展开
{
  "name": "github-tools",                  // 插件稳定 ID,命令/工具注册时用它做命名空间。
  "version": "1.2.0",                      // 插件版本,用于兼容性检查和升级回滚。
  "entry": "./dist/index.js",              // 入口文件,PluginManager 会 import 这个模块。
  "capabilities": {
    "tools": ["github.searchIssues", "github.createIssue"], // 暴露给模型的可调用工具。
    "commands": ["/github"],                                // 暴露给用户的斜杠命令。
    "skills": ["github-workflow"]                           // 可被 skill matcher 注入的工作流能力。
  },
  "permissions": {
    "network": ["api.github.com"],          // 网络白名单,避免插件随意访问外部地址。
    "filesystem": "none",                  // 文件系统权限;none 表示不允许读写本地文件。
    "secrets": ["GITHUB_TOKEN"]             // 允许读取的密钥名,运行时仍应脱敏审计。
  },
  "engines": {
    "agentCli": ">=0.8.0"                   // 最低 CLI 版本,避免旧 runtime 加载不兼容插件。
  }
}

Manifest 的作用:

  • 告诉用户插件会扩展什么。
  • 告诉系统插件需要什么权限。
  • 告诉加载器入口在哪里。
  • 告诉版本兼容范围。

没有 manifest 的插件生态很快会变成“运行未知代码”。

Plugin API 要小

给插件的 API 越大,越难保证安全和兼容。

代码块TS · 9 行收起展开
// PluginApi = 暴露给插件的最小 API。
// 设计原则:只给插件注册能力的入口,不把内部 runtime 全交出去。
type PluginApi = {
  registerTool(tool: ToolDefinition): void;          // 注册工具,执行仍走统一 Tool Executor。
  registerCommand(command: CommandDefinition): void; // 注册 slash command。
  registerSkill(skill: SkillDefinition): void;       // 注册 skill。
  getConfig<T>(key: string): T | undefined;          // 读取插件自己的配置。
  log(event: PluginLogEvent): void;                  // 写插件日志,便于排错。
};

不要一开始就暴露:

  • 任意文件系统读写。
  • 任意网络请求。
  • 内部 session 对象。
  • 模型原始 API key。
  • 权限系统的绕过开关。

插件想执行动作,应该注册工具;工具执行时仍然通过 Tool Executor 和 Permission Gate。

插件加载生命周期

插件加载生命周期 Agent CLI Manager manifest trust import register Registry ready

生命周期事件:

代码块TS · 8 行收起展开
// PluginLifecycle = 插件生命周期钩子。
// 钩子越多,能力越强,风险也越高。
type PluginLifecycle = {
  onLoad?(api: PluginApi): Promise<void>;               // 插件加载时执行,最危险。
  onSessionStart?(session: SessionInfo): Promise<void>; // 新会话开始时执行。
  onSessionEnd?(session: SessionInfo): Promise<void>;   // 会话结束时执行。
  onUnload?(): Promise<void>;                           // 插件卸载时清理资源。
};

onLoad 最危险。
因为它在用户还没让模型调用工具之前就能运行代码。
因此加载插件本身也应该是一个需要信任的动作。

Capability Model

插件权限不要只有“启用/禁用”。

代码块TS · 8 行收起展开
// Capability = 插件能力声明。
// 不是简单“插件启用/禁用”,而是细到网络、文件、secret、工具名。
type Capability =
  | { kind: "tool"; names: string[] } // 能注册/调用哪些工具。
  | { kind: "command"; names: string[] } // 能注册哪些命令。
  | { kind: "network"; hosts: string[] } // 能访问哪些域名。
  | { kind: "filesystem"; scope: "none" | "workspace" | "custom"; paths?: string[] } // 文件访问范围。
  | { kind: "secret"; names: string[] }; // 能读取哪些密钥。

权限检查:

代码块TS · 14 行收起展开
// assertCapability = 插件执行敏感动作前的能力检查。
// 插件声明和用户授予的能力必须覆盖这次请求,否则拒绝。
function assertCapability(plugin: PluginIdentity, requested: CapabilityRequest) {
  // 读取这个插件已经被授予的能力。
  const granted = loadGrantedCapabilities(plugin.name);

  // 判断已授予能力是否覆盖本次请求。
  if (!capabilityCovers(granted, requested)) {
    throw new AgentError("PLUGIN_PERMISSION_DENIED", "Plugin lacks capability.", {
      plugin: plugin.name,
      requested
    });
  }
}

这样一个 GitHub 插件可以访问 API.github.com,但不能顺手扫用户磁盘。

Plugin 与 MCP 的区别

这两个很容易混。

维度PluginMCP Server
运行位置Agent CLI 进程内或本地加载独立进程/远端服务
协议CLI 自定义 API标准 MCP 协议
权限CLI 自己管CLI + MCP 配置共同管
语言通常跟 CLI 运行时相关任意语言
隔离较弱,除非 sandbox较强,进程边界
适合扩展 CLI UI/命令/技能暴露工具和资源生态

实用建议:

  • 能做成 MCP 的外部工具,优先做 MCP。
  • 需要深度扩展 CLI 体验,再做 plugin。
  • Skill 不需要代码执行时,不要做 plugin。

命名稳定性是协议

模型会依赖工具名、命令名、skill 名。

坏命名:

代码块TEXT · 3 行收起展开
runThing
doStuff
tool1

好命名:

代码块TEXT · 4 行收起展开
read_file
edit_file
search_text
github_search_issues

命名原则:

  • 动词 + 对象。
  • 不用缩写。
  • 不随版本频繁改。
  • deprecate 时保留兼容层。

工具名改一次,历史会话、skill、文档、模型行为都会受影响。

版本兼容

插件和 skill 都需要版本意识。

代码块TS · 8 行收起展开
// Compatibility = 插件/skill/schema 的兼容性版本。
// 用来防止 CLI 升级后旧插件悄悄失效。
type Compatibility = {
  cliVersion: string;        // CLI 主程序版本。
  pluginApiVersion: string;  // 插件 API 版本。
  toolSchemaVersion: string; // 工具 schema 版本。
  skillSchemaVersion: string;// skill frontmatter/body 规范版本。
};

破坏性变化要这样处理:

  1. 新增新名字或新字段。
  2. 老字段标记 deprecated。
  3. 运行时输出警告。
  4. 几个版本后再移除。

不要悄悄改 schema。模型看不懂“悄悄”。

安全风险清单

风险场景防线
Plugin 初始化窃取文件onLoad 直接读磁盘manifest + sandbox + trusted source
Plugin 注册危险工具delete_workspace工具权限仍走统一 gate
Skill prompt injectionskill 被项目文件伪造只从可信目录加载
Skill 冲突多个流程互相打架priority / exclusive group
Command 重名覆盖插件覆盖 /help禁止覆盖内置命令
Tool schema 欺骗描述说只读,实际写runtime permission 不信描述
Secret 泄漏插件拿到 API keysecret scope + redaction
Supply chainnpm 包被投毒lockfile / signature / hash

一句话:描述不可信,运行时边界才可信。

可观测性

平台化之后,必须知道每个能力来自哪里。

代码块TS · 10 行收起展开
// CapabilityProvenance = 能力来源记录。
// 当工具失败或行为异常时,要能看出它来自内置、插件、MCP 还是用户配置。
type CapabilityProvenance = {
  name: string;                                  // 能力名,例如 github.createIssue。
  kind: "command" | "tool" | "skill";            // 能力类型。
  source: "builtin" | "plugin" | "mcp" | "user"; // 来源。
  packageName?: string;                          // 如果来自插件/MCP,记录包名。
  version?: string;                              // 来源版本。
  path?: string;                                 // 本地来源路径。
};

当工具失败时,错误要带 provenance:

代码块TEXT · 4 行收起展开
Tool github.createIssue failed
source: plugin github-tools@1.2.0
permission: network api.github.com allowed
error: 401 Bad credentials

这样用户知道是模型错、工具错、插件错,还是配置错。

插件供应链与隔离策略

插件系统一旦允许第三方扩展,就不再只是“加载一段配置”。它变成了供应链问题:谁发布、谁安装、运行时能碰什么、出事后能不能定位和禁用。

插件治理至少要分三层:

层级解决什么最小机制
安装前插件是否可信来源、签名、hash、权限声明
加载时插件能注册什么能力manifest 校验、capability allowlist、版本兼容
运行时插件能力是否越界sandbox、统一 ToolExecutor、audit、禁用开关

不要让插件在 onLoad() 里直接拿到无限权限。推荐把插件能力声明写成可审计对象:

代码块TS · 24 行收起展开
// PluginSecurityProfile = 插件安全画像。
// 安装、加载、运行三阶段都围绕这个对象做判断。
type PluginSecurityProfile = {
  pluginId: string;                    // 插件稳定 ID,不随展示名变化。
  version: string;                     // 插件版本,用于回滚和兼容判断。
  source: {
    registry: string;                  // 来源仓库或 marketplace。
    packageHash: string;               // 安装包 hash,防止同版本内容漂移。
    signature?: string;                // 可选签名,成熟平台应要求签名。
  };
  declaredCapabilities: Array<
    | "read_workspace"                 // 读取工作区文件。
    | "write_workspace"                // 修改工作区文件。
    | "network"                        // 访问网络。
    | "spawn_process"                  // 启动子进程。
    | "register_tool"                  // 注册工具。
    | "register_skill"                 // 注册 skill。
  >;
  runtimeLimits: {
    maxStartupMs: number;              // 加载超时,防止插件卡住 CLI。
    maxMemoryMb: number;               // 内存上限。
    allowedHosts: string[];            // network capability 的 host 白名单。
  };
};

加载生命周期应该是“先验证,再注册”,而不是边执行边相信:

步骤允许做什么禁止做什么
read manifest读取静态元数据执行插件代码
verify package校验 hash/signature/version自动拉取任意依赖
resolve capabilities计算权限集合让插件自己解释权限
create sandbox创建受限上下文暴露宿主全局对象
register contributions注册 command/skill/tool 描述直接执行副作用
activate on demand用户触发后激活启动时批量跑插件逻辑

插件故障要能快速隔离:

故障平台动作用户可见信息
加载超时禁用本次加载,记录 pluginId/version哪个插件启动慢
manifest 不兼容跳过插件,提示所需 CLI 版本需要升级 CLI 还是插件
工具执行失败走 ToolExecutor 错误归一化能力来源和错误码
权限越界拒绝并标记安全事件插件请求了什么越界能力
连续失败自动熔断插件禁用原因和恢复方式

最小隔离策略可以先不做完整容器,但至少要做到:

  1. 插件不能绕过 CommandRouter / ToolExecutor / SkillLoader 的统一入口。
  2. 插件注册的是能力描述,不是直接拿宿主对象随便调用。
  3. 每个能力都带 provenance:builtin、plugin、mcp、user config。
  4. 插件失败不应该拖垮主 CLI;最多禁用该插件或该能力。
  5. 插件升级要保留旧版本 hash 和失败记录,方便回滚。

真正成熟的插件系统不是“能装插件”,而是“插件出了事也能停、能查、能退、能限制影响半径”。

最小平台实现顺序

不要一上来做完整插件市场。建议顺序:

  1. 内置 Command Router。
  2. 内置 Tool Registry + 权限系统。
  3. 本地 Skill Loader,只读 Markdown。
  4. MCP Server 配置加载。
  5. Plugin Manifest 解析,但先只允许注册 skill。
  6. Plugin 注册 command/tool。
  7. Capability permission。
  8. 插件签名、版本、市场、隔离。

每一步都能独立产生价值。

Java 后端视角类比

Agent CLI 概念Java 后端类比
Command RouterController 中的管理接口
Skill Loader规则配置 / 策略配置
Tool RegistryService Bean Registry
Plugin ManagerSpring Boot Starter / SPI
CapabilityRBAC 权限点
Manifestpom.xml + 配置元数据
MCP Server外部微服务
Tool schemaOpenAPI / DTO
ProvenanceBean 来源 / Actuator 信息

如果你理解 Spring 的 Bean、Starter、AutoConfiguration,就能理解 Agent CLI 的插件化:
核心问题永远是“谁能注册能力、能力从哪里来、运行时是否受统一治理”。

读源码时的高阶问题

看到 command/skill/plugin 模块时,继续问:

  1. Slash command 是否在模型前处理?
  2. 命令解析是否支持 flags/subcommands?
  3. 内置命令能否被覆盖?
  4. Skill 是否只从可信目录加载?
  5. Skill 触发是否可解释?
  6. 多 skill 冲突如何处理?
  7. Skill 注入是否有 token 预算?
  8. Plugin 是否有 manifest?
  9. Plugin 是否能绕过 Tool Executor?
  10. Plugin 权限是启用/禁用,还是 capability 级别?
  11. MCP、Plugin、Tool 的边界是否清晰?
  12. 失败日志里是否能看出能力来源?

命令、技能、插件这三层如果设计清楚,Agent CLI 才能从一个“聊天壳子”演化成可扩展工程平台。