跳至正文
来两杯美式
返回

让用户自定义 Agent 能力,而不打开注入的后门

By 来两杯美式
发布于

前三篇讲了内置能力怎么做成「服务端单一事实源」:薄壳路由、目录下发、按会话拉协议、四套版本号。内置能力的协议由平台工程师写,天然可信。

但内置能力永远覆盖不了长尾需求。「每天下班前提醒我整理日报」「每周五把本周周报链接发到某个文档」——这类一人一份的个人流程,平台不可能逐个开发。于是有了第四篇的主题:把「定义能力」开放给用户

这是整个系列里安全密度最高的部分,因为它直面一个不舒服的事实:

让用户自定义能力而不打开注入后门总结:自由文本协议的三个风险、结构化 workflow 加服务端渲染、四道结构化防线、user: 专属能力与托管外部工作流两条路线对比及系列四条原则

一旦允许用户提交自由文本的「协议」,你就给 prompt injection 开了一条制度化通道。

为什么自由文本协议是后门

设想最直接的实现:给用户一个编辑框,让他写自己的 SKILL.md,存进数据库,Agent 拉取时原样下发。用户瞬间获得了对 Agent 会话的完整指令权。

「用户本来就能跟 Agent 对话,多一个协议有什么区别?」区别很大:

  1. 协议的指令权重远高于对话。前三篇建立的整套机制都在向 Agent 强调「协议是最高指令,严格照做,不凭记忆」。用户聊天里说「忽略所有规则」会被忽略;协议里写同样的话,会被忠实执行。
  2. 执行时机不受用户当场监督。协议可能在未来的任何会话被加载——包括用户已经忘了自己写过什么的会话、用户被钓鱼后不知情写下的内容。
  3. 危害会跨能力扩散。协议里一句「先调用删除工具清空素材」,伤害的是别的能力的工具。

所以威胁模型不是「防用户」,而是「防一切会流进协议文本的不可信内容」——用户本人、被钓鱼的用户、抄来模板的用户。原则只有一条:

不可信输入进 prompt 之前,必须先变成可验证的结构。

只存结构化 workflow,协议由服务端渲染

落到数据模型上,用户自定义能力不存 Markdown,存这个:

// 简化的结构
UserCapability = {
  ownerId,            // 归属用户
  name,               // snake_case 唯一标识
  displayName,
  whenToUse: string[],      // 目录里的触发词
  status: "draft" | "active",
  version: number,          // 乐观锁
  workflow: {
    objective: string,            // 目标(纯描述文本)
    steps: [                      // 1-20 步
      | { kind: "platformCapability",
          capabilityId: string,   // 只能引用内置能力 ID
          instruction: string }   // 该步骤要做什么
      | { kind: "localTool",
          toolName: string,       // 只能引用已声明的工具绑定
          hostProfile: string,
          instruction: string }
    ],
    completionCriteria: string,
  },
  platformCapabilityIds: string[], // 声明的平台能力白名单
}

关键在「渲染」这一步:Agent 拉取 user: 能力的协议时,服务端根据 workflow 结构实时生成 Markdown——目标、执行边界、编号步骤、完成条件,全部由模板产出。用户的自由文本只能出现在 objective / instruction / completionCriteria 这三个被明确标注为「步骤说明」的字段里,永远不会被解释为指令、工具调用或权限声明。

数据库模型上我们留了一句注释,算是这个设计的宪法条款:

只存结构化 workflow;运行时协议由服务端根据 workflow 和已声明绑定生成,因此用户不能通过自由文本扩大可调用能力范围。

四道防线

结构化是骨架,校验是肌肉。实际校验是层层递进的四道:

第一道:文本字段的正则拦截。 objective 等文本字段会过一遍正则,拦截「调用/使用/执行某某工具」「忽略以上」这类明显越界的措辞。代码注释里我们对它的定位写得很诚实:best-effort 提示性拦截,不是安全边界。正则挡得住手滑,挡不住蓄意——真正的防线在后面三道。把不完美的防线放在第一道而把话说死,是为了防止自己将来误以为它可靠。

第二道:引用完整性校验。 workflow 里的每个步骤只能引用 platformCapabilityIds 白名单里的内置能力、和已声明的工具绑定。想引用一个没声明的东西?校验直接失败。这一道是关键:用户能表达「对什么做什么」,不能表达「用什么做」以外的东西

第三道:inputSchema 只放行 draft-07 子集。 工具绑定的输入 schema 由用户自己声明(描述他的本地工具长什么样),但只允许基础类型和组合——$refanyOfallOfoneOfnot 全部禁止。原因:这些构造在后续做 schema 校验、schema 比对时都有边角案例,收窄到子集换来的是「校验器永远是对的」。

第四道:运行时下发收窄。 编辑态声明的白名单可以比运行态宽(方便用户增删步骤),但下发时只带 workflow 实际引用的能力集合。「声明的」和「能用的」是两个集合,后者永远是前者的子集——即使用户声明了一堆能力,没在步骤里用到的不会到达 Agent 手里。

四道防线的共同点:每一道都基于结构,没有一道基于内容理解。不依赖 LLM 判断「这段话是否恶意」——凡是需要模型判断的安全边界,都不是边界。

工具绑定:平台不碰你的工具

localTool 类型的步骤引用的是「宿主机 MCP 绑定」——用户自己在 Agent 宿主上配置的本地工具(比如操作本地日历、本地文件的 MCP 工具)。平台侧只存描述信息:

UserCapabilityToolBinding = {
  name, // 逻辑名,workflow 里引用它
  hostProfile, // 宿主环境标识,如 "claude-code"
  mcpServerId, // 宿主机上的 MCP server id
  mcpToolName, // 工具名
  inputSchema: JSON, // draft-07 子集
  requiresUserConfirmation: true, // 默认必须确认
};

宪法条款之二同样写在模型注释里:

平台只保存工具标识与输入 schema,绝不注册、代理或执行该工具。

也就是说数据面完全不走平台:Agent 拿到绑定信息后,从自己宿主机上已连接的 MCP 工具目录里核对 server id、工具名和 schema,核对通过后直接本地调用。平台连转发的机会都没有。

两个细节值得展开:

绑定缺失时显式失败。 用户换了一台机器、没配置某个工具,服务端渲染出的协议会包含一段明确的失败文本:「配置缺失,停止执行」。不是静默跳过该步骤,不是降级,是停。半途而废的自动化比报错危险得多——它产生「已经做完了」的错觉。

requiresUserConfirmation 默认 true,且确认是「本次」的。 把平台读取的任何内容传给本地工具前,必须取得用户本次会话的明确确认,并说明将发送哪些数据。不能「上次同意过」、不能「创建能力时同意过」——因为用户创建时确认的是 workflow 结构,不是运行时数据的内容。这个默认值的方向很重要:安全默认值应该是「麻烦的」,让用户主动选择省事,而不是反过来。

创建流程也要防社工

自定义能力的创建入口是几个 MCP 对话工具(start_creategenerate_draftconfirm_draft),用户跟 Agent 聊着天就把能力建了。这里有两个容易忽略的防线:

LLM 草稿生成器的「绝不编造」约束。 聊天式创建里,让 LLM 从用户描述生成结构化草稿。系统提示词里写死了:信息不足就返回空缺,绝不编造工具绑定和能力引用——宁可多问一轮,不能替用户「猜」一个工具名。生成的草稿依然要过前面四道校验,LLM 在这个流程里只有建议权,没有决定权。

写操作全部走审计。 创建、激活、停用都是 mutation,自动进审计日志。加上工程侧的约束:每人 50 个能力、每能力 20 个绑定的配额(配额检查用行锁防并发超发)、激活态的能力不能直接改(必须回 draft 再改,配合乐观锁)——这些不是刁难用户,是限制任何单一账号出问题时的影响半径。

另一条路线:托管外部工作流

「用户自定义能力」还有一条完全不同的路线:用户在外部工作流平台(比如 Dify)上搭好流程,把它的 MCP endpoint 登记进来,由平台代理调用。我们也实现了这条路线,两者并存。

托管路线的核心动作:

  1. 入口校验:强制 HTTPS、无凭据、域名白名单,外加 SSRF 防护(拒绝 localhost 和私网地址段);
  2. 快照工具列表:连接该 endpoint、listTools(),把工具清单和参数 schema 落库存快照;
  3. 调用前校验:每次工具调用先确认目标在快照内、参数按快照 schema 校验通过,再转发,带超时保护。

对比一下两条路线,会发现它们回答的是同一个问题——不可信的用户扩展如何进入信任边界——但答案的方向相反:

user: 专属能力托管外部工作流
用户定义什么结构化 workflow 步骤外部平台上的完整流程
平台的角色只存声明,渲染协议,不执行代理发现、校验、转发
数据面在哪Agent 宿主本地直连工具全部经过平台
主要攻击面文本字段注入、越权引用恶意 endpoint(SSRF、快照后替换)、参数注入
核心防线结构化 + 引用校验入口白名单 + 快照 + 调用前校验

选型逻辑其实很简单:流程要用平台没有的本地资源(本机文件、本地日历),走 user: 路线,因为只有宿主 Agent 够得到本地;流程要复用平台的数据和系统能力、且用户想要可视化编排,走托管路线,因为平台代理才能提供统一的审计和重试。两条路线的用户心智也不同——user: 是「教会助手一个习惯」,托管是「接入一个现成服务」。

系列总结

四篇写完,把整个设计的三条可迁移原则做个收束:

  1. 凡是会被 LLM 当作指令读的东西,都要有服务端单一事实源。快照会在用户机器、Agent 记忆、调用日志三个地方腐烂,其中记忆最难防——所以协议要「按会话下发、用完即弃」,并显式禁止进记忆。
  2. 接口设计决定薄壳能不能保持薄。目录条目把 protocolTool + protocolInput 一起下发,控制流也是服务端的;目录是权限的投影而不是维护出来的清单,权限变化自动传导。
  3. 不可信输入进 prompt 前,先变成可验证的结构。四道防线全部基于结构而非内容理解,LLM 只有建议权没有决定权——需要模型来判断的安全边界,都不是边界。

再加上版本管理那条:版本号按「谁需要因这个变化而行动」来拆——大多数变更应该惊动 nobody。

Agent Skill 这个领域还很新,「Skill 该长什么样」远没有共识。我们这套「薄壳 + 服务端目录 + 动态协议」跑了小半年,最让我确信走对了的信号是:流程改了几十次,没有一个用户需要为此重装过任何东西。如果你的 Skill 也在越写越厚,不妨数一数它的行数——超过一百行的时候,里面大概已经有一半内容属于服务端了。


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
查看系列全部文章
  1. 01.MCP Server 选型决策树:从语言到审计,六个维度一次说清
  2. 02.当 MCP Server 只是"翻译"现成能力:FastMCP + Mem0 + Redis 的选型逻辑
  3. 03.当 MCP Server 要"嵌进"对外平台:Node.js + 官方 SDK + API Key 的选型逻辑
  4. 04.SSE 已被 MCP 判为 legacy:存量服务怎么迁、新服务怎么选
  5. 05.MCP 权限怎么分级?只读、可操作和高风险动作(附完整源码)
  6. 06.MCP 工具权限码怎么设计?read、write、dangerous 三层模型(附完整源码)
  7. 07.自建 SearXNG + MCP:给 AI 工具接入可控的网页搜索
  8. 08.Agent Skill 是快照,快照会腐烂:我把业务流程全部搬到了服务端
  9. 09.能力目录与协议下发:把接口设计和控制流一起交给 Agent
  10. 10.四套版本号:把 Agent Skill 的版本管理挪回服务端之后
  11. 11.让用户自定义 Agent 能力,而不打开注入的后门
  12. 12.MCP 2026-07-28 改了什么:从有状态会话到无状态核心
  13. 13.Anthropic 发布 Agent Skills:AI Agent 的能力该如何被打包
  14. 14.Agent Skills 设计模式:用文件夹给 Agent 装上专业能力
  15. 15.Agent Skills 最佳实践:从评测、结构拆分到安全审查
  16. 16.MCP Server 升级 2026-07-28:不是换依赖,而是重画协议边界
  17. 17.MCP Client 升级 2026-07-28:服务端能留兼容窗口,我们偏不兼容旧协议
  18. 18.MCP 爆炸之后,Agent 怎么从一堆工具里选对那一个

上一篇
精选数据与垂直 AI:AI 时代的护城河在哪
下一篇
四套版本号:把 Agent Skill 的版本管理挪回服务端之后