skill-vet — 智能体技能审查工具 是做什么的?
在安装智能体技能包之前,使用此命令进行审查。当任务涉及来自此机器外部的技能包时——市场下载、他人链接的仓库、客户端发送的包——都应先运行此命令,再将包安装到智能体将加载它的任何位置。
单一命令
skill-vet vet <技能包文件夹> [--json] [--strict]
(通过 pipx install skill-vet 或 pip install skill-vet 安装。从源码检出运行,同样的命令是 python3 src/skill_vet.py vet <技能包文件夹> …。)
- 参数是包的根文件夹(包含 SKILL.md 的文件夹)。每次运行一个包。
- 退出码:0 判定 CLEAN 或 REVIEW,1 判定 BLOCKED(使用
--strict时 REVIEW 也返回 1),2 输入错误(文件夹不存在、不是文件夹、参数错误)。在--json模式下,每个错误(包括参数错误)都是一个带有error键的 JSON 对象,永远不会输出 traceback。 - 只读且不执行任何代码。 Vet 将包作为静态文本读取:它从不导入、运行、shell 调用或求值包中的任何内容。其自身源码不导入任何网络或子进程模块。可以安全地指向一个你完全不信任的包。
危险模型(产品核心)
包被认为危险仅当它做了以下三件事之一——所有者的固定规则(2026-10-30),严格应用:
- 窃取(STEALS)——读取凭据存储、浏览器配置文件、SSH 密钥或环境机密,并将结果发送或复制出去。
- 监视(SPIES)——捕获屏幕、键盘、麦克风、摄像头或浏览器历史记录,并持久化或发送捕获内容,且文档未披露该捕获。
- 隐蔽发送(SENDS IN SECRET)——网络调用的负载或目的地被隐藏:到未公开端点的编码负载、发送到未公开端点的本地文件内容、结合包外写入的发送,或者来自声称离线工作的包的任意发送。
单一模式命中本身不构成危险判定。 每个测试需要在同一个文件中同时出现证据的组合,且发现结果会引用两个证据的文件和行号。任何不足组合的情况都会给出包含证据的警告。
| 同一文件中的证据 | 判定结果 |
|---|---|
| 机密读取 + 发送,或机密读取 + 复制/移动到外部路径 | DANGER_STEALING(BLOCKED) |
| 捕获 +(发送或持久化),捕获类型未在 SKILL.md / README.md 中披露 | DANGER_SPYING(BLOCKED) |
| 发送 + 编码负载,端点未披露或非字面量 | DANGER_HIDDEN_SENDING(BLOCKED) |
| 发送 + 读取包外文件,端点未披露 | DANGER_HIDDEN_SENDING(BLOCKED) |
| 发送 + 写入包外,端点未披露 | DANGER_HIDDEN_SENDING(BLOCKED) |
| 发送 + 文档声称离线/仅本地运行 | DANGER_HIDDEN_SENDING(BLOCKED) |
| 机密读取,同一文件中无出口操作 | SECRET_ACCESS(WARN) |
| 向文档从未提及的端点发送,无隐藏组合 | UNDISCLOSED_NETWORK(WARN) |
| 发送时没有字面量 URL 可检查 | DYNAMIC_ENDPOINT(WARN) |
| 编码负载到达已披露端点 | ENCODED_PAYLOAD(WARN) |
| 捕获 + 持久化/发送,捕获类型已在文档中披露 | DISCLOSED_CAPTURE(WARN) |
| 捕获,无持久化或发送 | CAPTURE_CAPABILITY(WARN) |
公开的判断原则:
- 窃取不需要隐蔽:将凭证发送到文档中描述的端点仍然是窃取——文档承认它并不会使其安全。披露仅区分监视和隐蔽发送与其诚实对应物。
- 在同一个文件中已被判定为窃取出口操作一半的发送,不会再次被计为隐蔽发送。
- 捕获“披露”按类型对照 SKILL.md / README.md 中的普通词语检查(截图、按键、麦克风、摄像头、浏览器历史等)。捕获并说明的包是可明确接受的能力,而不是间谍。
- 出口成分按文件收集,包括所有代码行和注释(保守原则:能力证据不会因为靠近注释而被忽略)。卫生检查(shell=True、eval、下载执行)跳过完整行注释——注释中提及的模式是文档,不是代码。
规则表
| 规则 | 严重级别 | 依据 |
|---|---|---|
MISSING_SKILL_MD | ERROR | 包根目录缺少 SKILL.md(不同大小写的名称会产生 SKILL_MD_CASE,WARN,并使用该文件)。 |
FRONTMATTER_MISSING | ERROR | SKILL.md 没有 --- 块。 |
FRONTMATTER_PARSE | ERROR | 存在超出支持子集的前置信息行(见下文);引用该行。 |
REQUIRED_FIELD | ERROR | name 或 description 缺失或为空。 |
NAME_DIR_MISMATCH | WARN | 规范化后的前置信息名称 ≠ 文件夹名称。 |
NAME_FORMAT | WARN | 名称不是小写 kebab-case 格式。 |
BROAD_TOOL_GRANT | WARN | allowed-tools 条目为 *、Tool(*) 通配符或裸 Bash。像 Bash(npm run *) 这样的限定形式不会触发。 |
BROKEN_REFERENCE | ERROR | SKILL.md 中引用的本地文件(markdown 链接或反引号路径)或脚本中的路径字面量不存在。解析时先尝试引用文件所在文件夹,再尝试包根目录。 |
REFERENCE_OUTSIDE_PACK | WARN | 引用解析到包文件夹之外。 |
LARGE_FILE | WARN | 文件超过 1 MiB(1,048,576 字节)。 |
BUNDLED_BINARY | WARN | 二进制文件(已知二进制扩展名,或前 8 KiB 包含 NUL 字节)。无法静态审查二进制文件。 |
SYMLINK_OUT | INFO | 符号链接的目标离开包;不扫描该目标。 |
DANGER_STEALING | ERROR, danger | 上述组合表。 |
DANGER_SPYING | ERROR, danger | 上述组合表。捕获类型:屏幕、键盘、音频、摄像头、历史记录(同一文件中历史存储读取加数据库读取)。 |
DANGER_HIDDEN_SENDING | ERROR, danger | 上述组合表。 |
SECRET_ACCESS | WARN | 凭据存储/环境机密读取,同一文件中无出口操作。机密来源:.ssh / 密钥文件、.aws、.gnupg、.netrc、存储的凭据文件、浏览器存储和配置文件、.env 文件、环境访问(命名包含 TOKEN / SECRET / PASSWORD / API_KEY / …)、整体环境读取。 |
UNDISCLOSED_NETWORK | WARN | 网络调用的端点主机未出现在 SKILL.md 或 README.md 中。当主机(或其父域名)作为 URL 出现在文档中时,视为已披露。 |
DYNAMIC_ENDPOINT | WARN | 文件中没有字面量 URL 的网络调用。 |
ENCODED_PAYLOAD | WARN | 编码器的输出到达已披露端点的发送。判断“到达”的依据:编码器调用在发送的调用段内,或由编码器赋值的变量用于该段。 |
DISCLOSED_CAPTURE | WARN | 捕获 + 持久化/发送,已按类型披露。 |
CAPTURE_CAPABILITY | WARN | 捕获 API,同一文件中无持久化或发送。 |
HYGIENE_SHELL_TRUE | WARN | 代码行中存在 shell=True / shell: true。 |
HYGIENE_DYNAMIC_EXEC | WARN | 代码行中存在 eval( / exec( / os.system( / os.popen( / __import__(。 |
HYGIENE_DOWNLOAD_EXEC | WARN | 下载内容通过管道传给 shell(`curl … |
HYGIENE_OBFUSCATION | WARN | 文本文件中出现 base64 块(120+ 字符,大小写混合且含数字或 +/)、十六进制块(120+ 字符)、或连续 8 个以上 \xNN 转义。每个类型每个文件一个发现。 |
CONCEALED_UNICODE | WARN | 文本文件中出现不可见字符(零宽字符、单词连接符、软连字符、双向控制符、Unicode 标签字符);报告数量、首行位置和字符名称。前导 BOM 为签名而非隐藏,被忽略。 |
CONCEALED_BLANK_RUN | WARN | markdown 文件中出现 50 行以上连续空行,且其后有内容。 |
HIDDEN_COMMENT | WARN | markdown 文件中出现 120 字符以上的 HTML 注释。 |
WRITES_OUTSIDE_PACK | WARN | 脚本写入绝对路径、用户路径或系统路径(/tmp,/etc,~/,$HOME,…)。 |
INSTALL_HOOK | WARN | 安装时运行的文件:setup.py、install.* / setup.sh、.pth 文件,或包含 preinstall/install/postinstall 钩子的 package.json。 |
判定结果:BLOCKED = 至少一个危险发现 → 退出码 1。REVIEW = 无危险,但至少一个 ERROR 或 WARN → 退出码 0(使用 --strict 时为 1)。CLEAN = 无 ERROR 或 WARN(可保留 INFO)→ 退出码 0。REVIEW 故意返回 0:REVIEW 意味着“应由人类阅读”,而用于安装门控的脚本应在危险发生时失败,而不是对警告失败——--strict 专为不赞同此策略的人提供。
前置信息子集
解析器读取技能包实际使用的前置信息格式:key: value 标量(去除引号)、key: [a, b] 内联列表、key: 后跟 - item 块列表、以及折叠到前一个标量的缩进延续行。它故意不是完整的 YAML 解析器。超出子集的行会产生 FRONTMATTER_PARSE 错误并引用该行——vet 报告它无法读取的内容,而不是猜测。
扫描范围
- 包中每个文件都被记录并获取大小。跳过
.git和__pycache__目录。 - 文本扫描覆盖脚本文件(根据扩展名或 shebang)、markdown 文件和其他文本文件,每文件最大 2 MiB。二进制文件和包外符号链接目标被记录但不扫描。
- 引用收集自 SKILL.md 和脚本文件。其他 markdown 文件中的引用(如 references/*.md 链接到其他文件)在 v1.0.0 中不会跟进。
- 披露仅对照包根目录的 SKILL.md 和 README.md 进行判断——这是安装包的人预期已阅读的两个文档。
JSON 模式(--json)
一个对象:
tool,version,commandpack:dir(传入的路径),dirname,name(前置信息中的名称或 null),skill_filefiles[]:path,bytes,kind(script|markdown|text|binary|other),binary,scanned,symlink_outfindings[]:rule,severity(ERROR|WARN|INFO),danger(布尔),file(或 null),line(或 null),message,fixsummary:findings,errors,warnings,info,danger——计数与findings完全一致verdict(CLEAN|REVIEW|BLOCKED),verdict_rule,limits
发现结果按确定性排序:先 danger,然后严重级别、文件、行号、规则——同一包始终产生相同报告。
明确非目标(诚实说明)
- 它从不执行任何代码。 不运行、不导入、不编译、不解析 shell;不解码负载(编码块被报告,但不打开)。
- 静态模式是证据,不是证明。 一个在运行时用无害片段组装行为、将组合跨越两个文件(vet 按文件判断组合)、或用纯正明文编写社工内容的包可以通过。CLEAN 判定仅表示没有模式触发——README 如此声明,每个报告也如此说明。
- 二进制文件无法触及。 捆绑的二进制文件被标记——正是因为 vet 无法对其做出任何判断。
- 无遥测、无网络、无写入。 仅使用标准库;源码中完全不包含网络或子进程代码(测试会解析其导入并证明)。
- 模式表是快照。 新的捕获库、新的凭据存储和新的客户端 API 不断出现;源码中的表按发布版本标记日期,不被视为完整。