📌 上一篇我聊了 Agent 身份认证这件事,发完之后有读者跟我说:“原来 Agent 还能有身份证,涨知识了。”

我回去又琢磨了一下,发现一个问题:就算给 Agent 发了身份证,事情也没完。

之前聊企业不敢让 AI Agent 独自干活,第一道坎是 Agent 身份问题。如果系统不知道「谁在操作」,后面的授权、审计和追责都没有明确主体。

我当时觉得,先把身份搞定了,其他问题慢慢来。

但仔细一想,发现有个更隐蔽的口子还没堵上。

因为 Agent 真正执行任务时,不是只靠模型自己回答,而是会调用各种工具和能力:

查合同、读发票、调 CRM、发通知、创建审批、调用内部 API……

这些能力,在很多 Agent 系统里被称为 Skill

然后我就意识到:

💡 就算 Agent 本身有了身份证,它调用的 Skill 可能还在裸奔。


一、Agent 调工具的真实风险

先看三个场景。

场景 1:工具包被替换

某个合同查询 Skill 经过审核后正常上线,Agent 被配置为允许调用它。后来有人把 Skill 包里的某个文件替换了,加入了额外的接口调用。Agent 仍然在调用同一个 Skill,但行为已经变了。

白名单没有拒绝它,因为 Skill 名称没变、版本号没变、白名单配置也没变。

场景 2:依赖污染

Skill 引入了一个第三方依赖包,该依赖的新版本被注入了恶意代码。Skill 本身的代码没问题,但它依赖的东西有问题。

场景 3:权限声明漂移

Skill 最初声明只读合同数据,后续版本加入了写入客户信息的能力,但权限配置里的 allowlist 仍然显示它"只读"。系统没有拦截,因为 allowlist 是按 Skill 名称配的,不是按实际内容。

这些问题的共同点是:Agent 的身份没变,但它手里的工具变了。

如果企业只管「哪个 Agent 可以调用哪个 Skill」,但不管「这个 Skill 还是不是当初审核过的那一份」,风险就会从 Agent 身份层转移到工具层。


二、Skill 和普通 Tool / 插件有什么区别

很多人会问:这跟 MCP 工具的 allowlist 有什么区别?跟浏览器插件有什么区别?

区别主要在分发方式和信任模型

浏览器插件 常见 MCP Tool Skill
分发方式 插件商店审核上架 开发者本地注册或服务端配置 Skill 包上传到平台
信任来源 商店审核 + 浏览器机制 本地环境、服务配置和宿主平台 平台扫描 + 签名 + 上架记录
篡改检测 浏览器/商店机制 取决于具体实现 文件摘要 + 签名验签
运行时校验 浏览器强制执行 取决于宿主平台 安装、加载或关键调用前可验签

我这里说的 Skill,不只是一个函数描述或一个接口地址,而是一个可分发的能力包——里面有 SKILL.md、脚本、依赖、参考资料和配置文件。Agent 调用它时,拿到的不只是"一个工具名",而是一整套可以执行业务动作的能力。

所以 Skill 的核心问题不是"能不能被调用",而是:

它作为一个能力包,被上传、扫描、签名、上架、使用之后,内容还能不能被验证?


三、一个可信 Skill 应该经历什么流程

我一开始想的流程特别简单:上传、扫描、签名、上架。

后来研究了几版实现,才意识到这不是一个"上架流程"的问题,而是一个信任传递的问题。

┌──────────────────────────────────────────┐
│  1. 上传                                  │
│  产出:原始 Skill 包文件(zip)           │
└──────────────────┬───────────────────────┘
                   ▼
┌──────────────────────────────────────────┐
│  2. 扫描                                  │
│  产出:扫描报告                            │
│  说明:用户可通过 scanId 查看扫描详情       │
└──────────────────┬───────────────────────┘
                   ▼
┌──────────────────────────────────────────┐
│  3. 签名                                  │
│  产出:文件清单(MF)+ 数字签名(P7S)     │
│  说明:签名结果封装在 zip 包的 META-INF    │
│  目录下                                   │
└──────────────────┬───────────────────────┘
                   ▼
┌──────────────────────────────────────────┐
│  4. 上架                                  │
│  产出:上架记录                            │
│  说明:记录签名时间、状态等                │
└──────────────────┬───────────────────────┘
                   ▼
┌──────────────────────────────────────────┐
│  5. 使用                                  │
│  产出:Agent 调用结果                      │
│  说明:安装、加载或关键调用前可执行验签     │
└──────────────────────────────────────────┘

拆开来看,每一步都有自己的"信任锚":

  • 上传 锚定的是原始文件
  • 扫描 锚定的是安全基线,回答"这份 Skill 的风险水位如何"
  • 签名 锚定的是内容完整性,回答"文件有没有被改过"
  • 上架 锚定的是发布状态,相当于"上岗许可证"
  • 使用 锚定的是运行时状态,确认当前调用的还是当初通过审核的那份文件

这里面最容易被忽视的,是签名和验签。

很多系统做到 allowlist 就停了,但 allowlist 只能说明"这个名字被允许",不能说明"这个包没被改过"。


四、签名到底在签什么

以一个已经签名的 VeriAgent Skill 包为例,最终 zip 里面会多出两个文件:

skill-package.zip
├── SKILL.md                    ← Skill 定义文件
├── scripts/                    ← 业务代码
├── references/                 ← 参考资料
└── META-INF/
    ├── SKILL_VERIFY.MF         ← 文件摘要 + 扫描记录 ID
    └── SKILL_VERIFY.P7S        ← 对 MF 的数字签名

SKILL_VERIFY.MF 可以理解成这份 Skill 包的"文件清单"。它会记录每个业务文件的摘要,以及这次扫描对应的 Scan-Id

一个简化后的内容大概长这样:

Manifest-Version: 1.0
Created-By: VeriAgent Signing Engine 1.0
Scan-Id: scan-task-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Sign-Algorithm: SHA-256

Name: scripts/handler.js
SHA-256-Digest: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=

Name: SKILL.md
SHA-256-Digest: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=

这里有两个关键点:

  1. 每个文件都有自己的 SHA-256 摘要。文件内容只要变一个字符,摘要就会变。
  2. MF 本身会再被生成一份数字签名,也就是 SKILL_VERIFY.P7S。这样别人不能悄悄改 MF 里的摘要。

所以签名真正解决的不是"这个 Skill 永远安全",而是:

这份 Skill 包当前的内容,和当初签名时记录的内容是否一致。


五、怎么验签:总不能每次都手动解压吧

如果每次都让安全同学手动解压、看 MF、算哈希、比对文件,肯定不现实。

所以更合理的方式,是把验签也做成一个 Skill。

比如现在 VeriAgent 里就有一个专门的验签 Skill,它做的事情可以概括成两步:

  1. 先验证 MF:检查 META-INF/SKILL_VERIFY.MFMETA-INF/SKILL_VERIFY.P7S 是否存在,并验证 P7S 能否证明 MF 没被改过。
  2. 再验证文件:MF 验证通过后,逐个计算 zip 内业务文件的 SHA-256 摘要,和 MF 里的记录做对比。

最终它不会只返回一句"通过"或"失败",而是返回结构化结果:

verified: true
manifestSignatureVerified: true
manifestSignatureMessage: P7S 已验证 MF 未被篡改
totalFiles: 7
matchedFiles: 7
mismatchedFiles: []
missingFiles: []
extraFiles: []
metadata.scanId: scan-task-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
metadata.hashType: SHA-256

这样一来,系统至少能分清几类问题:

结果 含义
manifestSignatureVerified: false MF 本身没有通过 P7S 验证,文件清单可能被改过
manifestSignatureVerified: trueverified: false MF 没问题,但业务文件和清单不一致
verified: true 当前包符合这套验签规则,内容和签名时一致

这就把"我相信这个 Skill 没问题",变成了"我可以验证它是否还是那份 Skill"。

当然,验签也不一定要在每一次普通调用前都完整执行一遍。更实际的做法是:在上传、签名、上架、安装、加载或关键调用前强制验签;运行中再结合缓存、包摘要和文件监控来降低成本。

(VeriAgent 目前已经把验签做成了一个可用的 Skill,感兴趣的同学可以到 官网 看看完整流程。)


六、安全扫描和签名不是一回事

这里有一个容易混淆的点:

安全扫描和 Skill 签名解决的是不同问题。

安全扫描 Skill 签名
回答的问题 这个 Skill 包有没有明显风险? 这个 Skill 包是不是经过认证的版本?
检查什么 高危依赖、异常文件、不合规内容 文件内容完整性、签名有效性
产物 扫描报告、风险等级、scanId MF 文件、P7S 签名、验签结果
性质 风险识别 完整性验证

扫描像体检,签名像封条。

体检能发现当时有没有明显问题;封条能证明后来有没有被人拆过。两者都重要,但谁也不能替代谁。

所以,签名不等于证明 Skill 永远安全

更准确地说:

  • 扫描 负责识别风险:这个包有没有依赖漏洞、异常文件、不合规内容
  • 签名和验签 负责保证内容完整性可验证:包有没有被改过,还是不是当初签名的那个版本
  • 平台准入 负责做决策:扫描是否通过、签名是否有效、当前状态是否允许上架和调用

七、这套机制解决什么,不解决什么

为了避免把"可信"讲得太玄,我觉得需要把边界说清楚。

它能解决的是:

  • Skill 来源和版本是否可管理
  • Skill 包内容是否被替换
  • 签名后的文件是否被改动
  • 当前调用的 Skill 是否仍然匹配签名时的文件清单
  • 扫描记录是否可以通过 scanId 追溯

它暂时不能单独解决的是:

  • Skill 运行时是否一定不会做坏事
  • 第三方依赖未来是否会出现新漏洞
  • Agent 是否会在合法工具里做出错误决策

所以它不是终点,而是 Agent 进入生产系统前的一层基础设施。

先让工具包能被扫描、签名、验签,后面才谈得上更细的动态权限、运行时熔断和行为审计。


八、回到 Agent 落地:身份、工具、审计三层

当企业评估一个 Agent 是否可以进入生产系统时,真正要看的是三层:

问题 能力
身份层 这个 Agent 是谁? Agent 数字身份
工具层 它调用的 Skill 是否可信? 安全扫描 + 文件签名 + 验签 Skill
审计层 它做了什么? 行为日志 + 调用记录

身份层解决「谁在操作」。

工具层解决「它调用的能力是否可信」。

审计层解决「事后能不能追溯」。

如果只有 Agent 身份,没有可信 Skill 准入,那么系统只能知道"谁在调用",但不知道"它手里的工具是否被换过"。

如果只有工具准入,没有后续审计,那么系统能证明工具没被改,却仍然很难解释"它到底做了什么"。

所以这是一个顺序问题:

先让 Agent 成为可识别的主体,再让它调用的 Skill 成为可验证的能力,最后才能把关键调用沉淀成可追溯的证据

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐