v0.2.1 适配指南:SKILL 通道 + 通用二进制传输¶
协议版本:0.2.0 → 0.2.1
目标读者:python-sdk / rust-sdk 实现工程师
发布节奏:加性升级 + 一处条件性行为变更——SKILL / 通用二进制传输为全新通道(greenfield,无迁移);client:tool_call 仅在二进制内容超内联预算时行为改变(小结果线路兼容)
兼容性边界(先读这条)
- 纯文本 / 小二进制(≤ 内联预算)线路完全兼容:旧 SDK 不实现新通道也能继续跑原有 tool_call / 配置 / desktop 流程
- 大二进制会"静默截断"旧 Agent:若工具返回 / SKILL 资源含超预算二进制,未适配的 Agent SDK 会拿到空
data/blob(句柄在_meta里被忽略)。所以大二进制正确性要求 Agent + Computer SDK 同步升级 - SKILL 通道是新增能力,不实现则该能力缺失,但不影响存量功能
Greenfield 阅读提示
本指南假设 SDK 已实现 0.2.0。SKILL / blob-transfer 两章按 Greenfield 实现规范读(无旧形式可迁移);client:tool_call 一章是对既有事件结果处理的增量改造,重点章节。
1. 摘要¶
0.2.1 引入两条全新通道并把它们与既有 tool_call 在二进制传递上统一契约:
- SKILL 通道(全新):Agent 发现并使用 Computer 纳管的 SKILL 包。新事件
client:get_skills/client:get_skill,更新通知server:update_skills/notify:update_skills。多 source(mcp:/marketplace:/user),合成 name 命名空间,包根沙箱。 - 通用二进制传输(全新):
client:get_blob+ 不透明无状态blob_handle的生产者-消费者模型。任何通道把大/二进制内容以句柄旁路,Agent 经统一client:get_blob分块拉取,sha256自证完整性。 get_skillhybrid:文本且 ≤ 内联预算 → 内联body;二进制 / 过大文本 →blob_handle。tool_call二进制一致性(唯一触及既有事件):CallToolResult超预算二进制 content item 清空内联、经_meta.a2c_blob_handle走同一 blob-transfer 契约。- 错误码扩展:新增
4016/4017/4018;4014复用于 SKILL name 未注册;SKILL 通道不使用4015。
核心改动一句话版¶
Computer 端新增 SKILL 纳管 + 一个通用字节泵;Agent 端新增 SKILL 发现/读取 + 一个"见到
blob_handle就分块拉取并校验 sha256"的统一消费例程,并把它接到get_skill与tool_call两处。
2. 新增事件常量¶
SDK 的事件常量模块(python-sdk a2c_smcp/smcp.py、rust-sdk crates/smcp/src/lib.rs)MUST 增补:
| 常量(建议名) | 事件名 | 方向 | 必须实现方 |
|---|---|---|---|
GET_SKILLS_EVENT |
client:get_skills |
Agent→Server→Computer | Computer 处理 / Server 路由 |
GET_SKILL_EVENT |
client:get_skill |
Agent→Server→Computer | Computer 处理 / Server 路由 |
GET_BLOB_EVENT |
client:get_blob |
Agent→Server→Computer | Computer 处理 / Server 路由 |
UPDATE_SKILLS_EVENT |
server:update_skills |
Computer→Server | Server 处理 |
UPDATE_SKILLS_NOTIFICATION |
notify:update_skills |
Server→房间广播 | Agent 接收 |
server:update_skills / notify:update_skills 复用 UpdateComputerConfigReq({computer: str}),与 server:update_config 同构——无需新结构。
Server 若按事件白名单路由,需把上述
client:*加入白名单;若已是通用client:*转发则零改动。Server 不重组 blob,按computer逐条 ack 转发即可。
3. 新增数据结构¶
完整定义见 数据结构;此处给 SDK 落型摘要(字段语义以 spec 为准)。
SKILL
A2CSkillRef:name(必选,合成全局唯一,跨工具对齐裸名:marketplace<plugin>:<skill>/ user 裸名 / mcpmcp:<server>:<skill>)/source(必选,完整 provenance)/uri?(仅 MCP)/path(必选,Computer 本地绝对目录——staging 落盘是所有 source 统一第一步,恒存在)/description(必选)/license? compatibility? allowed_tools? skill_metadata?(frontmatter 派生)/version?(非 frontmatter:marketplace 取plugin.json、mcp 取_meta、user 缺省)。无mcp_server字段(source-agnostic,勿臆造)。GetSkillsReq:agent, req_id, computer;GetSkillsRet:skills: list[A2CSkillRef], req_id(排除孤儿,不排序去重)。GetSkillReq:agent, req_id, computer, name, rel_path?(POSIX 相对路径,缺省"SKILL.md")。注意:无 chunk 字段——分块属 blob-transfer,不在 get_skill。GetSkillRet:name, rel_path, mime_type, total_size, sha256,外加body?与blob_handle?恰一存在,req_id。
通用二进制传输
BlobHandle = str:不透明、Computer 铸造、无状态可重解析(无 session / 无 TTL)。Agent MUST NOT 解析/拼接/伪造/跨 Computer 复用。GetBlobReq:agent, req_id, computer, blob_handle, chunk_offset?(资源字节绝对偏移,缺省 0), max_chunk_bytes?(客户建议,Computer clamp)。GetBlobRet:blob_handle, mime_type, total_size, sha256, chunk_offset, eof, blob(base64 本块字节), req_id。
「资源字节」基准——三处口径必须一致
total_size / chunk_offset / sha256 一律基于 Agent 最终消费的资源字节:SKILL.md → frontmatter 剥离后 body;其它文件 → 原始字节;占位符 $TFROBOT_* 不展开(展开是 Agent SDK prompt 渲染层职责)。SDK 计算/校验 sha256 必须用同一口径,否则跨端校验恒失败。
4. SKILL 通道适配¶
4.1 Computer SDK(生产方,工作量最大)¶
- Skill Registry:把
mcp:/marketplace:/user多 source 物化(staging 落盘)到统一本地安装目录;每包符合 marketplace SKILL v1 §2(SKILL.md必存在,包根名 = frontmattername)。 - name 合成与 lexer:分形态——marketplace
<plugin>:<skill>(2 段)/ user 裸<skill>(1 段)/ mcpmcp:<server>:<skill>(3 段);按段数 +mcp:字面消歧(lexer 见 spec §1.4)。MCP server 段规范化 = Claude Code 通用规则([^a-zA-Z0-9_-]→_),不实现claude.ai特例。校验失败的 SKILL 不进 Registry(记 ERROR,不向 Agent 硬报错)。 client:get_skills:从 Registry 读可用项,排除孤儿(来源断开),按发现序返回,不读 body。client:get_skill:name lexer 失败 →4016;name 不在 Registry → 复用4014;rel_pathsafe_join后realpath必须仍在包根内,否则4017(reason∈traversal/forbidden/not_found);total_size超 SDK 可配上限 →4017 too_large(不铸句柄);仅 SKILL.md 剥 frontmatter,其它原样;文本且 ≤ 内联预算 →body,否则铸blob_handle。- 变更检测 →
server:update_skills:MCPResourceListChanged/Updated(skill://)、Marketplace git 源重拉对账、User DropIn / SDK 管理 UX 任一变更触发;探测机制 SDK 自决。 - 安全铁律:包根绝对路径只由 Registry 经 name 解析,禁止从 name/rel_path 推导;
.skillenv等敏感文件任何rel_path下都不可读出(命中即4017 forbidden,不泄漏存在性)。
4.2 Agent SDK(消费方)¶
- 收
notify:update_skills→ 自动client:get_skills刷新清单(建议)。 client:get_skill(name[, rel_path]):响应分支body(直接用)vsblob_handle(转 §5 拉取例程)。name当不透明可比较字符串,原样取自A2CSkillRef.name,不自行拼接(否则4016)。- 占位符展开(
$TFROBOT_*)是 Agent SDK 在 prompt 渲染层的职责,不是 Computer 协议层职责。
4.3 Server SDK¶
路由三个新 client:*;收 server:update_skills 向房间广播 notify:update_skills(复用既有 update 广播机制)。
5. 通用二进制传输适配(最高复用价值章节)¶
client:get_blob 是所有大/二进制内容的唯一搬运通道,被 get_skill 与 tool_call 共用。实现一次,两处复用。
5.1 Agent SDK:统一拉取例程(务必抽成单一可复用函数)¶
行为契约(Python reference impl;rust-sdk 按同一契约实现,协议文档不堆多语言示例):
def drain_blob(call, computer: str, blob_handle: str) -> tuple[bytes, str]:
"""call = 已封装的 emit-with-ack(self.call / emit_with_ack)。返回 (完整字节, mime_type)。"""
buf = bytearray()
offset = 0
expect_sha = expect_total = mime = None
while True:
ret = call("client:get_blob", {
"agent": ..., "req_id": new_req_id(), "computer": computer,
"blob_handle": blob_handle, "chunk_offset": offset,
})
# 4018 → 见 §7:invalid_handle/forbidden 不重试;gone 回生产者重取;range 修正 offset
if expect_sha is None:
expect_sha, expect_total, mime = ret["sha256"], ret["total_size"], ret["mime_type"]
elif ret["sha256"] != expect_sha or ret["total_size"] != expect_total:
return drain_blob(call, computer, blob_handle) # 源被改写:从 0 重读
buf += base64.b64decode(ret["blob"])
offset = ret["chunk_offset"] + len(base64.b64decode(ret["blob"]))
if ret["eof"]:
break
if hashlib.sha256(buf).hexdigest() != expect_sha:
raise IntegrityError # 损坏,重读
return bytes(buf), mime
要点:chunk_offset 绝对偏移、无服务端状态 → 幂等/可续传/可并行;背压由 pull 节奏天然提供;eof ⟺ offset+本块 == total_size;空资源 = 单响应 total_size=0/eof=true。
5.2 Computer SDK:句柄解析¶
- 铸造:不透明、无状态、可确定性回源;绝不编码可被 Agent 利用越权的明文路径。
- 解析(每次
get_blob):重跑铸造通道的边界校验(SKILL → §4.1 沙箱),防御纵深;失败 →4018 forbidden;源消失 →gone;chunk_offset越界 →range;句柄无法识别 →invalid_handle。 - 单块 ≤ Server
maxHttpBufferSize(计入 base64 +33% 与 envelope);max_chunk_bytes缺省时 Computer 自定。 client:get_blob不是任意文件读原语。
6. client:tool_call 二进制一致性适配 ⚠️(唯一触及既有事件)¶
CallToolResult 是 MCP 标准结构,schema 不可变。0.2.1 起:
- 逐个二进制 content item(
ImageContent/AudioContent/EmbeddedResource的 blob)按与 get_skill 同一内联预算判定 ≤ 预算:维持原生内联 base64,行为不变(小截图零额外往返)> 预算:清空该 item 的内联data/blob,在该 item 的_meta写a2c_blob_handle,并镜像a2c_total_size/a2c_sha256(MIME 复用 item 既有mimeType)- 工具自身失败仍走 MCP
CallToolResult.isError(不引入 A2C 事件级错误码——既有不变量)
载体差异仅因 MCP 结构不可变所致(与 SMCPTool.meta 的 a2c_tool_meta 命名空间旁路同构);除位置外,句柄语义/拉取/校验/错误与 get_skill 完全一致。
Computer SDK 改造:tool_call 返回前,遍历 CallToolResult.content,对超预算二进制 item 执行上述替换(复用 §5.2 铸造)。
Agent SDK 改造(破坏性关键点):消费 CallToolResult 时必须遍历 content item 检测 _meta.a2c_blob_handle,命中则调 §5.1 drain_blob 还原字节后再交付上层。不实现 = 大二进制工具结果静默变空。
7. 错误码适配¶
| 码 | 事件 | 触发 | Agent 行为 |
|---|---|---|---|
4016 |
client:get_skill |
name 格式非法(lexer 失败) |
不重试;原样用 A2CSkillRef.name |
4017 |
client:get_skill |
铸造期:details.reason ∈ traversal/forbidden/not_found/too_large(+rel_path/total_size) |
traversal/forbidden/too_large 不重试;not_found 回退仅用 SKILL.md |
4018 |
client:get_blob |
拉取期:details.reason ∈ invalid_handle/forbidden/gone/range |
invalid_handle/forbidden 不重试;gone 回生产者重取句柄;range 修偏移重试 |
4014(复用) |
client:get_skill |
name 合法但不在 Registry(不存在/卸载/孤儿) | 刷新清单后重试 |
- SKILL 通道不使用
4015(未声明resourcescapability 的 server 在物化阶段已排除)。 - 沿用既有扁平 ErrorPayload shape:
code/message顶层,code-specific 字段顶层,诊断在details。detailsMUST NOT 透传给最终用户。 details.reason为开放枚举,SDK 解析 MUST 容忍未来新增值(默认按"不重试 + 诊断"兜底)。
8. 兼容性与发布建议¶
- 版本语义:0.2.0 → 0.2.1(v0.x PATCH)。新通道纯加性;
tool_call仅大二进制路径行为变更。 - 同步升级要求:要让大二进制(工具截图 / SKILL 二进制资源)正确,Agent SDK 与 Computer SDK MUST 同步升级;只升一端 → 大二进制空数据。纯文本/小二进制可不同步。
- Server SDK 只需事件路由 + 广播,向后兼容,建议先行升级。
- 建议发布顺序:Server → Computer → Agent;三方均升后大二进制能力方完整。
9. 适配检查清单¶
Server SDK
- 事件常量增补 5 项;
client:get_skills/get_skill/get_blob路由;server:update_skills→notify:update_skills广播
Computer SDK
- Skill Registry + 多 source staging(marketplace SKILL v1 §2 包结构)
- name 合成 / lexer / 规范化(不实现
claude.ai特例);校验失败不进 Registry -
get_skills(排除孤儿)/get_skill(4016/4017/4014、frontmatter 仅 SKILL.md、inline-vs-handle、too_large) - 变更检测三源 →
server:update_skills -
get_blob:句柄解析 + 重施沙箱 + 切片 + 4018;单块 ≤ Server buffer -
tool_call:超预算二进制 content item →_meta.a2c_blob_handle(+a2c_total_size/a2c_sha256) - 安全:包根只经 Registry 解析;
.skillenv任何 rel_path 不可读
Agent SDK
- 单一
drain_blob例程(offset 循环 / eof / sha256 校验 / total_size 变更重读 / 4018 处理) -
get_skill分支bodyvsblob_handle→drain -
tool_call消费遍历 content_meta.a2c_blob_handle→drain(破坏性关键点) -
notify:update_skills→ 重拉get_skills - 4016/4017/4018 解析;
details.reason开放枚举兜底;details不外泄 - 占位符展开在 prompt 渲染层(不在协议层)