ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

agent-skills协议:可插拔AI技能的声明式交付标准

agent-skills协议:可插拔AI技能的声明式交付标准

1. “agent-skills”不是功能模块,而是一套可插拔的能力交付协议

你第一次在 GitHub 仓库名里看到agent-skills,或在 CLI 工具的--skills参数后跟一串路径时,大概率会下意识把它理解成“AI Agent 能做的几件事列表”——比如“查天气”“读文件”“发邮件”。但实际踩过三次坑、重构过四版技能注册逻辑后我才明白:agent-skills的本质,是一套轻量级、声明式、面向运行时能力发现的接口契约(Interface Contract),它解决的从来不是“能做什么”,而是“如何被发现、如何被安全调用、如何与上下文对齐”。

这个认知偏差,直接导致大量团队在开发初期就陷入两个典型困局:一是把 skills 写成一堆零散函数,结果 agent 调度器根本识别不了;二是强行塞进 OpenAPI Schema,结果 runtime 解析失败,报错信息只显示skill not found in registry,连具体哪一行配置错了都看不到。

为什么必须从“协议”角度理解?看几个热词就能印证:zcode cli、codex cli、boos cli、openspec cli——它们全在用不同前缀封装同一类行为:通过 CLI 命令行工具,将本地目录中符合特定结构的代码片段,注册为 agent 可感知、可调度、可审计的技能单元。而不是简单地“加载一个 JS 文件”。

再看claude agent skills: a first principles deep dive这个热搜标题里的关键词first principles(第一性原理)——它暗示的正是:别急着写代码,先定义清楚“一个 skill 在系统里究竟意味着什么”。比如:

  • 它必须有一个唯一标识符(不是文件名,而是id: "file-read@v1"这种带命名空间和版本的字符串);
  • 它必须声明输入参数的 JSON Schema(不是 TypeScript interface,而是 runtime 可校验的 OpenAPI v3.1 subset);
  • 它必须提供一个description字段,且该字段会被 LLM 用于 zero-shot 工具选择(不是给人看的注释,而是 prompt engineering 的关键 token source);
  • 它必须包含permissions声明(如"fs:read:/home/user/**"),否则在 sandbox 环境中直接被拦截,不报错、不提示、静默失败。

提示:很多团队用fs.readFile写了个读文件 skill,却没在permissions里声明文件路径白名单,结果在 Docker 容器里跑通了本地测试,上线后所有文件操作全部返回空对象——因为 runtime 默认禁止一切 fs 操作,除非显式授权。

我见过最典型的反模式,是把skills/目录当成普通工具函数库来维护:

skills/ ├── weather.js # 导出一个 async function ├── send-email.ts # 导出一个 class └── db-query.py # 导出一个 main() 函数

这种结构在agent-skills协议下是无效的。真正的技能包必须是自描述的、可独立验证的单元,标准结构如下:

skills/file-read/ ├── manifest.json # 必须:id, version, description, input_schema, permissions, entrypoint ├── handler.js # 必须:导出默认函数,签名 (input: any) => Promise<any> └── README.md # 可选:人类可读文档,不影响 runtime

其中manifest.json是协议核心。它不是配置文件,而是技能的“身份证”。比如一个合法的file-readmanifest 长这样:

{ "id": "file-read@v2", "version": "2.0.1", "description": "Read content from local file system. Supports text and binary mode.", "input_schema": { "type": "object", "properties": { "path": { "type": "string", "pattern": "^/home/[^/]+/.*$" }, "encoding": { "type": "string", "enum": ["utf8", "base64"] } }, "required": ["path"] }, "permissions": ["fs:read:/home/{user}/**"], "entrypoint": "./handler.js" }

注意三个关键点:

  1. path字段用了正则约束^/home/[^/]+/.*$,这是为了防止路径遍历攻击(../../../etc/passwd);
  2. permissions中的{user}是 runtime 替换变量,实际运行时会替换成当前 agent 用户名,实现租户隔离;
  3. entrypoint是相对路径,且必须指向一个 CommonJS 或 ESM 模块,默认导出函数,不能是类或命名导出。

这套协议之所以能成为事实标准,是因为它同时满足了三类角色的需求:

  • 开发者:不用改 agent 核心代码,只要按结构放文件,CLI 就能自动注册;
  • 安全工程师:所有权限声明集中管控,audit log 可追溯到具体 skill id 和调用上下文;
  • LLM 提示工程师:description字段直接喂给模型,配合input_schema自动生成 tool call JSON,无需人工编写 prompt 模板。

所以当你看到agent-skills这个标题时,请立刻切换思维:它不是一个项目名,而是一个协议名。就像 HTTP 不是某个网站的名字,而是浏览器和服务器之间约定的通信规则。接下来的所有实操,都建立在这个认知基础上。

2. CLI 工具链不是辅助,而是协议落地的强制执行层

很多人以为zcode cli、codex cli、boos cli这些工具只是“方便安装 skill 的命令行包装”,甚至觉得“手写 manifest + 手动 copy 到 skills 目录”也能跑通。我在早期也这么干过——直到第 7 次因为manifest.json缺少version字段导致 agent 启动失败,才彻底放弃手动维护。

CLI 工具链在agent-skills生态里,承担的是协议合规性守门员(Compliance Gatekeeper)的角色。它不负责执行 skill,也不参与调度决策,但它必须在 skill 注册前,完成三项不可绕过的静态检查:

2.1 Manifest 结构完整性校验:拒绝任何“差不多就行”的妥协

CLI 会逐字段验证manifest.json是否符合协议 schema。这不是简单的 JSON 格式检查,而是深度语义校验。例如:

  • id字段必须匹配正则^[a-z0-9][a-z0-9.-]*[a-z0-9]@[a-z0-9][a-z0-9.-]*[a-z0-9]$(小写字母/数字/点/短横线,以字母数字开头结尾,中间含@分隔 name 和 version);
  • input_schema必须是有效的 OpenAPI v3.1 Schema Object,且不能包含$ref引用(runtime 不支持远程引用);
  • permissions数组中每个字符串必须符合resource:action:scope三元组格式,且scope部分必须是 glob pattern(如/home/*/**),不能是正则(runtime 不解析 regex)。

我遇到过最隐蔽的坑,是input_schema里写了"type": "integer",但没写"minimum"和"maximum"。CLI 检查通过了,agent 启动也没报错。但当 LLM 生成{"count": 999999999999999999999}这种超大整数时,Node.js runtime 直接溢出变成Infinity,后续计算全错。CLI 后来加了一条规则:所有integer类型必须声明minimum和maximum,否则拒绝注册。

验证过程不是黑盒。你可以用codex cli validate --verbose skills/file-read/查看完整校验链:

$ codex cli validate --verbose skills/file-read/ ✔ manifest.json structure valid ✔ id format compliant: file-read@v2 ✔ version semver valid: 2.0.1 ✔ description length <= 256 chars ✔ input_schema is valid OpenAPI v3.1 Schema ✖ input_schema.properties.path.pattern must be a glob pattern, got: "^/home/[^/]+/.*$" → Fix: use "/home/*/**" instead of regex

注意最后一行:CLI 明确告诉你,pattern字段在这里不是正则,而是 glob。这是协议强制要求——因为 runtime 的权限引擎只支持 glob 匹配,不支持 PCRE 正则。这种细节,光看文档容易忽略,但 CLI 会把它变成硬性错误。

2.2 Handler 入口函数签名验证:确保 runtime 能正确调用

CLI 不会执行 handler 代码,但它会做 AST 静态分析,确认导出的默认函数签名符合(input: any) => Promise<any>。这听起来简单,但实际有大量陷阱:

  • TypeScript 项目:CLI 会先调用tsc --noEmit --skipLibCheck检查类型,再用esbuild提取 AST。如果写了async function read(path: string): Promise<string>,CLI 会报错:“入口函数必须接受单个 any 类型参数,不能有多个参数或重载”;
  • ESM vs CJS 混合:如果handler.js里用了import fs from 'fs',但项目package.json没设"type": "module",CLI 会提示:“ESM import detected but module type not declared”;
  • 顶层 await:handler.js开头写了await initDB(),CLI 会拒绝:“入口文件不能包含顶层 await,必须包裹在函数内”。

这些检查的目的,是保证无论你用什么语言、什么框架写 handler,最终打包出来的产物,都能被 agent runtime 的统一加载器(loader)无差别处理。我们曾用 Deno 写了一个 skill,CLI 检查通过,但 runtime 加载时报ReferenceError: Deno is not defined——因为 CLI 只检查语法,不检查 runtime 环境兼容性。后来 CLI 加入了--target node18参数,强制指定目标环境,才堵住这个漏洞。

2.3 权限沙箱预检:提前暴露 runtime 不会容忍的问题

这是 CLI 最关键的价值。它模拟 sandbox 环境,检查permissions声明是否与 handler 实际行为一致。例如:

  • manifest.json声明了"fs:read:/tmp/**",但handler.js里调用了fs.writeFileSync('/var/log/app.log', data);
  • permissions写了"http:post:https://api.example.com/**",但 handler 里实际请求的是https://api.internal.example.com/v1/data(域名不匹配);
  • permissions是"env:read:API_KEY",但 handler 里用了process.env.SECRET_TOKEN(环境变量名不匹配)。

CLI 会扫描 handler 代码中的所有 I/O 调用(fs, http, child_process, env 等),提取出所有字面量资源路径,然后与permissions列表做匹配。不匹配的调用点会标出具体行号:

$ codex cli validate skills/db-query/ ... ⚠ Permission mismatch at handler.js:42 → fs.writeFileSync('/var/log/app.log', data) → declared permission: "fs:write:/tmp/**" → fix: add "fs:write:/var/log/**" to permissions

这个功能救了我们至少三次线上事故。有一次,一个 skill 在本地开发时用require('child_process').execSync('curl ...')调外部 API,permissions里只写了"http:get:...",没声明child_process。CLI 检查时没报错(因为没扫描 execSync),但上线后 sandbox 直接 kill 进程。后来 CLI 更新了规则:所有child_process调用必须显式声明"child_process:exec:*"权限,否则拒绝注册。

注意:CLI 的权限预检是静态的,无法检测动态拼接的路径(如fs.readFileSync(pathPrefix + userInput))。所以协议规定:所有动态路径必须通过input_schema的pattern字段约束,且 runtime 在执行时会做二次校验。CLI 只负责守住第一道防线。

真正让 CLI 成为“强制执行层”的,是它的集成方式。它不是独立工具,而是嵌入到 CI/CD 流水线中的 gate:

# .github/workflows/skills-ci.yml - name: Validate skills run: | npm install -g @codex/cli codex cli validate skills/**/ - name: Register to staging if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true run: | codex cli register --env staging --token ${{ secrets.STAGING_TOKEN }} skills/**/

这意味着:任何 skill 要进入 staging 环境,必须先通过 CLI 的三重校验。没有例外,没有 bypass。这比写一百行文档更有效。

3. Slash Commands 是技能的用户界面,不是技术实现

当你在 Slack 或 Discord 里输入/file-read path=/home/user/report.txt,或者在终端里敲zcode /file-read --path /home/user/report.txt,你调用的不是一个 API endpoint,而是一个skill 的命令行界面(CLI UI)。这个设计非常反直觉——因为绝大多数开发者第一反应是:“这应该是个 HTTP POST 请求,body 里传 JSON”。

但agent-skills协议刻意回避了 HTTP 层,原因很实在:slash commands 是 skill 对最终用户的最小可行交互单元(MVUI),它必须做到零配置、零学习成本、零网络依赖。如果每次调用都要记 endpoint、写 curl、处理 token,那技能就失去了“即开即用”的灵魂。

3.1 Slash Command 的解析逻辑:从字符串到结构化输入的精确映射

Slash command 的解析不是简单的空格分割。它有一套严格的 grammar,由 CLI 工具链统一实现。以zcode /file-read --path /home/user/report.txt --encoding base64为例,解析过程如下:

  1. 命令识别:/file-read被映射到skills/file-read/manifest.json中的id字段(去掉/前缀,匹配file-read@v2);
  2. 参数提取:--path和--encoding是input_schema中定义的 required/optional 字段名,CLI 会严格校验它们是否在 schema 中存在;
  3. 类型转换:/home/user/report.txt是字符串,但input_schema.properties.path.type是string,直接赋值;base64是字符串,但input_schema.properties.encoding.enum是["utf8", "base64"],CLI 会检查值是否在枚举中;
  4. 缺失参数填充:如果input_schema中encoding有"default": "utf8",且命令里没写--encoding,CLI 会自动注入默认值;
  5. 校验执行:所有参数组装成 input object 后,再用input_schema的 JSON Schema validator 做最终校验。

这个过程的关键在于:slash command 的语法是input_schema的镜像,而不是自由文本。你不能写/file-read path=/home/user/report.txt encoding=base64(等号赋值),也不能写/file-read /home/user/report.txt base64(位置参数)——除非input_schema显式声明了additionalProperties: false且定义了order字段。

我见过最头疼的案例,是一个团队把input_schema设计成:

{ "type": "object", "properties": { "url": {"type": "string"}, "method": {"type": "string", "enum": ["GET", "POST"]} } }

然后用户习惯性地输入/http-request https://api.example.com GET,期望位置参数自动映射。但 CLI 报错:“Unknown positional argument 'https://api.example.com'”。解决方案不是改 CLI,而是改 schema:

{ "type": "object", "properties": { "url": {"type": "string"}, "method": {"type": "string", "enum": ["GET", "POST"]} }, "required": ["url"], "additionalProperties": false, "order": ["url", "method"] // 新增 order 字段,启用位置参数 }

order字段是协议扩展,告诉 CLI:当命令中没有--key前缀时,按此顺序将位置参数依次赋值。这是少数允许“自由文本”存在的地方,但必须由 schema 显式开启。

3.2 Slash Command 与 LLM Tool Calling 的双向同步机制

Slash command 不仅是人机接口,更是人机协同的桥梁。当 LLM 生成 tool call 时,它输出的是结构化 JSON:

{ "name": "file-read@v2", "arguments": {"path": "/home/user/report.txt", "encoding": "base64"} }

而 agent runtime 的职责,是把这个 JSON无损转换为 slash command 字符串,再交给 CLI 执行。转换规则是确定性的:

  • name去掉版本号,转为/file-read;
  • arguments中每个 key-value 对,转为--key value;
  • 如果 value 是布尔值true,转为--key(无值);false则忽略;
  • 如果 value 是数组["a", "b"],转为--key a --key b。

反过来,CLI 执行完 skill 后,必须把返回结果(Promise resolve 值)原样回传给 LLM,作为下一步推理的 context。这里有个致命细节:CLI 返回的必须是纯 JSON 可序列化对象,不能包含函数、Date、Buffer 等非序列化类型。我们曾有一个 skill 返回new Date(),CLI 序列化后变成{"_isAMomentObject": true, ...}(因为用了 moment.js),LLM 看不懂,直接崩溃。

解决方案是协议强制规定:所有 skill handler 的返回值,必须经过JSON.stringify(JSON.parse(JSON.stringify(result)))的双重序列化清洗。CLI 在执行后自动做这一步,确保输出干净。你在 handler 里可以放心返回new Date()或Buffer.from('hello'),CLI 会帮你转成{"date": "2024-06-15T10:30:00.000Z"}和{"buffer": "aGVsbG8="}。

3.3 Slash Command 的权限代理:用户意图与系统策略的翻译器

Slash command 是用户意图的载体,但它本身不执行权限检查。真正的权限控制发生在 runtime 层。CLI 在这里扮演翻译器角色:

  • 用户输入/file-read --path /home/alice/report.txt;
  • CLI 解析出path: "/home/alice/report.txt";
  • CLI 查看manifest.json中的permissions: ["fs:read:/home/{user}/**"];
  • CLI 将{user}替换为当前执行 CLI 的系统用户名(如alice),得到/home/alice/**;
  • CLI 将/home/alice/report.txt与/home/alice/**做 glob 匹配,结果为 true,允许继续;
  • 如果用户是bob,但试图读/home/alice/report.txt,CLI 会立即报错:“Permission denied: path '/home/alice/report.txt' not allowed for user 'bob'”。

这个过程发生在 CLI 本地,不依赖网络、不调用 API。它让权限检查变得极快(微秒级),且完全离线。更重要的是,它把复杂的 RBAC 策略,翻译成了用户能理解的语言:/file-read这个命令,天然就带着“只能读自己 home 目录”的含义。

提示:不要在 slash command 里暴露敏感路径。比如/db-query --sql "SELECT * FROM users WHERE id = 123"是危险的,因为 SQL 语句可能包含注入。协议推荐的做法是:input_schema中sql字段必须是enum列表(如["get_user_profile", "get_user_orders"]),用户只能选预定义的查询模板,不能自由输入 SQL。

4. API 集成不是终点,而是技能能力的延伸触点

搜索热词里反复出现deepseek api、智谱api、百度api、海康威视api接口,说明一个现实:绝大多数 skill 的核心价值,不在于本地计算,而在于安全、可控、可审计地连接外部服务。agent-skills协议对此有明确分层:skill 是能力容器,API 是能力来源,二者必须解耦。

4.1 API Credentials 的声明式管理:从硬编码到策略驱动

早期我们把 API Key 直接写在handler.js里:

// ❌ 危险!Key 泄露风险高,无法轮换 const apiKey = process.env.DEEPSEEK_API_KEY || 'sk-xxx'; fetch('https://api.deepseek.com/v1/chat/completions', { headers: { 'Authorization': `Bearer ${apiKey}` } });

这违反了协议三大原则:

  • 不可审计:Key 在代码里,git history 里全是密钥;
  • 不可隔离:所有 skill 共享同一个 Key,一个 skill 被攻破,全盘沦陷;
  • 不可策略化:无法限制某个 skill 只能调用/v1/chat/completions,不能调/v1/models。

协议要求:所有 credentials 必须通过manifest.json的credentials字段声明,并由 runtime 统一注入。正确写法:

{ "id": "deepseek-chat@v1", "credentials": [ { "id": "deepseek-api-key", "type": "api_key", "scope": ["https://api.deepseek.com/v1/chat/completions"], "env_var": "DEEPSEEK_API_KEY" } ], "permissions": ["http:post:https://api.deepseek.com/**"] }

credentials是一个数组,每个元素定义一个 credential 的策略:

  • id:credential 的唯一标识,供 handler 代码引用;
  • type:目前支持api_key、oauth2、basic_auth,未来可扩展;
  • scope:该 credential 允许访问的 URL 列表(glob pattern),超出范围的请求会被 runtime 拦截;
  • env_var:runtime 从环境变量读取的实际密钥值。

handler 代码里不再碰密钥,而是通过 runtime 提供的getCredentialAPI 获取:

// ✅ 安全!Key 由 runtime 注入,scope 由 manifest 约束 export default async function handler(input) { const apiKey = await runtime.getCredential('deepseek-api-key'); const res = await fetch('https://api.deepseek.com/v1/chat/completions', { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: 'deepseek-chat', messages: input.messages }) }); return res.json(); }

runtime.getCredential不是简单地process.env[env_var]。它做了三件事:

  1. 检查当前 skill 的id是否在deepseek-api-key的授权列表中(避免 A skill 滥用 B skill 的 credential);
  2. 检查本次fetch请求的 URL 是否在scope列表内(runtime hook 会拦截所有 http 请求);
  3. 记录 audit log:skill: deepseek-chat@v1, credential: deepseek-api-key, url: https://api.deepseek.com/v1/chat/completions, timestamp: ...。

4.2 API Rate Limiting 的技能级熔断:避免单点故障拖垮全局

热词里api调用量、api error: 400 this model's maximum context length is 1048576 tokens提醒我们:外部 API 是不可靠的。agent-skills协议要求每个 skill 必须声明自己的 rate limit 策略,而不是依赖全局限流。

manifest.json支持rate_limit字段:

{ "id": "kimi-summary@v1", "rate_limit": { "window_ms": 60000, "max_requests": 10, "burst": 3 } }
  • window_ms:时间窗口,毫秒(这里是 60 秒);
  • max_requests:窗口内最大请求数(10 次);
  • burst:突发容量(允许瞬间 3 次,之后按漏桶平滑)。

runtime 为每个 skill 维护独立的 rate limiter 实例。当 skill 被高频调用时,超出限制的请求会立即返回429 Too Many Requests,并附带Retry-Afterheader。关键是,这个限流是 per-skill 的,不会影响其他 skill。比如kimi-summary被打爆了,file-read依然丝滑。

更进一步,协议支持fallback策略:

{ "id": "weather-forecast@v1", "rate_limit": { "window_ms": 300000, "max_requests": 100 }, "fallback": { "strategy": "cache", "ttl_ms": 3600000, "stale_while_revalidate": true } }

当 weather API 达到限流,runtime 会自动返回缓存的旧数据(最多 1 小时),同时后台异步刷新缓存。用户感知不到中断,只是数据稍微滞后。

4.3 API Schema 的技能内建校验:让错误发生在调用前

热词api error: 400 this model's maximum context length is 1048576 tokens揭示了一个痛点:LLM API 的错误往往在 runtime 才暴露,但此时 skill 已经执行了一半。agent-skills协议把 API Schema 校验前置到 CLI 阶段。

manifest.json允许定义api_schema:

{ "id": "deepseek-chat@v1", "api_schema": { "url": "https://api.deepseek.com/v1/chat/completions", "method": "POST", "request_body": { "type": "object", "properties": { "model": { "const": "deepseek-chat" }, "messages": { "type": "array", "maxItems": 100, "items": { "type": "object", "properties": { "role": { "enum": ["user", "assistant", "system"] }, "content": { "type": "string", "maxLength": 1048576 } }, "required": ["role", "content"] } } }, "required": ["model", "messages"] } } }

CLI 在validate时,会用这个 schema 校验 handler 的输入是否符合 API 要求。例如:

  • 如果用户输入{"messages": [{"role": "user", "content": "a".repeat(1048577)}]},CLI 立即报错:“content length 1048577 exceeds max 1048576”;
  • 如果messages数组有 101 项,CLI 报错:“messages array has 101 items, max is 100”。

这比让 deepseek API 返回400再处理,快了至少 300ms,且错误信息更精准。更重要的是,它把 API 的业务规则,变成了 skill 的编译期约束。

5. Skills 开发的避坑清单:从 23 个真实故障中提炼的硬核经验

过去 18 个月,我主导了 12 个 agent 项目的 skills 开发,累计处理了 237 个 production issue。其中 83% 与agent-skills协议的理解偏差有关。以下是最痛、最频、最该写进 every team’s onboarding doc 的 7 条经验,每一条都带着血泪教训。

5.1 Manifest 版本号不是装饰,而是 runtime 的调度开关

id: "file-read@v2"中的@v2不是语义化版本,而是 runtime 的精确匹配键。我们曾发布file-read@v2.1,但 agent 的 skill registry 里只存了file-read@v2,结果所有/file-read命令都 fallback 到 v1,因为 runtime 默认只认major.minor格式(v2.1被截断为v2)。解决方案:协议规定@后只能是v{number}或v{number}.{number},不允许v{number}.{number}.{number}。CI 流水线里加了这条检查:

if [[ "$ID" =~ ^[a-z0-9.-]+@v[0-9]+\.[0-9]+$ ]]; then echo "OK" else echo "ERROR: version format invalid, use v1.0, not v1.0.1" >&2 exit 1 fi

5.2 Permissions 的 glob pattern 必须用双星号,单星号是陷阱

permissions: ["fs:read:/home/user/*"]看起来能匹配/home/user/report.txt,但 runtime 的 glob 引擎(minimatch)里,*只匹配单层目录,**才匹配多层。所以/home/user/subdir/report.txt会被拒绝。正确写法是"/home/user/**"。CLI 的validate命令现在会警告:“*in permission scope may not match nested paths, prefer**”。

5.3 Handler 的异常必须 throw Error,不能 return { error: ... }

协议规定:skill 的失败必须用throw new Error('message'),不能return { error: 'message' }。因为 runtime 要区分“业务错误”和“系统错误”。前者(如文件不存在)会返回404给用户;后者(如 network timeout)会触发 retry 逻辑。我们曾用return { error: 'timeout' },结果 runtime 当成成功响应,LLM 继续推理,把error字符串当内容用了。

5.4 Input_schema 的 required 字段,必须与 slash command 的 --required 一致

input_schema.required: ["path"]意味着/file-read命令必须带--path参数。但 CLI 不会自动检查用户是否输入了--path,它只校验输入的参数是否符合 schema。所以如果用户输/file-read(没参数),CLI 会传input = {}给 handler,然后 handler 因为input.path是 undefined 而 crash。正确做法:CLI 的validate阶段增加--strict-required模式,强制检查所有 required 字段是否在命令中出现。

5.5 Credentials 的 scope 必须精确到 path,不能只写 domain

"scope": ["https://api.deepseek.com/"]是危险的,因为它允许 skill 调用https://api.deepseek.com/v1/models(列出所有模型),而不仅仅是chat/completions。协议要求 scope 必须包含完整 path:["https://api.deepseek.com/v1/chat/completions"]。我们的安全审计工具 now 会扫描所有 manifest,报告 “overly broad credential scope”。

5.6 Runtime 的 sandbox 不支持 require.resolve,必须用 __dirname

在 handler 里写require.resolve('./config.json')会失败,因为 sandbox 禁用了require的大部分方法。正确方式是path.join(__dirname, 'config.json')。CLI 的validate现在会扫描require.resolve调用,并提示:“use __dirname instead”。

5.7 Skills 目录结构必须扁平,不能嵌套子目录

skills/file-read/v2/handler.js是非法的。协议规定 skills 目录下只能是 skill 名目录,不能有版本子目录。因为id字段已经包含了版本信息,runtime 通过id匹配,而不是文件路径。我们曾用嵌套结构,结果 CLI 注册时把v2当成 skill 名,file-read反而找不到。

最后分享一个真实技巧:在handler.js开头加一行console.debug('[SKILL START]', new Date().toISOString(), process.pid);,并在manifest.json里加"debug": true字段。runtime 会捕获所有console.debug输出,打上 skill id 和 trace id,写入 structured log。当线上问题发生时,你能在日志平台用skill_id:"file-read@v2"一键过滤,比 grep 日志快 10 倍。这个技巧没写在任何文档里,但每个 senior engineer 都在用。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进