跳转至

A2C-SMCP 错误处理规范

状态: 草案

本章节目前为草案状态,具体错误码与结构仍在演进中。

概述

A2C-SMCP 协议定义了统一的错误处理机制,确保 Agent、Server、Computer 之间能够正确传递和处理错误信息。

错误码定义

通用错误码

代码 名称 含义 典型触发场景
400 Bad Request 无效请求格式 数据结构校验失败、字段缺失或类型错误
401 Unauthorized 未授权 认证失败、Token 无效
403 Forbidden 权限违规 跨房间访问、Agent 独占冲突、未授权操作
404 Not Found 资源不存在 工具或 Computer 不存在、MCP 配置缺失
408 Timeout 请求超时 工具调用超过约定超时时间未返回
500 Internal Error 内部错误 Server 或 Computer 端逻辑异常

工具调用错误码

代码 名称 含义
4001 Tool Not Found tool_name 在指定 MCP Server 中不存在;不含 mcp_server 本身缺失场景(用 4014)
4002 Tool Disabled 工具被禁用
4003 Tool Execution Failed 工具执行失败
4004 Tool Timeout 工具执行超时
4005 Tool Requires Confirmation 工具需要二次确认
4006 Tool Authorization Required 工具需要 MCP 上游授权(如 OAuth 2.0),Computer 当前无有效凭证或尚未完成授权(见下方4006/4007 判定决策表)
4007 Tool Authorization Failed MCP 上游授权流程失败、Token 已失效、刷新失败、或权限不足(见下方4006/4007 判定决策表)

MCP Server 路由错误码

代码 名称 含义
4014 MCP Server Not Found 引用的 mcp_server(bundle_id)未注册(见 §MCP Server Not Found)
4015 MCP Capability Not Supported MCP Server 已注册但未声明所需 capability(见 §MCP Capability Not Supported)

BundleID / exposed_tool_name 的配置问题不进协议错误码:重复 bundle_id(no-double-open)、非法 bundle_id、同 server 内 alias 撞出相同 exposed_tool_name——均在配置加载期由 Computer 检出,属 Computer 本地配置诊断(日志 / 本地 UI),不由任何 client:* 事件触发(对齐 SKILL「物化失败 / 孤儿 / 跨 source 冲突不进协议错误码」先例)。畸形 ToolMeta Server 声明(非对象 / tags 类型错误)同属此模式——工具组装期由 Computer 检出并降级(三层合并规则 + 配置诊断),不产生任何错误码。仅 client:tool_call 的 tool_name(exposed_tool_name)在 ExposedToolMapping 未命中属运行期客户端错误 → 复用 4001 Tool Not Found。

SKILL 通道错误码

代码 名称 含义
4016 Invalid Skill Name client:get_skill 入参 name 格式非法(违反 SKILL name lexer 规则;见 §Invalid Skill Name)
4017 Skill Resource Not Accessible client:get_skill 的 rel_path 路径穿越 / 命中禁止文件 / 包内不存在(见 §Skill Resource Not Accessible)

复用与不使用:SKILL name 格式合法但不存在(不存在 / 已卸载 / 已孤儿)复用 4014 MCP Server Not Found 语义;name 格式非法 → 4016;name 有效但 rel_path 不可达 → 4017。SKILL 通道不使用 4015——未声明 resources capability 的 server 在物化阶段即被排除,不会上送 Agent。物化失败 / 孤儿 / 跨 source 冲突等内部事故不进协议错误码(Computer 日志即可,batch 接口对部分失败健壮)。二进制 / 过大文本经 blob_handle 转 client:get_blob:铸造期的沙箱与上限属 4017,拉取期的句柄/源/范围属 4018,二者不重叠。

通用二进制传输错误码

代码 名称 含义
4018 Blob Not Accessible client:get_blob 句柄无效 / 重施鉴权失败 / 源消失 / 范围越界(见 §Blob Not Accessible)
4019 Blob Write Failed client:put_blob 会话无效 / 声明非法 / 顺序错乱 / 超上限 / 会话繁忙 / 沙箱不可写 / 完整性不符 / IO 失败(见 §Blob Write Failed)

边界:4018 属传输下行拉取期;4019 属传输上行写入期,二者方向相反、互不重叠。下行资源的鉴权与绝对上限由铸造句柄的生产者通道在铸造期决断(SKILL → 4017,不通过则不铸造句柄);上行资源的绝对上限由 Computer 在首块决断(total_size 超限 → 4019 too_large,零字节落盘)。4017(SKILL 铸造期专属)与 MCP CallToolResult.isError(MCP 事件专属)均不用于上行。详见 通用二进制传输。

连接与房间管理错误码

代码 名称 含义
4008 Protocol Version Mismatch HTTP 握手阶段,URL query 中的 a2c_version 与 Server 不兼容
4101 Room Full 房间已有 Agent
4102 Room Not Found 房间不存在
4103 Not In Room 未加入房间
4104 Cross Room Access 跨房间访问被拒绝

错误响应格式

A2C-SMCP 协议级错误(HTTP 握手层 + Socket.IO ack 层)统一采用扁平 ErrorPayload shape:标准字段 code / message 顶层平铺,code-specific 字段(如 mcp_server / capability)顶层并列,诊断信息封装在 details 子对象内。

无嵌套 envelope

协议不使用 {"error": {"code": ..., "message": ...}} 形式的嵌套包装。SDK 反序列化时直接读取顶层字段,禁止二次 unwrap。

作用域:本节定义所有 client:* 事件 ack 层与 HTTP 握手层的协议级错误响应 shape——任一 client:* 路由(包括 client:get_tools / client:get_desktop 等历史未列举专属错误码的路由)在 ack 通道上返回的协议级错误,均 MUST 采用本节定义的扁平 shape;SDK 路由层 MUST 对 ack payload 命中 ErrorPayload(即顶层含 code 且语义匹配)做原样透传,不得按路由开关此契约。不适用:(a) server:join_office 等返回 (success, error_msg) 元组的事件,见 §事件级错误处理;(b) client:tool_call 工具失败,使用 MCP CallToolResult.isError=true,见 §错误传播。

Flat ErrorPayload schema

class ErrorPayload(TypedDict, total=False):
    code: int                       # 必选:错误码
    message: str                    # 必选:人类可读描述
    details: NotRequired[dict]      # 可选:诊断容器,仅供日志 / 调试
    # 各错误码可附加 code-specific 顶层字段,详见下方总表

传输分层

层 错误码 承载方式
HTTP 握手层 4008 HTTP 400 响应 body(JSON)+ X-A2C-Error-Code header(冗余诊断)
Socket.IO ack 层 4014 / 4015 ack callback 第一参(dict)

两层使用同一种 flat shape,差别仅在传输通道。

各错误码标准字段总表

除 code / message 外的顶层 code-specific 字段,以及 details 内推荐 key:

code 顶层 code-specific 字段 details 内推荐 key
4008 server_version / client_version / min_supported / max_supported —
4014 mcp_server —
4015 mcp_server / capability —
4016 — name
4017 — reason / rel_path / total_size
4018 — reason
4019 — reason / upload_id / chunk_offset / total_size

各错误码完整 payload 示例与触发时机详见对应章节(§4008 / §4014 / §4015)。

details 字段约束(协议级)

details 是诊断信息容器,承载日志 / 调试上下文。协议级约束:

  • Agent MUST NOT 把 details 内容透传给最终用户——避免泄露内部实现细节
  • 协议级标准 key 见上表;新增 standard key 算 minor 升级
  • 业务自定义 key 可附加,不进入协议演进语义(不算 breaking)

details 内禁止包含的敏感信息

  • API 密钥或 Token
  • 内部 IP 地址或端口
  • 数据库连接信息
  • 用户密码或凭证
  • 堆栈跟踪(生产环境)

SDK 反序列化建议

按 code 分发到 code-specific TypedDict 解析,不要写 over-generic details: dict 把所有顶层字段折叠收集——会丢失类型信息与 IDE 推导能力。

注:协议规范仅提供 Python reference impl。其他 SDK 由各自实现决定具体解析模式——A2C-SMCP 协议文档不堆砌多语言示例。

事件级错误处理

server:join_office 响应

# 成功
(True, None)

# 失败
(False, "Room already has an agent")

client:tool_call 响应

工具调用使用 MCP 的 CallToolResult 结构返回结果:

class CallToolResult:
    content: list[TextContent | ImageContent | EmbeddedResource]
    isError: bool  # 是否为错误结果
    meta: dict     # 结果级元数据(线上 key 为 meta,承载 a2c_* 扩展标记)

当 isError=True 时,content 中包含错误信息。isError=True 可由普通失败、超时或取消产生;三者通过结果级 meta 标记区分:

终态 isError 结果级 meta 标记 说明
普通失败 True 无 a2c 终态标记(授权失败另带 meta.error_code,见 §MCP 上游授权错误响应) 工具自身执行失败
超时 True meta.a2c_timeout = true Agent / Computer 端超时;见 §超时处理
取消 True meta.a2c_cancelled = true(+ 可选 meta.a2c_cancel_reason) 由 notify:tool_call_cancel 中断;见 事件 §notify:tool_call_cancel

标记完整定义见 数据结构 §CallToolResult 结果级 A2C 标记。

超时处理

Agent 端超时

Agent 在发起工具调用时指定 timeout:

result = await agent.emit_tool_call(
    computer="my-computer",
    tool_name="slow_tool",
    params={},
    timeout=30  # 30 秒超时
)

超时后,Agent 应:

  1. 发送 server:tool_call_cancel 取消请求
  2. 返回超时错误给调用方
# 超时返回示例
CallToolResult(
    content=[TextContent(text="Tool call timeout")],
    isError=True,
    meta={"a2c_timeout": True}   # 结果级标记:标识超时(区别于取消的 a2c_cancelled)
)

命名空间收敛: 历史示例曾用未加命名空间的 meta={"timeout": True};现统一到 a2c_* 命名空间下的 a2c_timeout,与 a2c_cancelled 一致,均为结果级 meta 标记。详见 数据结构 §CallToolResult 结果级 A2C 标记。

Server 端超时

Server 在转发请求时应设置合理的超时:

  • 使用 Agent 请求中的 timeout 值
  • 添加少量缓冲时间(如 5 秒)

Computer 端超时

Computer 应在工具执行超时时:

  1. 尝试中断工具执行
  2. 返回超时错误

取消语义(无 ack、无错误码)

server:tool_call_cancel 为 fire-and-forget:Server 收到后仅向房间广播 notify:tool_call_cancel,不回执(无 ack)。

取消刻意不分配错误码、不投递扁平 ErrorPayload,理由与 §飞行中断连 同源:

  • 无 ack 通道即无错误码读者;取消是广播通知而非请求-响应,强行定义回执错误码无意义。
  • Agent 端发出 server:tool_call_cancel 后收到的 ack 为 None 是合规预期,MUST NOT 据此判定"未实现 / 失败"。

取消的"结果"通过原 client:tool_call 的 ack 体现,而非取消事件本身:被中断时 Computer 对原调用返回 CallToolResult(isError=True, meta={"a2c_cancelled": True}),见 §client:tool_call 响应。

错误码复用 / 不使用:取消链路不新增错误码、亦不复用任何既有错误码(4004 Tool Timeout 仅用于超时,不用于取消)。req_id 命中不到在途调用时 Computer 静默忽略(不回错误码),见 事件 §notify:tool_call_cancel。

飞行中断连(in-flight disconnect)

Agent 发出 client:* 事件后,在 Server 转发 / Computer 处理 / 响应回程任一阶段断连,Server MAY 静默丢弃此 in-flight 请求(不 ack、不投递扁平 ErrorPayload)。

理由(为什么不分配新错误码):

  • ack callback 归属于 originator socket——originator 断连后,ack 投递路径本身已经消失,"错误码"没有读者,强行定义无意义
  • 是否在线属于 Socket.IO 传输层关注点,A2C 应用层不应越界用错误码表达连接状态——保持职责分层
  • 与"Computer 已响应但 Agent 在响应送达前断连"在本质上是同一类事件(originator 不可达),协议不为该类事件分配专属错误码

Agent 实现要求:

  • MUST NOT 依赖 ack 超时检测自身断连——使用 Socket.IO disconnect / connect_error 事件
  • client:get_* 类事件建议本地 ack 等待超时 ≥ 30s(覆盖 Server 路由 + Computer 处理冗余)
  • client:tool_call 沿用 payload 内 timeout + Server 处理冗余(不另设 ack 超时)

Server 实现要求:

  • in-flight 路径上发现 originator session 已不可达(如 session is None / SID 已注销)时,MUST NOT crash
  • 推荐 raise 一个受控的 namespace 异常(如 SMCPNamespaceError),由框架级 handler 静默收编为"不 ack"——不使用 assert(生产 -O 模式会被剥离)
  • MAY 写入审计日志;MUST NOT 向 Computer 或其它房间成员广播"originator gone"事件
  • 已转发给 Computer 的请求若回程时 originator 已断连,Server 直接丢弃响应,不回滚 Computer 端副作用——副作用幂等性是 Computer 自身的设计契约

重试策略

建议的重试策略

错误类型 是否重试 策略
400 Bad Request 否 修复请求后重试
401 Unauthorized 否 重新认证后重试
403 Forbidden 否 不重试
404 Not Found 否 不重试
408 Timeout 可选 指数退避重试
500 Internal Error 可选 指数退避重试

指数退避示例

async def retry_with_backoff(func, max_retries=3):
    for i in range(max_retries):
        try:
            return await func()
        except TimeoutError:
            if i == max_retries - 1:
                raise
            wait_time = (2 ** i) + random.uniform(0, 1)
            await asyncio.sleep(wait_time)

错误传播

工具调用错误传播链

MCP Server → Computer → Server → Agent
     │           │          │        │
     └───────────┴──────────┴────────┘
                错误信息保留

每一层应:

  1. 记录错误日志
  2. 将错误信息向上传播
  3. 不丢失原始错误细节

日志记录建议

# 推荐的日志格式
logger.error(
    "Tool call failed",
    extra={
        "req_id": req_id,
        "tool_name": tool_name,
        "computer": computer,
        "error_code": error.code,
        "error_message": error.message
    }
)

协议版本不匹配(4008)

触发时机:客户端(Agent / Computer)通过 Socket.IO 连接 Server,HTTP 中间件层校验 URL query 中的 a2c_version 发现与 Server 不兼容。校验发生在 Socket.IO 处理之前,业务代码无法影响。

4008 是 HTTP body code,不是 WS close code

明确语义边界

4008 是 ErrorPayload.code 字段值,承载于 HTTP 400 响应 body 中。 4008 不是 WebSocket close code。polling 握手路径上版本校验在 HTTP 层完成、发生在 WS 帧建立之前。 WS-only 直连握手另有拒绝形态:支持 ASGI denial-response 的栈仍回字节一致的 4008 HTTP body;不支持时回退到协议指定的、与 4008 取不同值的 WebSocket close code 4900(详见 versioning.md §5)。 SDK 实现 MUST NOT 把 4008(ErrorPayload.code)与 4900(WS close code)混淆或互换。

HTTP 响应规范

HTTP/1.1 400 Bad Request
Content-Type: application/json
X-A2C-Error-Code: 4008

{
  "code": 4008,
  "message": "Protocol version mismatch",
  "server_version": "0.2.0",
  "client_version": "0.1.5",
  "min_supported": "0.2.0",
  "max_supported": "0.2.999"
}

X-A2C-Error-Code 响应 header 是冗余诊断辅助:当客户端无法访问 body 时(罕见 transport 边角情况),可从 header 中识别错误类型。不替代 body,body 是 single source of truth。

字段说明

字段 必需 说明
code 是 固定 4008
message 是 人类可读的错误信息
server_version 是 Server 当前支持的协议版本(Client 据此决定是否升级)
client_version 是 Server 从 URL query 读取的客户端版本(回显供诊断)
min_supported 是 Server 支持的最低协议版本
max_supported 是 Server 支持的最高协议版本

SDK 实现要求

  • Client SDK 必须解析 HTTP 400 的响应 body,识别 code: 4008,转化为专属异常(如 ProtocolVersionError),禁止静默重试
  • 异常信息应明确告知用户两边版本,便于快速判断应升级哪端
  • 客户端 transport 顺序 SHOULD 配置为 polling → websocket(绝大多数 socketio 客户端默认即如此),保证首个握手请求是 HTTP polling,body 可访问
  • 可选:SDK 在本地日志中打印诊断信息(当前 SDK 声称的 PROTOCOL_VERSION 常量、接收到的 server_version)

Python 解析示例(reference impl)

import json
import socketio
from a2c_smcp import PROTOCOL_VERSION
from a2c_smcp.exceptions import ProtocolVersionError

sio = socketio.AsyncClient()
try:
    await sio.connect(
        f"wss://server.example.com?a2c_version={PROTOCOL_VERSION}",
        auth={"token": "...", "agent_id": "..."},   # role 经 server:join_office 声明
    )
except socketio.exceptions.ConnectionError as e:
    # python-socketio 在 HTTP 非 2xx 时把 body 放进异常 message
    raw = str(e)
    try:
        body = json.loads(raw)
    except json.JSONDecodeError:
        raise  # 非协议级错误,保持原异常
    if body.get("code") == 4008:
        raise ProtocolVersionError(
            client_version=body.get("client_version"),
            server_version=body.get("server_version"),
            min_supported=body.get("min_supported"),
            max_supported=body.get("max_supported"),
        )
    raise

注:协议规范仅提供 Python reference impl。其他 SDK(Rust / TypeScript / Go 等)的具体解析模式由各 SDK 自行决定——A2C-SMCP 协议文档不堆砌多语言示例。

详细的版本语义、兼容性规则与握手流程见 协议版本与握手。


MCP 上游授权错误响应

MCP 协议自身定义了工具服务器的 OAuth 2.0 授权流程。A2C-SMCP 不介入该握手过程;本章仅定义授权结果如何向 Agent 反馈,使 Agent 能够区分"工具坏了"与"工具需要用户授权"。

完整的安全边界、Computer 实现要求与 Agent 行为约束,见 security.md → MCP 上游授权(OAuth 2.0 等)。 本章仅承载响应结构与错误码,行为层约束以 security.md 为准,避免重复维护。

授权失败的响应结构

当 Computer 调用 MCP 工具因上游授权问题失败时,使用 CallToolResult 返回错误,并在 meta 中携带结构化提示:

CallToolResult(
    content=[TextContent(text="此工具需要授权 GitHub 账号后方可使用")],
    isError=True,
    meta={
        "error_code": 4006,  # 或 4007
        "mcp_server": "github-mcp",
        "auth_hint": {
            # 可选:引导用户完成授权的提示信息
            "action": "user_authorization_required",
            "message": "请在 Computer 宿主环境完成 GitHub OAuth 登录后重试"
        }
    }
)

4006/4007 判定决策表

为消除"未授权 vs 授权失败"的边界歧义,Computer MUST 按下表把上游 MCP Server / OAuth Provider 的具体表现映射到协议错误码:

上游表现 错误码 语义
Computer 从未为该 MCP Server 配置授权(无 token / 无 client credentials) 4006 用户首次授权
上游返回 HTTP 401 Unauthorized(凭证缺失、非法、未提供) 4006 用户重新授权
上游返回 HTTP 403 Forbidden(已登录但权限/scope 不足) 4007 已授权但能力不足,用户调整 scope 或换账号
Token 过期 + 刷新失败(refresh_token 无效 / 被撤销) 4007 历史授权失效,用户需重新走 OAuth 流程
用户已主动 revoke OAuth 授权 4007 历史授权已被撤销
凭证存在但 scope 不足(上游未明确返回 401/403,但断言权限不够) 4007 同 403 类,归"已授权但失败"
OAuth provider 自身故障(5xx / 网络) 4003 Tool Execution Failed 不属于授权语义,按通用工具失败处理

简明判别:

  • 区分点是"用户是否曾经授权"——4006 = 没授权过 / 凭证不存在;4007 = 曾授权但当前不可用
  • 当 Computer 已判定上游失败属授权类但无法区分 401 与 403 时,MUST 报 4006(见降级语义;提示用户重新走授权流程是稳妥兜底)

Agent 行为差异:Agent 收到 4006 应引导用户首次/重新完成授权;收到 4007 应引导用户检查权限设置或重新授权——区别仅在 UX 文案,机器路由可统一。

降级语义:授权类失败的硬映射

判定决策表假定 Computer 能观测到上游的明确表现(HTTP 状态码、Token 过期信号等)。现实中,MCP 客户端库可能把授权失败吸收进自有错误变体而丢失原始状态码——例如 rmcp 把 401 + WWW-Authenticate 短路成 AuthRequired 语义变体、其字符串化不含 401 字样;mcp-python 在某些路径把 401/403 抛进传输任务组并拆连接,调用方既拿不到状态码也拿不到结构化错误。为防止此类情形让授权失败被错误降级,Computer MUST 遵守:

  • 一旦判定上游失败属授权类——无论依据是 HTTP 状态码、库的语义错误变体、还是 WWW-Authenticate 响应头——MUST 按判定决策表映射为 4006 或 4007;MUST NOT 降级为 4003 Tool Execution Failed,亦 MUST NOT 返回缺 meta.error_code 的 isError=true 通用失败结果。
  • 当上游失败已判定属授权类但无法区分 401 与 403(库只给出"需要授权"语义而无状态码)时,MUST 报 4006(提示用户重新走授权流程是稳妥兜底)。

本条把原"简明判别"中"无法可靠判别时倾向报 4006"由建议升格为 MUST,并显式覆盖"客户端库吞掉状态码"这一判定决策表未直接处理的维度。"是否进 4006/4007 而非掉出表外"是协议硬约束;至于 Computer 靠状态码匹配、错误变体 downcast、还是传输层拦截来判定授权类,属 SDK 自治——协议只规定结果,不规定方法。

可观测判据:禁止挂起至超时

授权失败的 surfacing MUST NOT 表现为调用挂起至 Agent 超时:

  • 上游对 tools/call 返回授权失败(401/403/语义变体)后,Computer MUST 在自身超时或 Agent 超时之前(取较早者)产出携带 meta.error_code 的 CallToolResult(isError=true),而非让 client:tool_call 的响应永远悬空。conformance 通过 time-box 二元观测验证(返结果 vs 挂起);具体 time-box 阈值归 SDK 自治,协议只约束"不得挂至超时"。
  • 当 Computer 已观测到授权失败信号(HTTP 状态码 / 库语义错误变体 / WWW-Authenticate 响应头)但 CallToolResult 因底层库拆连接、关流、或把异常投递给后台任务组而不会经原路径返回时,Computer MUST 在自身层面兜底,按降级语义合成 4006/4007 结果返回。仅在已观测到授权失败信号的前提下才合成授权错误——纯网络抖动 / provider 5xx / 无授权信号的超时等"无响应"不得归类为授权失败(见判定决策表 5xx 行 → 4003)。

此条针对已坐实的失效:python-sdk 在 mcp-python streamable_http.py post_writer 上 401/403 被抛进传输任务组→拆连接关 read_stream→call_tool 挂起至 agent timeout(POST 阶段授权失败)。检测与兜底机制归 SDK 自治,但"不得挂起"是对端可感的硬约束。真实传输 conformance 向量见 fixtures/auth_error_conformance_vectors.json 与 Computer 管理面一致性测试 → §4.8 上游授权错误 surfacing。

不建模"流内授权失败":合规 MCP server 按 MCP 授权规范在 HTTP 层 POST 阶段返 401/403(已由 conformance 向量覆盖),SSE 流内不承载授权失败。故 (a) 流内 JSON-RPC error event 经正常错误路径回灌(双端库均正常处理,分类器覆盖即可,无需兜底)、(b) 流传输层错误(连接断 / 重连失败,无授权信号)不归授权语义(按 判定决策表 5xx/网络行 → 4003 或超时)均不单独建模为授权向量。

字段说明

字段 强度 说明
meta.error_code MUST 4006(未授权)或 4007(授权失效),按上方判定决策表映射
meta.mcp_server MUST 触发授权错误的 MCP Server 的 bundle_id(与 get_config 归属一致,Agent 可 correlate 到具体 server)
meta.auth_hint SHOULD 面向用户的非敏感提示对象。Computer SHOULD 提供以协助 Agent/Host 引导用户;缺失时 Agent 仍能基于 error_code 做兜底处理
meta.auth_hint.action MAY 机器可读动作标识(如 user_authorization_required / token_refresh_required),便于 Host 路由到不同 UI
meta.auth_hint.message SHOULD 用户可读的一句话引导(如"请在 Computer 宿主环境完成 GitHub OAuth 登录后重试");强烈建议提供以满足最小可用 UX

auth_hint 安全边界

auth_hint 是协议层唯一面向 Agent 暴露的授权相关字段。为防止凭证经此通道泄漏给 Agent,Computer MUST NOT 在 auth_hint(任何子字段)中包含以下任何一类内容:

类别 禁止字段示例
访问凭证 access_token / refresh_token / id_token / bearer_token / 任何形式的 Token
OAuth 流程参数 code / code_verifier / code_challenge / state / nonce
客户端凭证 client_id / client_secret / assertion / client_assertion
完整授权 URL 任何包含上述 query 参数的完整 URL(即使已经 url-encode 也禁止)
用户敏感数据 用户密码、TOTP/OTP、安全问题答案

允许包含:

  • 自然语言描述("需要登录 GitHub 后重试")
  • MCP Server 名称、工具名称
  • 不含敏感参数的着陆页 URL(如 https://example.com/login,无 query)

完整凭证传播禁令见 security.md → 零凭证传播原则。auth_hint 是该原则下的唯一豁免——豁免范围严格限于上表"允许包含"部分。

MCP Server Not Found(4014)

触发时机:客户端事件(client:get_resources / client:tool_call 等)引用的 mcp_server(bundle_id)在 Computer 上未注册。

响应结构(Socket.IO ack 数据):

{
  "code": 4014,
  "message": "MCP Server not registered",
  "mcp_server": "office_docs"
}

字段说明:

字段 必需 说明
code 是 固定 4014
message 是 人类可读
mcp_server 是 客户端引用的 server 的 bundle_id

Agent 行为建议:刷新 server list(调用 client:get_config)后重试;持续不存在则提示用户 server 已下线。

MCP Capability Not Supported(4015)

触发时机:MCP Server 已注册,但未声明所请求事件依赖的 capability。例如 client:get_resources 调用时该 server 未声明 resources capability。

响应结构(Socket.IO ack 数据):

{
  "code": 4015,
  "message": "MCP Server does not support 'resources' capability",
  "mcp_server": "office_docs",
  "capability": "resources"
}

字段说明:

字段 必需 说明
code 是 固定 4015
message 是 人类可读
mcp_server 是 目标 server 的 bundle_id
capability 是 缺失的 capability 名(resources / tools / prompts 等 MCP 标准 capability)

Agent 行为建议:跳过此 server,不再向其发送同类事件;可在 server list UI 中标注能力缺失。

Invalid Skill Name(4016)

触发时机:client:get_skill 入参 name 不符合 SKILL name lexer 规则(skill.md §1.4)——段数 ∉ {1, 2, 3}、3 段但首段 ≠ mcp、或某段字符集非法(leaf 段须符合 marketplace SKILL v1 §3.1)。注意 user 源裸名为合法 1 段、无 :,不因「缺 :」判错。属参数校验类硬错:Computer 在进入 Skill Registry 查询路径之前即拒绝(防止从 name 推导 FS 路径越权)。

响应结构(Socket.IO ack 数据):

{
  "code": 4016,
  "message": "Invalid skill name format",
  "details": { "name": "bad name!!" }
}

字段说明:

字段 必需 说明
code 是 固定 4016
message 是 人类可读
details.name 否 被拒的原始 name,仅供诊断(Agent MUST NOT 透传给最终用户)

与 4014 的边界:name 格式合法但不存在(未注册 / 已卸载 / 孤儿)→ 复用 4014;name 格式非法 → 4016。两者互斥:Computer 先做 lexer 校验(不过则 4016),通过后再查 Registry(未命中则 4014)。

Agent 行为建议:4016 属客户端构造错误,不应重试同一 name;应回溯 client:get_skills 返回的 A2CSkillRef.name 原样使用,不要自行拼接 / 改写。

Skill Resource Not Accessible(4017)

触发时机:client:get_skill 的 name 合法且命中 Registry,但 rel_path 无法安全服务。Computer 在 Registry 解析出包根后、读取文件前完成校验(包根绝对路径来源唯一是 Registry,禁止从 name / rel_path 推导)。

判定:rel_path 缺省 SKILL.md;safe_join(包根, rel_path) 后 realpath 必须仍落在包根内。下列任一 → 4017:

details.reason 触发
traversal rel_path 为绝对路径、含 ..、或符号链接逃逸出包根
forbidden 命中敏感文件(.skillenv 及 Computer 判定的凭证类文件)——无论是否存在一律按 forbidden,MUST NOT 泄漏存在性
not_found 路径合法且在包根内,但目标文件不存在
too_large 资源总字节数超过 SDK 可配的绝对上限——首块前即拒,零字节传输,带 details.total_size

响应结构(Socket.IO ack 数据):

{
  "code": 4017,
  "message": "Skill resource not accessible",
  "details": { "reason": "traversal", "rel_path": "../../etc/passwd" }
}

字段说明:

字段 必需 说明
code 是 固定 4017
message 是 人类可读
details.reason 是 traversal / forbidden / not_found / too_large(开放枚举,未来可非破坏新增)
details.rel_path 否 被拒的原始 rel_path,仅供诊断(Agent MUST NOT 透传给最终用户)
details.total_size 否 仅 too_large:资源总字节数,供 Agent 告知用户/降级决策

安全不变量:forbidden 对"文件不存在"与"文件存在但敏感"返回同一 reason,不暴露敏感文件存在性。.skillenv 在任何 rel_path 下都不可经此通道读出——这是 SKILL 通道 §9 安全模型 的硬约束,不是可选项。

Agent 行为建议:traversal / forbidden 属客户端构造错误,不重试;not_found 说明 SKILL.md 披露的路径与实际包内容不一致,可回退到仅用 SKILL.md body;too_large 不重试,依 details.total_size 向用户说明或改用其它策略(该资源不经此通道传输)。rel_path 应严格取自 SKILL.md 正文披露的引用,不自行拼接。

成功路径:解析通过且未超上限时,文本可内联走 body;二进制 / 过大文本则 get_skill 铸造 blob_handle,字节经 client:get_blob 拉取(拉取期错误见 4018)。

Blob Not Accessible(4018)

触发时机:client:get_blob 拉取阶段——blob_handle 无法解析回一个当前仍被铸造通道授权的可读源。资源的鉴权与绝对上限已在铸造期由生产者通道决断(不通过则根本没有句柄),故 4018 只承载拉取期失败。

判定:

details.reason 触发
invalid_handle blob_handle 格式非法 / 非本 Computer 铸造 / 无法识别
forbidden 句柄可解析,但重施铸造通道鉴权失败(如 SKILL 现已 orphan、.skillenv 等仍禁止)——防御纵深,句柄内容绝不被直接信任
gone 源已不可达:SKILL 卸载 / 文件删除 / 内容已无法服务
range chunk_offset < 0 或 > total_size

响应结构(Socket.IO ack 数据):

{
  "code": 4018,
  "message": "Blob not accessible",
  "details": { "reason": "gone" }
}

字段说明:

字段 必需 说明
code 是 固定 4018
message 是 人类可读
details.reason 是 invalid_handle / forbidden / gone / range(开放枚举,未来可非破坏新增)

安全不变量:client:get_blob 不是任意文件读原语。Computer 每次解析句柄 MUST 重跑铸造通道的边界校验(SKILL → §9 沙箱);句柄即便编码了路径也绝不被直接信任。.skillenv 等敏感文件因在铸造期已被 4017 forbidden 拦截,永不会有指向它的句柄。

Agent 行为建议:invalid_handle / forbidden 不重试;gone 回到生产者通道重新获取(如重新 client:get_skill 取新 blob_handle);range 修正 chunk_offset 后重试。跨块若 sha256 / total_size 变化,从 offset 0 重读。

Blob Write Failed(4019)

触发时机:client:put_blob 写入阶段——上传会话不被接纳 / 声明非法 / 分块顺序错乱 / 超出上限 / 沙箱不可写 / 完整性不符 / 落盘 IO 失败。绝对上限(total_size 声明)在首块由 Computer 决断(超限零字节落盘),其余失败发生在会话存续或末块定稿时。

判定:

details.reason 触发
invalid_upload upload_id 格式非法 / 非本 Computer 分配 / 已过期(会话闲置超时 / 孤儿回收后)
invalid_declaration 首块声明非法:total_size < 1、字段缺失、声明字段在后续块重复携带
range chunk_offset != 已收字节(in-order 违反,无稀疏缓冲);或末块 chunk_offset + 本块字节数 != total_size
too_large 首块声明 total_size 超 Computer 可配上限 → 拒绝且零字节落盘
busy 并发上传会话数已达 Computer 可配上限——Agent SHOULD 退避后从 0 重传(新 upload_id)
forbidden landing root 未配置 / 不可写(fail-closed,零字节落盘)
integrity 末块定稿时重算 sha256 与首块声明不符 → 丢弃 .part,不返回 landing_path
io_error 落盘 IO 失败(磁盘满 / 权限 / 文件系统错误)

响应结构(Socket.IO ack 数据):

{
  "code": 4019,
  "message": "Blob write failed",
  "details": { "reason": "invalid_upload", "upload_id": "..." }
}

字段说明:

字段 必需 说明
code 是 固定 4019
message 是 人类可读
details.reason 是 invalid_upload / invalid_declaration / range / too_large / busy / forbidden / integrity / io_error(开放枚举,未来可非破坏新增)
details.upload_id 否 出错的 upload_id(若有)
details.chunk_offset 否 出错的块偏移(range 时)
details.total_size 否 超限时的声明值 / Computer 上限(too_large 时)

复用与不使用:上行写入期一律 4019,不使用 4018(下行拉取期专属,方向相反)、不使用 4017(SKILL 铸造期专属)、不使用 MCP CallToolResult.isError(本事件为 A2C 自有事件,非 MCP 事件)。

安全不变量:client:put_blob 不是任意文件写原语——落点由 Computer 决断(landing root,config-first),name_hint 穿越组件 MUST 被消毒剥离或忽略(自定安全名),落点构造上严格落于 root 内;landingRoot 仅受信 scope(user / local / flag / policy / embed)可设,project scope 提供 MUST 被拒绝(防 clone 仓库重定向写目标)。integrity / too_large / forbidden 路径下零产物:无 .part 残留、无部分文件可见。GC 严格限于 landing root(computer-management §7 不变量 #5)。

Agent 行为建议:invalid_upload → 新 upload_id 从 0 重传;invalid_declaration / range 属客户端构造错误,检查声明与偏移后重传(新会话);too_large → 不重试,改用分治 / 压缩 / 留上下文策略;busy → 退避等待后重试;forbidden / io_error → 不重试,字节留上下文(Computer 侧状态问题,无法靠重传解决);integrity → 检查本地字节(本地损坏 / 传输损坏)后从 0 重传。

版本错配叙述:4019 是 0.4.0 新增码。旧 SDK 收到 4019 会按各自「未知错误码」策略处理(具体行为由各 SDK 定义)——加性错误码的版本错配随版本推进自然消解,非阻塞项。

TODO

以下功能尚未实现,计划在后续版本中完善:

参考

  • MCP CallToolResult: https://github.com/modelcontextprotocol/specification
  • Socket.IO 错误处理: https://socket.io/docs/v4/handling-errors/