Skip to main content

smcp/
lib.rs

1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4/// 基础设施工具模块 / Infrastructure utilities(原子 IO / 环境变量 / 路径;治理层 SET/SKL 共用)。
5pub mod utils;
6
7/// SKILL name 解析与合成(段数消歧 lexer)/ SKILL name parse & synthesis (segment-count lexer)。
8pub mod skill_name;
9pub use skill_name::{
10    is_valid_skill_name, normalize_mcp_server_segment, parse_skill_name,
11    synthesize_marketplace_name, synthesize_mcp_name, synthesize_user_name, ParsedSkillName,
12    SkillNameError, SkillNameKind,
13};
14
15/// 协议版本解析与兼容性判定 / Protocol version parse & compatibility。
16pub mod version;
17pub use version::{
18    is_compatible, ProtocolVersion, ProtocolVersionError, ProtocolVersionParseError,
19};
20
21/// SMCP协议的命名空间
22pub const SMCP_NAMESPACE: &str = "/smcp";
23
24/// A2C-SMCP 协议版本号 / A2C-SMCP protocol version
25///
26/// 锁定为 `MAJOR.MINOR` = `0.2.0`。SKILL(v0.2.1)与通用二进制传输(v0.2.1)等均为**加性升级**,
27/// 不改变 `MAJOR.MINOR`,因此该常量保持 `"0.2.0"`,用于 HTTP 握手阶段的版本协商。
28///
29/// Locked to `MAJOR.MINOR` = `0.2.0`. SKILL (v0.2.1) and generic binary transfer (v0.2.1) are
30/// **additive** upgrades that do not bump `MAJOR.MINOR`; this constant stays `"0.2.0"` and is used
31/// for version negotiation during the HTTP handshake.
32///
33/// 协议依据 / Protocol: `a2c-smcp-protocol` versioning.md。
34/// Python 参考 / Python reference: `a2c_smcp/smcp.py`。
35pub const PROTOCOL_VERSION: &str = "0.2.0";
36
37/// 标准错误码模块 / Standard error codes module
38///
39/// ⚠️ 与 [`ErrorCode`] 枚举是**两套有意不合并的命名空间**(对齐 Python `a2c_smcp/smcp.py`,
40/// 其 `ErrorCode` 同样不含 4001–4005 / 4101–4104,合并会偏离参考实现):
41/// - 本模块 = **传输/管理层码** + 工具/房间码(400–500、4001–4005、4101–4104)。
42/// - [`ErrorCode`] = **协议级闭集**(404、4006–4018),是 [`is_protocol_error_payload`] 识别的集合,
43///   也是 `client:*` ack 协议级错误必用的码。
44/// - 两者仅 `404` 重合。
45pub mod error_codes {
46    // 通用错误码 / General error codes
47    pub const BAD_REQUEST: i32 = 400;
48    pub const UNAUTHORIZED: i32 = 401;
49    pub const FORBIDDEN: i32 = 403;
50    pub const NOT_FOUND: i32 = 404;
51    pub const TIMEOUT: i32 = 408;
52    pub const INTERNAL_ERROR: i32 = 500;
53
54    // 工具调用错误码 / Tool call error codes
55
56    /// 工具调用**前**查找失败:目标工具在任一活动 MCP Server 上都不存在(路由 / 注册阶段)。
57    /// 仅用于「调用前」的工具解析失败;工具**执行**阶段的失败必须使用
58    /// [`TOOL_EXECUTION_FAILED`](4003),二者语义严格区分。
59    ///
60    /// Tool lookup failed **before** invocation: the target tool is absent on every active MCP
61    /// server (routing / registry stage). Use only for pre-call resolution failures; failures
62    /// during tool **execution** MUST use [`TOOL_EXECUTION_FAILED`] (4003).
63    pub const TOOL_NOT_FOUND: i32 = 4001;
64    pub const TOOL_DISABLED: i32 = 4002;
65    /// 工具**执行**失败:工具已成功解析并被调用,但在执行过程中返回错误或抛出异常。
66    /// 与查找阶段失败的 [`TOOL_NOT_FOUND`](4001)严格区分。
67    ///
68    /// Tool **execution** failed: the tool was resolved and invoked, but returned an error or
69    /// raised during execution. Strictly distinct from the lookup-stage [`TOOL_NOT_FOUND`] (4001).
70    pub const TOOL_EXECUTION_FAILED: i32 = 4003;
71    pub const TOOL_TIMEOUT: i32 = 4004;
72    pub const TOOL_REQUIRES_CONFIRMATION: i32 = 4005;
73
74    // 房间管理错误码 / Room management error codes
75    pub const ROOM_FULL: i32 = 4101;
76    pub const ROOM_NOT_FOUND: i32 = 4102;
77    pub const NOT_IN_ROOM: i32 = 4103;
78    pub const CROSS_ROOM_ACCESS: i32 = 4104;
79}
80
81/// WebSocket 握手版本拒绝的 close code(RFC 6455 私有段 4000–4999)。
82///
83/// 用于 **WS-only 直连握手**在协议版本不匹配时的拒绝,是服务端运行栈不支持
84/// ASGI WebSocket Denial Response 时的回退形态。`4900` 不携带结构化 body
85/// (WS close reason ≤123 字节)。
86///
87/// ⚠️ **MUST NOT** 与 [`ErrorCode::ProtocolVersionMismatch`](`4008`)混用或互转:
88/// - `4008` 是 [`ErrorCode`] 值,作为 `ErrorPayload.code` 承载于 **HTTP 400 body**;
89/// - `4900` 是 **WS close code**,不承载结构化 body。
90///
91/// 二者是不同命名空间、有意取不同数值。协议依据 / Protocol: versioning.md。
92///
93/// WebSocket close code (RFC 6455 private range 4000–4999) for rejecting a WS-only direct
94/// handshake on protocol version mismatch (fallback when the server stack lacks ASGI WebSocket
95/// Denial Response). MUST NOT be conflated/converted with [`ErrorCode::ProtocolVersionMismatch`]
96/// (`4008`): 4008 is an `ErrorPayload.code` carried in the HTTP 400 body; 4900 is a WS close code
97/// with no structured body. Different namespaces, intentionally different values.
98pub const WS_VERSION_HANDSHAKE_REJECTED_CLOSE_CODE: i32 = 4900;
99
100/// A2C-SMCP 协议错误码(v0.2.0 起;v0.2.1 加性追加 `4016` / `4017` / `4018`)。
101///
102/// 镜像 Python 参考实现 `a2c_smcp/smcp.py::ErrorCode`,**序列化 / 反序列化均为整数**
103/// (与协议 `ErrorPayload.code` 的线格式一致)。协议依据 / Protocol: error-handling.md。
104///
105/// ⚠️ 与 [`error_codes`] 模块区分:本枚举是**协议级闭集**([`is_protocol_error_payload`] 识别 +
106/// `client:*` ack 必用);[`error_codes`] 是传输/管理层与工具/房间码。两者仅 `404` 重合,有意不合并。
107///
108/// 语义要点 / Semantics:
109/// - [`ErrorCode::NotFound`](`404`):通用「资源不存在」。本 SDK 用于 `client:*` 路由层
110///   目标 Computer 名未命中(error-handling.md 明确「Computer 不存在」归 404);镜像协议已有定义,非新增。
111/// - [`ErrorCode::McpServerNotFound`](`4014`):v0.2.1 复用——SKILL `name` **格式合法但不存在**
112///   (未注册 / 已卸载 / 孤儿)复用此码;`name` 格式非法 → [`ErrorCode::SkillNameInvalid`](`4016`);
113///   `name` 有效但 `rel_path` 不可达 → [`ErrorCode::SkillResourceNotAccessible`](`4017`)。
114/// - SKILL 通道**不使用** `4015`:未声明 `resources` capability 的 server 在物化阶段即被排除,不上送 Agent。
115/// - `4017` / `4018` 的 `details.reason` 为**开放枚举**:解析方 MUST 容忍未知值并兜底
116///   (默认「不重试 + 诊断」),未来可非破坏地新增 reason。
117///
118/// Mirrors the Python reference `ErrorCode`; serializes to / from an **integer** matching the
119/// protocol `ErrorPayload.code` wire format.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
121#[repr(i32)]
122pub enum ErrorCode {
123    /// 通用「资源不存在」/ Generic "resource not found"(含 `client:*` 路由层 Computer 名未命中)。
124    NotFound = 404,
125    /// MCP 上游要求授权 / MCP upstream requires authorization。
126    ToolAuthorizationRequired = 4006,
127    /// MCP 上游授权失败 / MCP upstream authorization failed。
128    ToolAuthorizationFailed = 4007,
129    /// 协议版本不匹配(承载于 HTTP 400 body)/ Protocol version mismatch (carried in HTTP 400 body)。
130    ///
131    /// ⚠️ MUST NOT 与 WS close code [`WS_VERSION_HANDSHAKE_REJECTED_CLOSE_CODE`](`4900`)混用。
132    ProtocolVersionMismatch = 4008,
133    /// MCP Server 路由未命中 / MCP server not found(v0.2.1 复用于 SKILL name 合法但 Registry 未命中)。
134    McpServerNotFound = 4014,
135    /// MCP 能力不支持 / MCP capability not supported。
136    McpCapabilityNotSupported = 4015,
137    /// SKILL `name` 违反 lexer 规则(格式硬错)/ SKILL name violates lexer rules (hard format error)。
138    SkillNameInvalid = 4016,
139    /// SKILL 资源不可达 / SKILL resource not accessible(rel_path 穿越 / .skillenv forbidden / not_found / too_large)。
140    SkillResourceNotAccessible = 4017,
141    /// 二进制 blob 不可达 / Binary blob not accessible(invalid_handle / forbidden / gone / range)。
142    BlobNotAccessible = 4018,
143}
144
145impl ErrorCode {
146    /// 返回错误码的整数值 / Return the integer code value.
147    pub const fn code(self) -> i32 {
148        self as i32
149    }
150
151    /// 从整数值解析错误码;未知值返回 `None` / Parse from an integer; unknown values return `None`.
152    pub fn from_code(code: i32) -> Option<Self> {
153        match code {
154            404 => Some(Self::NotFound),
155            4006 => Some(Self::ToolAuthorizationRequired),
156            4007 => Some(Self::ToolAuthorizationFailed),
157            4008 => Some(Self::ProtocolVersionMismatch),
158            4014 => Some(Self::McpServerNotFound),
159            4015 => Some(Self::McpCapabilityNotSupported),
160            4016 => Some(Self::SkillNameInvalid),
161            4017 => Some(Self::SkillResourceNotAccessible),
162            4018 => Some(Self::BlobNotAccessible),
163            _ => None,
164        }
165    }
166}
167
168impl Serialize for ErrorCode {
169    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
170    where
171        S: serde::Serializer,
172    {
173        serializer.serialize_i32(self.code())
174    }
175}
176
177impl<'de> Deserialize<'de> for ErrorCode {
178    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
179    where
180        D: serde::Deserializer<'de>,
181    {
182        let code = i32::deserialize(deserializer)?;
183        Self::from_code(code)
184            .ok_or_else(|| serde::de::Error::custom(format!("unknown A2C-SMCP error code: {code}")))
185    }
186}
187
188/// A2C-SMCP flat 错误负载 / A2C-SMCP flat error payload
189///
190/// 协议 0.2.2 统一错误形态:顶层 `code`/`message`,诊断信息置于可选的 `details` 容器。
191/// **禁止**嵌套 `{"error": {...}}` envelope —— 所有 `client:*` ack 路由的协议级错误 MUST 为本结构。
192/// 线格式 / Wire shape: `{ "code": <int>, "message": <str>, "details"?: <object> }`。
193///
194/// `details` 是诊断容器,Agent **MUST NOT** 原样透传给最终用户(防信息泄露)。
195/// `details` is a diagnostic container; the Agent **MUST NOT** propagate it verbatim to end users.
196///
197/// 协议依据 / Protocol: `a2c-smcp-protocol` error-handling.md(flat ErrorPayload,禁止二次 unwrap)。
198/// Python 参考 / Python reference: `a2c_smcp/smcp.py` 的 `ErrorPayload`。
199///
200/// 协议 0.2.x 对特定码在**顶层平铺**分流字段(对齐 Python `smcp.py:484` `ErrorPayload`
201/// `total=False` TypedDict):
202/// - 4008(协议版本不兼容)→ `server_version` / `client_version` / `min_supported` /
203///   `max_supported`(HS-01 #21 / HS-02 #22;构造见 [`ErrorPayload::version_mismatch`])。
204/// - 4014(MCP Server 未命中)→ `mcp_server_name`;4015(能力不支持)→ `mcp_server_name` /
205///   `capability`(SRV-01 #47 / AUTH-01 #23;构造见 [`ErrorPayload::with_mcp_server_name`] /
206///   [`ErrorPayload::with_capability`])。
207/// - 4016 / 4017 / 4018 的 code-specific 字段下沉到 `details` 子对象(无顶层平铺新字段)。
208///
209/// 未知顶层字段由 [`ErrorPayload::extra`](`#[serde(flatten)]`)**捕获并保留**,跨-SDK 往返
210/// 不静默丢字段——镜像 Python `total=False` TypedDict(运行时即 `dict`)的开放语义,避免旧
211/// Rust SDK 在协议非破坏性新增顶层字段后把它们悄悄抹掉。
212#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
213pub struct ErrorPayload {
214    /// 错误码(协议 `ErrorCode` 取值;线格式为裸整数)/ Error code (a protocol `ErrorCode` value; bare int on the wire)
215    pub code: i64,
216    /// 人类可读的错误描述 / Human-readable error message
217    pub message: String,
218    /// 诊断容器(可选;为空时不序列化)/ Diagnostic container (optional; skipped when absent)
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub details: Option<serde_json::Value>,
221    /// 4008 顶层分流:服务端协议版本 / top-level for 4008: server protocol version。
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub server_version: Option<String>,
224    /// 4008 顶层分流:客户端协议版本 / top-level for 4008: client protocol version。
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub client_version: Option<String>,
227    /// 4008 顶层分流:服务端支持的最小版本 / top-level for 4008: min supported。
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub min_supported: Option<String>,
230    /// 4008 顶层分流:服务端支持的最大版本 / top-level for 4008: max supported。
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub max_supported: Option<String>,
233    /// 4014 / 4015 顶层分流:未命中 / 缺能力的 MCP Server 名 / top-level for 4014&4015: MCP server name。
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub mcp_server_name: Option<String>,
236    /// 4015 顶层分流:缺失的 capability 名(如 `"resources"`)/ top-level for 4015: missing capability。
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub capability: Option<String>,
239    /// 未知顶层字段兜底容器:捕获本结构未显式建模的顶层键,跨-SDK 往返不丢字段(见结构体文档)。
240    /// 为空时不产出任何额外键(空 map flatten 后无输出),故不影响既有 byte 兼容契约。
241    /// Catch-all for unmodeled top-level keys so cross-SDK round-trips never silently drop fields.
242    #[serde(flatten)]
243    pub extra: serde_json::Map<String, serde_json::Value>,
244}
245
246impl ErrorPayload {
247    /// 创建 flat 错误负载 / Create a flat error payload
248    pub fn new(code: i64, message: impl Into<String>) -> Self {
249        Self {
250            code,
251            message: message.into(),
252            details: None,
253            server_version: None,
254            client_version: None,
255            min_supported: None,
256            max_supported: None,
257            mcp_server_name: None,
258            capability: None,
259            extra: serde_json::Map::new(),
260        }
261    }
262
263    /// 由协议 [`ErrorCode`] 构造 flat 错误负载,消除调用点手工 `i64::from(ErrorCode::X.code())` 样板。
264    ///
265    /// 产出的 `code` 必属 [`is_protocol_error_payload`] 识别的协议级闭集(编译期由 [`ErrorCode`] 保证),
266    /// 杜绝误用传输/管理层整数字面量(400 / 401 / 500 / 4101…)落入 `client:*` ack 致 Agent 端
267    /// [`is_protocol_error_payload`] 误判为「非协议错误」。
268    ///
269    /// Build a flat payload from a protocol [`ErrorCode`], removing manual
270    /// `i64::from(ErrorCode::X.code())` boilerplate and guaranteeing the wire `code` is always a
271    /// protocol-level value recognized by [`is_protocol_error_payload`].
272    pub fn from_error_code(code: ErrorCode, message: impl Into<String>) -> Self {
273        Self::new(i64::from(code.code()), message)
274    }
275
276    /// 设置整个 `details` 诊断容器 / Set the whole `details` diagnostic container
277    pub fn with_details(mut self, details: serde_json::Value) -> Self {
278        self.details = Some(details);
279        self
280    }
281
282    /// 向 `details` 对象插入单个字段(若 `details` 非对象则重置为对象)
283    /// Insert a single field into the `details` object (reset to an object if it is not one)
284    pub fn with_detail(
285        mut self,
286        key: impl Into<String>,
287        value: impl Into<serde_json::Value>,
288    ) -> Self {
289        let mut map = match self.details {
290            Some(serde_json::Value::Object(map)) => map,
291            _ => serde_json::Map::new(),
292        };
293        map.insert(key.into(), value.into());
294        self.details = Some(serde_json::Value::Object(map));
295        self
296    }
297
298    /// 顶层平铺 `mcp_server_name`(4014 / 4015 分流字段)/ Set top-level `mcp_server_name` (4014/4015).
299    pub fn with_mcp_server_name(mut self, name: impl Into<String>) -> Self {
300        self.mcp_server_name = Some(name.into());
301        self
302    }
303
304    /// 顶层平铺 `capability`(4015 缺失能力名,如 `"resources"`)/ Set top-level `capability` (4015).
305    pub fn with_capability(mut self, capability: impl Into<String>) -> Self {
306        self.capability = Some(capability.into());
307        self
308    }
309
310    /// 构造 4008(协议版本不兼容)flat ErrorPayload,顶层平铺 4 个版本分流字段。
311    ///
312    /// `min_supported` / `max_supported` 由 server 版本派生:同 `MAJOR.MINOR` 的整个 PATCH 段
313    /// (`{maj}.{min}.0` ~ `{maj}.{min}.999`)。`message` 固定为 `"Protocol version mismatch"`。
314    /// 与 Python middleware `_mismatch_body` 逐字段对齐。供 HS-01 (#21) 服务端握手中间件复用。
315    pub fn version_mismatch(client: &ProtocolVersion, server: &ProtocolVersion) -> Self {
316        Self {
317            code: i64::from(ErrorCode::ProtocolVersionMismatch.code()),
318            message: "Protocol version mismatch".to_string(),
319            details: None,
320            server_version: Some(server.to_string()),
321            client_version: Some(client.to_string()),
322            min_supported: Some(format!("{}.{}.0", server.major, server.minor)),
323            max_supported: Some(format!("{}.{}.999", server.major, server.minor)),
324            mcp_server_name: None,
325            capability: None,
326            extra: serde_json::Map::new(),
327        }
328    }
329}
330
331/// 判定 `value` 是否为协议级 **flat ErrorPayload**:顶层 `code` 属协议错误码、且无嵌套 envelope。
332///
333/// - flat shape(顶层 `code` 为 [`ErrorCode`] 取值)→ `true`
334/// - 嵌套 `{"error": {...}}`、未知码值、缺 `code`、`code` 非整数、或非对象 → `false`
335///
336/// server(透传判定)与 agent(抛协议错误)共用同一谓词,避免双重启发式漂移。
337/// Shared by the server (passthrough decision) and the agent (raise on protocol error) so the two
338/// never drift apart heuristically.
339///
340/// 对标 Python `a2c_smcp/smcp.py` 的 `is_protocol_error_payload`。
341/// 协议依据 / Protocol: error-handling.md(禁止对 ack 负载二次 unwrap)。
342pub fn is_protocol_error_payload(value: &serde_json::Value) -> bool {
343    value
344        .as_object()
345        .and_then(|obj| obj.get("code"))
346        .and_then(serde_json::Value::as_i64)
347        .and_then(|code| i32::try_from(code).ok())
348        .is_some_and(|code| ErrorCode::from_code(code).is_some())
349}
350
351/// 构造 `client:*` 路由层目标 Computer 名未注册时返回的 flat ErrorPayload(404)。
352///
353/// 与 Python 实现返回**逐字节一致**的负载(双实现镜像约束):
354/// `{ "code": 404, "message": "Computer with name '<name>' not found", "details": { "computer_name": "<name>" } }`。
355///
356/// 对标 Python `a2c_smcp/smcp.py` 的 `build_computer_not_found_error`。
357/// 协议依据 / Protocol: error-handling.md §404(工具或 Computer 不存在);所有 `client:*` ack 协议级错误 MUST 为 flat ErrorPayload。
358pub fn build_computer_not_found_error(computer_name: &str) -> ErrorPayload {
359    ErrorPayload::new(
360        i64::from(ErrorCode::NotFound.code()),
361        format!("Computer with name '{computer_name}' not found"),
362    )
363    .with_detail("computer_name", computer_name)
364}
365
366/// SMCP事件常量定义
367pub mod events {
368    /// 客户端请求获取工具列表
369    pub const CLIENT_GET_TOOLS: &str = "client:get_tools";
370    /// 客户端请求获取配置
371    pub const CLIENT_GET_CONFIG: &str = "client:get_config";
372    /// 客户端请求获取桌面信息
373    pub const CLIENT_GET_DESKTOP: &str = "client:get_desktop";
374    /// 客户端工具调用请求
375    pub const CLIENT_TOOL_CALL: &str = "client:tool_call";
376    /// 客户端资源发现请求(v0.2.0):透明转发 MCP `resources/list` / Resource discovery (v0.2.0).
377    pub const CLIENT_GET_RESOURCES: &str = "client:get_resources";
378    /// 客户端 SKILL 清单发现请求(v0.2.1)/ SKILL inventory discovery (v0.2.1)。
379    pub const CLIENT_GET_SKILLS: &str = "client:get_skills";
380    /// 客户端 SKILL 包内单资源拉取请求(v0.2.1)/ Single in-package SKILL resource pull (v0.2.1)。
381    pub const CLIENT_GET_SKILL: &str = "client:get_skill";
382    /// 客户端通用二进制拉取请求(v0.2.1)/ Generic binary pull (v0.2.1)。
383    pub const CLIENT_GET_BLOB: &str = "client:get_blob";
384
385    /// 服务器加入办公室请求
386    pub const SERVER_JOIN_OFFICE: &str = "server:join_office";
387    /// 服务器离开办公室请求
388    pub const SERVER_LEAVE_OFFICE: &str = "server:leave_office";
389    /// 服务器更新配置请求
390    pub const SERVER_UPDATE_CONFIG: &str = "server:update_config";
391    /// 服务器更新工具列表请求
392    pub const SERVER_UPDATE_TOOL_LIST: &str = "server:update_tool_list";
393    /// 服务器更新桌面请求
394    pub const SERVER_UPDATE_DESKTOP: &str = "server:update_desktop";
395    /// 服务器 SKILL 集合变化请求(v0.2.1):Computer 触发,Server 广播 `notify:update_skills`。
396    pub const SERVER_UPDATE_SKILLS: &str = "server:update_skills";
397    /// 服务器取消工具调用请求
398    pub const SERVER_TOOL_CALL_CANCEL: &str = "server:tool_call_cancel";
399    /// 服务器列出房间请求
400    pub const SERVER_LIST_ROOM: &str = "server:list_room";
401
402    /// 通知取消工具调用
403    pub const NOTIFY_TOOL_CALL_CANCEL: &str = "notify:tool_call_cancel";
404    /// 通知进入办公室
405    pub const NOTIFY_ENTER_OFFICE: &str = "notify:enter_office";
406    /// 通知离开办公室
407    pub const NOTIFY_LEAVE_OFFICE: &str = "notify:leave_office";
408    /// 通知更新配置
409    pub const NOTIFY_UPDATE_CONFIG: &str = "notify:update_config";
410    /// 通知更新工具列表
411    pub const NOTIFY_UPDATE_TOOL_LIST: &str = "notify:update_tool_list";
412    /// 通知更新桌面
413    pub const NOTIFY_UPDATE_DESKTOP: &str = "notify:update_desktop";
414    /// 通知 SKILL 集合变化(v0.2.1):Agent 据此自动重拉 `client:get_skills`。
415    pub const NOTIFY_UPDATE_SKILLS: &str = "notify:update_skills";
416
417    /// 通用通知前缀
418    pub const NOTIFY_PREFIX: &str = "notify:";
419}
420
421/// 请求ID,使用UUID确保全局唯一性
422#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
423pub struct ReqId(pub String);
424
425impl ReqId {
426    /// 生成新的请求ID(使用hex格式以匹配Python的uuid.uuid4().hex)
427    pub fn new() -> Self {
428        Self(Uuid::new_v4().simple().to_string())
429    }
430
431    /// 从字符串创建请求ID
432    pub fn from_string(s: String) -> Self {
433        Self(s)
434    }
435
436    /// 获取请求ID的字符串引用
437    pub fn as_str(&self) -> &str {
438        &self.0
439    }
440}
441
442impl Default for ReqId {
443    fn default() -> Self {
444        Self::new()
445    }
446}
447
448/// 角色类型
449#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
450#[serde(rename_all = "lowercase")]
451pub enum Role {
452    Agent,
453    Computer,
454}
455
456impl std::fmt::Display for Role {
457    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
458        match self {
459            Role::Agent => write!(f, "agent"),
460            Role::Computer => write!(f, "computer"),
461        }
462    }
463}
464
465/// 用户信息
466#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct UserInfo {
468    pub name: String,
469    pub role: Role,
470}
471
472/// 工具调用请求
473#[derive(Debug, Clone, Serialize, Deserialize)]
474pub struct ToolCallReq {
475    #[serde(flatten)]
476    pub base: AgentCallData,
477    pub computer: String,
478    pub tool_name: String,
479    pub params: serde_json::Value,
480    pub timeout: i32,
481}
482
483/// 获取计算机配置请求
484#[derive(Debug, Clone, Serialize, Deserialize)]
485pub struct GetComputerConfigReq {
486    #[serde(flatten)]
487    pub base: AgentCallData,
488    pub computer: String,
489}
490
491/// 更新计算机配置请求
492#[derive(Debug, Clone, Serialize, Deserialize)]
493pub struct UpdateComputerConfigReq {
494    pub computer: String,
495}
496
497/// 获取计算机配置返回
498#[derive(Debug, Clone, Serialize, Deserialize)]
499pub struct GetComputerConfigRet {
500    #[serde(skip_serializing_if = "Option::is_none")]
501    pub inputs: Option<Vec<serde_json::Value>>,
502    pub servers: serde_json::Value,
503}
504
505/// 工具调用返回(符合 MCP CallToolResult 标准)
506///
507/// `meta` 为**结果级**元数据(线上 key 为 `meta`),承载 0.2.2 的 `a2c_*` 取消/超时标记
508/// ([`tool_meta`] + [`ToolCallRet::mark_cancelled`] / [`ToolCallRet::mark_timeout`])。
509/// 二进制旁路句柄属**子级**标记,落在 content item 的 `_meta`(见 [`set_content_blob_sideband`]),
510/// **不**放结果级 `meta`——区分见 data-structures.md §结果级 meta vs 子级 _meta。
511#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct ToolCallRet {
513    #[serde(skip_serializing_if = "Option::is_none")]
514    pub content: Option<Vec<serde_json::Value>>,
515    #[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
516    pub is_error: Option<bool>,
517    #[serde(skip_serializing_if = "Option::is_none")]
518    pub req_id: Option<ReqId>,
519    /// 结果级 meta(承载 `a2c_cancelled` / `a2c_cancel_reason` / `a2c_timeout` 等)。
520    ///
521    /// 读写契约 / Read-write contract:标记落**结果级 `meta`**(A2C producer **MUST** 写此处,
522    /// data-structures.md §结果级 `meta` vs 子级 `_meta`)。consumer reader([`ToolCallRet::is_cancelled`]
523    /// 等)据此读 `meta`。协议另有 SHOULD——consumer 宜对结果级 `_meta` 线上 key 兜底宽松读取(优先 `meta`);
524    /// 🚧 本结构暂只认 `meta`(当前无 producer 写结果级 `_meta`,Python 连 consumer 侧都未实现,见 #42 跟进),
525    /// result-级 `_meta` 兜底**待补**,以免半实现 churn。
526    #[serde(skip_serializing_if = "Option::is_none")]
527    pub meta: Option<serde_json::Value>,
528}
529
530/// CallToolResult 的 A2C 标记键 / A2C marker keys for CallToolResult。
531///
532/// 落位规则(data-structures.md §结果级 meta vs 子级 _meta):
533/// - **结果级**(`CallToolResult.meta`):取消 / 超时标记 `a2c_cancelled` / `a2c_cancel_reason` / `a2c_timeout`。
534/// - **子级**(content item `_meta`):二进制旁路 `a2c_blob_handle` / `a2c_total_size` / `a2c_sha256`。
535///
536/// 对标 Python `a2c_smcp`(agent `_blob_sideband` / computer `socketio/client`)的同名键。
537pub mod tool_meta {
538    /// 结果级:取消标记(取消时 MUST = `true`)。
539    pub const A2C_CANCELLED_KEY: &str = "a2c_cancelled";
540    /// 结果级:取消原因(SHOULD,可选;当前恒为 [`A2C_DEFAULT_CANCEL_REASON`])。
541    pub const A2C_CANCEL_REASON_KEY: &str = "a2c_cancel_reason";
542    /// 结果级:超时标记(超时时 SHOULD = `true`)。
543    pub const A2C_TIMEOUT_KEY: &str = "a2c_timeout";
544    /// `a2c_cancel_reason` 的现阶段恒定取值 / The current fixed cancel reason。
545    pub const A2C_DEFAULT_CANCEL_REASON: &str = "agent_requested";
546    /// 子级(content item `_meta`):二进制旁路句柄。
547    pub const A2C_BLOB_HANDLE_KEY: &str = "a2c_blob_handle";
548    /// 子级:旁路资源总字节数。
549    pub const A2C_TOTAL_SIZE_KEY: &str = "a2c_total_size";
550    /// 子级:旁路资源全量 sha256(十六进制)。
551    pub const A2C_SHA256_KEY: &str = "a2c_sha256";
552
553    // ── MCP 上游授权错误(AUTH-01,error-handling.md §403)/ MCP upstream authorization error ──
554    // 协议字面键,**非** a2c 前缀;结果级 `CallToolResult.meta`(wire `_meta`)。授权失败的 tool_call
555    // 不走 flat ErrorPayload,而内嵌 CallToolResult(isError=true) + 下列三键,使 Agent 区分「工具坏了」
556    // 与「需授权」。Protocol-literal keys (NOT a2c-prefixed) for the upstream-auth CallToolResult.meta.
557
558    /// 结果级(MUST):授权错误码 `4006`/`4007`(整数,[`ErrorCode::ToolAuthorizationRequired`] /
559    /// [`ErrorCode::ToolAuthorizationFailed`])。
560    pub const AUTH_ERROR_CODE_KEY: &str = "error_code";
561    /// 结果级(MUST):触发授权错误的 MCP Server 标识 / the MCP server that raised the auth error。
562    pub const AUTH_MCP_SERVER_KEY: &str = "mcp_server";
563    /// 结果级(SHOULD):面向用户的**非敏感**授权提示对象(`action`/`message`,已脱敏)/
564    /// non-sensitive user-facing auth hint object (sanitized per error-handling.md §454)。
565    pub const AUTH_HINT_KEY: &str = "auth_hint";
566}
567
568impl ToolCallRet {
569    fn meta_object_mut(&mut self) -> &mut serde_json::Map<String, serde_json::Value> {
570        if !matches!(self.meta, Some(serde_json::Value::Object(_))) {
571            self.meta = Some(serde_json::Value::Object(serde_json::Map::new()));
572        }
573        match self.meta.as_mut() {
574            Some(serde_json::Value::Object(map)) => map,
575            _ => unreachable!("meta 刚被规整为对象"),
576        }
577    }
578
579    /// 标记取消(结果级 `meta.a2c_cancelled = true` + `a2c_cancel_reason`)。
580    ///
581    /// 取消语义 MUST `a2c_cancelled=true`;`reason` 缺省 [`tool_meta::A2C_DEFAULT_CANCEL_REASON`]。
582    pub fn mark_cancelled(&mut self, reason: Option<&str>) {
583        let reason = reason
584            .unwrap_or(tool_meta::A2C_DEFAULT_CANCEL_REASON)
585            .to_string();
586        let map = self.meta_object_mut();
587        map.insert(tool_meta::A2C_CANCELLED_KEY.to_string(), true.into());
588        map.insert(tool_meta::A2C_CANCEL_REASON_KEY.to_string(), reason.into());
589    }
590
591    /// 标记超时(结果级 `meta.a2c_timeout = true`)/ 超时语义 SHOULD `a2c_timeout=true`。
592    pub fn mark_timeout(&mut self) {
593        self.meta_object_mut()
594            .insert(tool_meta::A2C_TIMEOUT_KEY.to_string(), true.into());
595    }
596
597    /// 是否被取消(读结果级 `meta.a2c_cancelled`)/ Whether cancelled。
598    pub fn is_cancelled(&self) -> bool {
599        meta_bool(self.meta.as_ref(), tool_meta::A2C_CANCELLED_KEY)
600    }
601
602    /// 取消原因(读结果级 `meta.a2c_cancel_reason`)/ Cancel reason if present。
603    pub fn cancel_reason(&self) -> Option<&str> {
604        self.meta
605            .as_ref()
606            .and_then(|m| m.get(tool_meta::A2C_CANCEL_REASON_KEY))
607            .and_then(serde_json::Value::as_str)
608    }
609
610    /// 是否超时(读结果级 `meta.a2c_timeout`)/ Whether timed out。
611    pub fn is_timeout(&self) -> bool {
612        meta_bool(self.meta.as_ref(), tool_meta::A2C_TIMEOUT_KEY)
613    }
614}
615
616fn meta_bool(meta: Option<&serde_json::Value>, key: &str) -> bool {
617    meta.and_then(|m| m.get(key))
618        .and_then(serde_json::Value::as_bool)
619        .unwrap_or(false)
620}
621
622/// content item 的二进制旁路三元组 / The binary-sideband triple on a content item。
623#[derive(Debug, Clone, PartialEq, Eq)]
624pub struct BlobSideband {
625    /// 旁路句柄 / sideband handle(`_meta.a2c_blob_handle`)。
626    pub blob_handle: BlobHandle,
627    /// 全量字节数 / total bytes(`_meta.a2c_total_size`,可选)。
628    pub total_size: Option<u64>,
629    /// 全量 sha256 / full-content sha256(`_meta.a2c_sha256`,可选)。
630    pub sha256: Option<String>,
631}
632
633/// 在 content item 上写入二进制旁路 / Write the binary sideband onto a content item。
634///
635/// 写 `item._meta.a2c_blob_handle`(+ 可选 `a2c_total_size` / `a2c_sha256`)。`item` 非对象时不动。
636/// 对标 Python computer `socketio/client.py` 的旁路写入。
637pub fn set_content_blob_sideband(
638    item: &mut serde_json::Value,
639    blob_handle: &str,
640    total_size: Option<u64>,
641    sha256: Option<&str>,
642) {
643    let serde_json::Value::Object(obj) = item else {
644        return;
645    };
646    let meta = obj
647        .entry("_meta")
648        .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
649    if !meta.is_object() {
650        *meta = serde_json::Value::Object(serde_json::Map::new());
651    }
652    if let serde_json::Value::Object(meta) = meta {
653        meta.insert(
654            tool_meta::A2C_BLOB_HANDLE_KEY.to_string(),
655            blob_handle.into(),
656        );
657        if let Some(size) = total_size {
658            meta.insert(tool_meta::A2C_TOTAL_SIZE_KEY.to_string(), size.into());
659        }
660        if let Some(hash) = sha256 {
661            meta.insert(tool_meta::A2C_SHA256_KEY.to_string(), hash.into());
662        }
663    }
664}
665
666/// 读取 content item 的二进制旁路 / Read the binary sideband off a content item。
667///
668/// 命中 `item._meta.a2c_blob_handle`(非空字符串)→ `Some`;否则 `None`。对标 Python agent
669/// `_blob_sideband.py` 的旁路枚举。
670pub fn read_content_blob_sideband(item: &serde_json::Value) -> Option<BlobSideband> {
671    let meta = item.get("_meta")?;
672    let blob_handle = meta
673        .get(tool_meta::A2C_BLOB_HANDLE_KEY)
674        .and_then(serde_json::Value::as_str)
675        .filter(|s| !s.is_empty())?;
676    Some(BlobSideband {
677        blob_handle: blob_handle.to_string(),
678        total_size: meta
679            .get(tool_meta::A2C_TOTAL_SIZE_KEY)
680            .and_then(serde_json::Value::as_u64),
681        sha256: meta
682            .get(tool_meta::A2C_SHA256_KEY)
683            .and_then(serde_json::Value::as_str)
684            .map(str::to_string),
685    })
686}
687
688/// 获取工具请求
689#[derive(Debug, Clone, Serialize, Deserialize)]
690pub struct GetToolsReq {
691    #[serde(flatten)]
692    pub base: AgentCallData,
693    pub computer: String,
694}
695
696/// SMCP工具定义
697#[derive(Debug, Clone, Serialize, Deserialize)]
698pub struct SMCPTool {
699    pub name: String,
700    pub description: String,
701    pub params_schema: serde_json::Value,
702    pub return_schema: Option<serde_json::Value>,
703    #[serde(skip_serializing_if = "Option::is_none")]
704    pub meta: Option<serde_json::Value>,
705}
706
707/// 获取工具返回
708#[derive(Debug, Clone, Serialize, Deserialize)]
709pub struct GetToolsRet {
710    pub tools: Vec<SMCPTool>,
711    pub req_id: ReqId,
712}
713
714/// 代理调用数据(基类)
715#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
716pub struct AgentCallData {
717    pub agent: String,
718    pub req_id: ReqId,
719}
720
721/// 进入办公室请求
722#[derive(Debug, Clone, Serialize, Deserialize)]
723pub struct EnterOfficeReq {
724    pub role: Role,
725    pub name: String,
726    pub office_id: String,
727}
728
729/// 离开办公室请求
730#[derive(Debug, Clone, Serialize, Deserialize)]
731pub struct LeaveOfficeReq {
732    pub office_id: String,
733}
734
735/// 获取桌面请求
736#[derive(Debug, Clone, Serialize, Deserialize)]
737pub struct GetDesktopReq {
738    #[serde(flatten)]
739    pub base: AgentCallData,
740    pub computer: String,
741    #[serde(skip_serializing_if = "Option::is_none")]
742    pub desktop_size: Option<i32>,
743    #[serde(skip_serializing_if = "Option::is_none")]
744    pub window: Option<String>,
745}
746
747/// 桌面类型别名
748pub type Desktop = String;
749
750/// 获取桌面返回
751#[derive(Debug, Clone, Serialize, Deserialize)]
752pub struct GetDesktopRet {
753    #[serde(skip_serializing_if = "Option::is_none")]
754    pub desktops: Option<Vec<Desktop>>,
755    pub req_id: ReqId,
756}
757
758// ── 资源发现 / Resource discovery (v0.2.0) ─────────────────────────────────
759// 协议依据 / Protocol: events.md §client:get_resources(透明转发 MCP `resources/list`,cursor 翻页,
760// 不返回 resourceTemplates);data-structures.md §A2CResource / §ResourceAnnotations。
761// Python 参考 / reference: a2c_smcp/smcp.py(A2CResource / ResourceAnnotations / GetResourcesReq / GetResourcesRet)。
762//
763// 元数据下沉 / Metadata sink (v0.2.0):原 `window://` query 参数迁入 MCP 标准 `annotations`
764// (priority/audience/last_modified)与 A2C 扩展 `_meta`(如 fullscreen 等业务字段)。
765
766/// `client:get_resources` 事件名 / event name(顶层别名,等于 [`events::CLIENT_GET_RESOURCES`])。
767///
768/// 对齐 Python `a2c_smcp.smcp.GET_RESOURCES_EVENT`。
769pub const GET_RESOURCES_EVENT: &str = events::CLIENT_GET_RESOURCES;
770
771/// MCP Resource 标准 annotations 的目标受众 / target audience of a resource。
772///
773/// 对标 MCP `Annotations.audience`(`list[Literal["user", "assistant"]]`)。
774#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
775#[serde(rename_all = "lowercase")]
776pub enum ResourceAudience {
777    /// 面向最终用户 / for the end user。
778    User,
779    /// 面向 AI 助手 / for the AI assistant。
780    Assistant,
781}
782
783/// MCP Resource 标准 annotations(snake_case mirror)/ MCP standard Resource annotations。
784///
785/// 全字段可选(对标 Python `ResourceAnnotations(TypedDict, total=False)`)。`priority` 取值 `[0, 1]`
786/// (协议不在类型层强制范围,由生产方保证;边界 `0.0` / `1.0` 合法)。
787#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
788pub struct ResourceAnnotations {
789    /// 目标受众 / intended audience。
790    #[serde(default, skip_serializing_if = "Option::is_none")]
791    pub audience: Option<Vec<ResourceAudience>>,
792    /// 优先级 `[0, 1]`,越大越重要 / priority in `[0, 1]`, higher = more important。
793    #[serde(default, skip_serializing_if = "Option::is_none")]
794    pub priority: Option<f32>,
795    /// 最后修改时间(ISO 8601)/ last-modified timestamp (ISO 8601)。
796    #[serde(default, skip_serializing_if = "Option::is_none")]
797    pub last_modified: Option<String>,
798}
799
800/// A2C 协议层 Resource,snake_case mirror MCP `Resource` / A2C protocol-level Resource。
801///
802/// 全字段可选(对标 Python `A2CResource(TypedDict, total=False)`)。元数据分工 / metadata partition:
803/// - `annotations`:MCP 标准字段(priority / audience / last_modified)。
804/// - `_meta`([`A2CResource::meta`]):A2C 扩展(如 fullscreen 等业务字段)。
805#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
806pub struct A2CResource {
807    /// 资源 URI / resource URI(如 `window://...` 或业务自定义 scheme)。
808    #[serde(default, skip_serializing_if = "Option::is_none")]
809    pub uri: Option<String>,
810    /// 资源名 / resource name。
811    #[serde(default, skip_serializing_if = "Option::is_none")]
812    pub name: Option<String>,
813    /// 资源描述 / resource description。
814    #[serde(default, skip_serializing_if = "Option::is_none")]
815    pub description: Option<String>,
816    /// 资源 MIME 类型 / resource MIME type。
817    #[serde(default, skip_serializing_if = "Option::is_none")]
818    pub mime_type: Option<String>,
819    /// 资源字节数 / resource size in bytes。
820    #[serde(default, skip_serializing_if = "Option::is_none")]
821    pub size: Option<u64>,
822    /// MCP 标准 annotations / MCP standard annotations。
823    #[serde(default, skip_serializing_if = "Option::is_none")]
824    pub annotations: Option<ResourceAnnotations>,
825    /// A2C 扩展元数据(线字段名 `_meta`)/ A2C extension metadata (wire field `_meta`)。
826    #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
827    pub meta: Option<serde_json::Value>,
828}
829
830/// 资源发现请求 / Resource discovery request(`client:get_resources`)。
831///
832/// 透明转发 MCP 标准 `resources/list`;Agent 据此发现 window / 业务自定义 scheme 的资源。
833#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
834pub struct GetResourcesReq {
835    #[serde(flatten)]
836    pub base: AgentCallData,
837    /// 目标 Computer 名 / target Computer name。
838    pub computer: String,
839    /// 必填:目标 MCP Server 名称 / required: target MCP Server name。
840    pub mcp_server: String,
841    /// MCP 标准翻页游标(可选;缺省取首页)/ MCP-standard pagination cursor (optional)。
842    #[serde(default, skip_serializing_if = "Option::is_none")]
843    pub cursor: Option<String>,
844}
845
846/// 资源发现响应 / Resource discovery response(`GetResourcesRet`)。
847///
848/// 含 MCP 标准 cursor 翻页。`next_cursor` 缺省表示无下一页。
849#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
850pub struct GetResourcesRet {
851    /// 资源列表(可空)/ resource list (may be empty)。
852    #[serde(default)]
853    pub resources: Vec<A2CResource>,
854    /// 下一页游标(缺省 = 无下一页)/ next-page cursor (absent = last page)。
855    #[serde(default, skip_serializing_if = "Option::is_none")]
856    pub next_cursor: Option<String>,
857    /// 回显的请求 id(可选)/ echoed request id (optional)。
858    #[serde(default, skip_serializing_if = "Option::is_none")]
859    pub req_id: Option<ReqId>,
860}
861
862/// 列出房间请求
863#[derive(Debug, Clone, Serialize, Deserialize)]
864pub struct ListRoomReq {
865    #[serde(flatten)]
866    pub base: AgentCallData,
867    pub office_id: String,
868}
869
870/// 会话信息
871#[derive(Debug, Clone, Serialize, Deserialize)]
872pub struct SessionInfo {
873    pub sid: String,
874    pub name: String,
875    pub role: Role,
876    pub office_id: String,
877    /// 协议版本号(v0.2 新增):由 Server 在 HTTP 握手阶段从连接 URL query `a2c_version`
878    /// 写入,仅用于展示与诊断,**不参与二次校验**(兼容性已由握手中间件保证)。缺省(旧连接
879    /// 未协商)时省略序列化。对标 Python `SessionInfo.a2c_version: NotRequired[str]`。
880    ///
881    /// Protocol version (v0.2): written by the Server from the connect URL query `a2c_version`
882    /// during the HTTP handshake; for display/diagnostics only, never re-validated. Omitted from
883    /// serialization when absent (legacy connections that did not negotiate).
884    #[serde(default, skip_serializing_if = "Option::is_none")]
885    pub a2c_version: Option<String>,
886}
887
888/// 列出房间返回
889#[derive(Debug, Clone, Serialize, Deserialize)]
890pub struct ListRoomRet {
891    pub sessions: Vec<SessionInfo>,
892    pub req_id: ReqId,
893}
894
895/// 进入办公室通知
896#[derive(Debug, Clone, Serialize, Deserialize)]
897pub struct EnterOfficeNotification {
898    pub office_id: String,
899    #[serde(skip_serializing_if = "Option::is_none")]
900    pub computer: Option<String>,
901    #[serde(skip_serializing_if = "Option::is_none")]
902    pub agent: Option<String>,
903}
904
905/// 离开办公室通知
906#[derive(Debug, Clone, Serialize, Deserialize)]
907pub struct LeaveOfficeNotification {
908    pub office_id: String,
909    #[serde(skip_serializing_if = "Option::is_none")]
910    pub computer: Option<String>,
911    #[serde(skip_serializing_if = "Option::is_none")]
912    pub agent: Option<String>,
913}
914
915/// 更新MCP配置通知
916#[derive(Debug, Clone, Serialize, Deserialize)]
917pub struct UpdateMCPConfigNotification {
918    pub computer: String,
919}
920
921/// 更新工具列表通知
922#[derive(Debug, Clone, Serialize, Deserialize)]
923pub struct UpdateToolListNotification {
924    pub computer: String,
925}
926
927/// 通知类型枚举
928#[derive(Debug, Clone, Serialize, Deserialize)]
929#[serde(tag = "type")]
930pub enum Notification {
931    ToolCallCancel,
932    EnterOffice(EnterOfficeNotification),
933    LeaveOffice(LeaveOfficeNotification),
934    UpdateMCPConfig(UpdateMCPConfigNotification),
935    UpdateToolList(UpdateToolListNotification),
936    UpdateDesktop,
937}
938
939// ── SKILL 通道 / SKILL channel (v0.2.1 + v0.2.2 naming) ─────────────────────
940// 协议依据 / Protocol: events.md §client:get_skill[s] / §server:update_skills / §notify:update_skills;
941// data-structures.md §A2CSkillRef / §GetSkills* / §GetSkill*;skill.md §1 命名 / §6 数据结构 / §9 安全。
942// Python 参考 / reference: a2c_smcp/smcp.py(A2CSkillRef / GetSkillsReq|Ret / GetSkillReq|Ret)。
943//
944// 命名(v0.2.2)/ Naming:合成全局唯一**裸名**——user 1 段 `<skill>` / marketplace 2 段
945// `<plugin>:<skill>` / mcp 3 段 `mcp:<server>:<skill>`,段数消歧由 [`skill_name`] lexer 承担。
946// Agent **MUST** 当作不透明可比较字符串,判来源用 `source`(完整 provenance,**不**进 name)。
947
948/// `client:get_skills` 事件名 / event name(顶层别名,等于 [`events::CLIENT_GET_SKILLS`])。
949pub const GET_SKILLS_EVENT: &str = events::CLIENT_GET_SKILLS;
950/// `client:get_skill` 事件名 / event name(顶层别名,等于 [`events::CLIENT_GET_SKILL`])。
951pub const GET_SKILL_EVENT: &str = events::CLIENT_GET_SKILL;
952/// `server:update_skills` 事件名 / event name(顶层别名,等于 [`events::SERVER_UPDATE_SKILLS`])。
953pub const UPDATE_SKILLS_EVENT: &str = events::SERVER_UPDATE_SKILLS;
954/// `notify:update_skills` 通知名 / notification name(顶层别名,等于 [`events::NOTIFY_UPDATE_SKILLS`])。
955pub const UPDATE_SKILLS_NOTIFICATION: &str = events::NOTIFY_UPDATE_SKILLS;
956
957/// SKILL 引用对象 / Skill reference object —— `client:get_skills` 返回列表元素。
958///
959/// 必选 4 字段 / required 4:`name` / `source` / `path` / `description`(生产方 Computer **MUST** 发齐;
960/// 消费方 Agent **MUST NOT** 假定任一可选字段存在)。其余 6 个为可选(`#[serde(skip_serializing_if)]`)。
961///
962/// 关键约束 / Key invariants:
963/// - `name` 是协议主键(合成全局唯一裸名),Agent **MUST** 当不透明可比较字符串(勿解析结构)。
964/// - `source` 承载完整溯源(如 `mcp:tfrobot-tools` / `marketplace:acme-skills` / `user`),**不**进 `name`。
965/// - `path` 必选:Computer 本地绝对目录(staging 落盘是所有 source 统一第一步),面向 Agent SDK,LLM 不可见。
966/// - **无 `mcp_server` 字段**——Agent 侧协议表面与 source 无关;来源追溯由 `source` 与(仅 MCP)`uri` 承担。
967#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
968pub struct A2CSkillRef {
969    /// 主键:合成全局唯一裸名 / primary key: synthesized globally-unique bare name(必选 / required)。
970    pub name: String,
971    /// 来源元数据:完整 provenance / provenance(必选 / required;**不**进 `name`)。
972    pub source: String,
973    /// 仅 MCP 来源:`skill://host/skill-name`;Agent 非权威 / MCP-only, Agent non-authoritative(可选)。
974    #[serde(default, skip_serializing_if = "Option::is_none")]
975    pub uri: Option<String>,
976    /// 物化输出:Computer 本地绝对目录路径 / materialization output: local absolute dir(必选 / required)。
977    pub path: String,
978    /// SKILL.md frontmatter 派生描述(跨三源强制)/ description from frontmatter(必选 / required)。
979    pub description: String,
980    /// 许可证 / license(可选)。
981    #[serde(default, skip_serializing_if = "Option::is_none")]
982    pub license: Option<String>,
983    /// 兼容性声明 / compatibility(可选)。
984    #[serde(default, skip_serializing_if = "Option::is_none")]
985    pub compatibility: Option<String>,
986    /// frontmatter `allowed-tools` 规范化为列表 / normalized `allowed-tools` list(可选)。
987    #[serde(default, skip_serializing_if = "Option::is_none")]
988    pub allowed_tools: Option<Vec<String>>,
989    /// 版本(非 frontmatter 派生)/ version (not frontmatter-derived)(可选)。
990    #[serde(default, skip_serializing_if = "Option::is_none")]
991    pub version: Option<String>,
992    /// frontmatter.metadata 透传;A2C 不解释 / frontmatter.metadata passthrough(可选)。
993    #[serde(default, skip_serializing_if = "Option::is_none")]
994    pub skill_metadata: Option<serde_json::Value>,
995}
996
997/// 获取 SKILL 清单请求 / Get SKILL inventory request(`client:get_skills`)。
998#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
999pub struct GetSkillsReq {
1000    #[serde(flatten)]
1001    pub base: AgentCallData,
1002    /// 目标 Computer 名 / target Computer name。
1003    pub computer: String,
1004}
1005
1006/// 获取 SKILL 清单响应 / Get SKILL inventory response(`GetSkillsRet`)。
1007///
1008/// 轻量元数据,**不含** SKILL.md body(body 由 `client:get_skill` 拉取);排除孤儿、不排序、不去重。
1009#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1010pub struct GetSkillsRet {
1011    /// 当前已安装且可用 SKILL / currently installed & available skills。
1012    #[serde(default)]
1013    pub skills: Vec<A2CSkillRef>,
1014    /// 回显的请求 id(可选)/ echoed request id (optional)。
1015    #[serde(default, skip_serializing_if = "Option::is_none")]
1016    pub req_id: Option<ReqId>,
1017}
1018
1019/// 获取 SKILL 包内单个资源请求 / Get a single in-package SKILL resource(`client:get_skill`)。
1020///
1021/// SKILL 本质是文件夹:`rel_path` 缺省取包根 `SKILL.md`(入口),携带 `rel_path` 取包内其它资源
1022/// (渐进式披露)。安全:`rel_path` MUST 相对、无 `..`、无绝对路径,沙箱在 Computer 解析时强制。
1023#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1024pub struct GetSkillReq {
1025    #[serde(flatten)]
1026    pub base: AgentCallData,
1027    /// 目标 Computer 名 / target Computer name。
1028    pub computer: String,
1029    /// 必选:来自某 [`A2CSkillRef::name`] / required: from some `A2CSkillRef.name`。
1030    pub name: String,
1031    /// 可选:SKILL 包根 POSIX 相对路径;缺省 = `"SKILL.md"` / optional, defaults to `SKILL.md`。
1032    #[serde(default, skip_serializing_if = "Option::is_none")]
1033    pub rel_path: Option<String>,
1034}
1035
1036/// SKILL 资源载体 / SKILL resource carrier —— [`GetSkillRet`] 的 `body` 与 `blob_handle` **恰一存在**。
1037///
1038/// 文本且可内联 → [`SkillResource::Inline`];二进制或过大文本 → [`SkillResource::Blob`]
1039/// (转 `client:get_blob` 拉取)。由 [`GetSkillRet::resource`] 校验并返回。
1040#[derive(Debug, Clone, PartialEq, Eq)]
1041pub enum SkillResource<'a> {
1042    /// 内联文本内容(来自 `body`)/ inline text content (from `body`)。
1043    Inline(&'a str),
1044    /// 二进制旁路句柄(来自 `blob_handle`)/ binary sideband handle (from `blob_handle`)。
1045    Blob(&'a str),
1046}
1047
1048/// [`GetSkillRet`] 的 `body` / `blob_handle` 互斥不变式被破坏 / exclusivity invariant violated。
1049#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
1050pub enum SkillRetError {
1051    /// `body` 与 `blob_handle` 同时存在(违反 exactly-one)/ both present。
1052    #[error("GetSkillRet: body and blob_handle are mutually exclusive (both present)")]
1053    BothPresent,
1054    /// `body` 与 `blob_handle` 均缺失(违反 exactly-one)/ neither present。
1055    #[error("GetSkillRet: exactly one of body / blob_handle must be present (neither found)")]
1056    NeitherPresent,
1057}
1058
1059/// 获取 SKILL 包内单个资源响应 / Get-skill response(`GetSkillRet`)。
1060///
1061/// `body` 与 `blob_handle` **恰一存在**(exactly one)——经 [`GetSkillRet::resource`] 校验访问。
1062/// `total_size` / `sha256` 基于 **Agent 最终消费的资源字节**(SKILL.md → frontmatter 剥离后 body;
1063/// 其它 → 原始字节)。
1064#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1065pub struct GetSkillRet {
1066    /// 回显请求 name / echoed request name。
1067    #[serde(default, skip_serializing_if = "Option::is_none")]
1068    pub name: Option<String>,
1069    /// 回显请求 rel_path(缺省请求时回显 `"SKILL.md"`)/ echoed rel_path。
1070    #[serde(default, skip_serializing_if = "Option::is_none")]
1071    pub rel_path: Option<String>,
1072    /// 资源 MIME,如 `text/markdown` / `image/png` / resource MIME。
1073    #[serde(default, skip_serializing_if = "Option::is_none")]
1074    pub mime_type: Option<String>,
1075    /// 资源总字节数(基于最终消费字节)/ total bytes (final consumed bytes)。
1076    #[serde(default, skip_serializing_if = "Option::is_none")]
1077    pub total_size: Option<u64>,
1078    /// 全量资源 sha256 十六进制(完整性 + 变更检测)/ full-content sha256 (hex)。
1079    #[serde(default, skip_serializing_if = "Option::is_none")]
1080    pub sha256: Option<String>,
1081    /// 文本且 ≤ 内联预算:直接内容(与 `blob_handle` 恰一)/ inline body。
1082    #[serde(default, skip_serializing_if = "Option::is_none")]
1083    pub body: Option<String>,
1084    /// 否则:转 `client:get_blob` 的不透明句柄(与 `body` 恰一)/ otherwise: blob handle。
1085    #[serde(default, skip_serializing_if = "Option::is_none")]
1086    pub blob_handle: Option<BlobHandle>,
1087    /// 回显的请求 id(可选)/ echoed request id (optional)。
1088    #[serde(default, skip_serializing_if = "Option::is_none")]
1089    pub req_id: Option<ReqId>,
1090}
1091
1092impl GetSkillRet {
1093    /// 校验 `body` / `blob_handle` **恰一存在**并返回内容载体 / validate exactly-one & return carrier。
1094    ///
1095    /// 二者同时存在 → [`SkillRetError::BothPresent`];均缺失 → [`SkillRetError::NeitherPresent`]。
1096    pub fn resource(&self) -> Result<SkillResource<'_>, SkillRetError> {
1097        match (self.body.as_deref(), self.blob_handle.as_deref()) {
1098            (Some(body), None) => Ok(SkillResource::Inline(body)),
1099            (None, Some(handle)) => Ok(SkillResource::Blob(handle)),
1100            (Some(_), Some(_)) => Err(SkillRetError::BothPresent),
1101            (None, None) => Err(SkillRetError::NeitherPresent),
1102        }
1103    }
1104
1105    /// `body` / `blob_handle` 是否满足 exactly-one 不变式 / whether the exactly-one invariant holds。
1106    pub fn is_exclusive(&self) -> bool {
1107        self.body.is_some() ^ self.blob_handle.is_some()
1108    }
1109}
1110
1111// ── 通用二进制传输 / Generic binary transfer (v0.2.1) ──────────────────────
1112// 协议依据 / Protocol: blob-transfer.md(句柄契约 / 生产者-消费者模型 / 安全模型);
1113// data-structures.md §GetBlobReq / §GetBlobRet。任何通道把大/二进制内容以句柄旁路,Agent 经
1114// 统一 `client:get_blob` 无状态分块拉取,`sha256` 自证完整性。
1115// Python 参考 / reference: a2c_smcp/smcp.py(BlobHandle / GetBlobReq / GetBlobRet)。
1116
1117/// `client:get_blob` 事件名 / event name(顶层别名,等于 [`events::CLIENT_GET_BLOB`])。
1118///
1119/// 对齐 Python `a2c_smcp.smcp.GET_BLOB_EVENT`。
1120pub const GET_BLOB_EVENT: &str = events::CLIENT_GET_BLOB;
1121
1122/// 不透明、Computer 铸造、无状态可重解析的 blob 句柄(类型别名)。
1123/// Opaque, Computer-minted, stateless-reparseable blob handle (type alias)。
1124///
1125/// Agent **MUST** 视为不透明字符串:**MUST NOT** 解析 / 拼接 / 伪造 / 跨 Computer 复用
1126/// (blob-transfer.md §1)。
1127pub type BlobHandle = String;
1128
1129/// 通用二进制拉取请求 / Generic binary pull request(`client:get_blob`)。
1130///
1131/// Computer 无服务端状态、幂等可并行(`chunk_offset` 为资源**解码后字节**的绝对偏移)。
1132#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1133pub struct GetBlobReq {
1134    #[serde(flatten)]
1135    pub base: AgentCallData,
1136    /// 目标 Computer 名 / target Computer name。
1137    pub computer: String,
1138    /// 来自某通道响应的不透明句柄 / opaque handle from a producer response。
1139    pub blob_handle: BlobHandle,
1140    /// 资源字节绝对偏移;**缺省 0**(无状态幂等 → 可续传 / 重试 / 并行)。
1141    #[serde(default, skip_serializing_if = "Option::is_none")]
1142    pub chunk_offset: Option<u64>,
1143    /// 客户建议单块上限(字节);**缺省时由 Computer clamp 决定**。
1144    #[serde(default, skip_serializing_if = "Option::is_none")]
1145    pub max_chunk_bytes: Option<u64>,
1146}
1147
1148/// 通用二进制拉取响应 / Generic binary pull response(`GetBlobRet`)。
1149///
1150/// 完整性铁律:`eof` ⟺ `chunk_offset + len(decode(blob)) == total_size`;`eof` 后 Agent SHOULD
1151/// 校验重组 `sha256`;一次逻辑读取内 `sha256` / `total_size` MUST 稳定。空资源 = `total_size=0`,
1152/// 单响应 `eof=true`、`blob=""`。
1153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1154pub struct GetBlobRet {
1155    /// 回显的句柄 / echoed handle。
1156    pub blob_handle: BlobHandle,
1157    /// 资源 MIME(可选)/ resource MIME (optional)。
1158    #[serde(default, skip_serializing_if = "Option::is_none")]
1159    pub mime_type: Option<String>,
1160    /// 资源总字节数(首块即知;一次读取内恒定)/ total bytes。
1161    pub total_size: u64,
1162    /// 全量资源 sha256 十六进制(跨块恒定)/ full-content sha256 (hex)。
1163    pub sha256: String,
1164    /// 本块起始字节偏移 / this chunk's start byte offset。
1165    pub chunk_offset: u64,
1166    /// 末块标记 / end-of-file marker(⟺ `chunk_offset + 本块字节数 == total_size`)。
1167    pub eof: bool,
1168    /// 本块字节的 base64 编码 / base64 of this chunk's bytes。
1169    pub blob: String,
1170    /// 回显的请求 id(可选)/ echoed request id (optional)。
1171    #[serde(default, skip_serializing_if = "Option::is_none")]
1172    pub req_id: Option<ReqId>,
1173}
1174
1175#[cfg(test)]
1176mod tests {
1177    use super::*;
1178
1179    #[test]
1180    fn test_req_id_helpers() {
1181        let req_id = ReqId::new();
1182        assert!(!req_id.as_str().is_empty());
1183
1184        let req_id2 = ReqId::from_string("abc".to_string());
1185        assert_eq!(req_id2.as_str(), "abc");
1186
1187        let req_id3 = ReqId::default();
1188        assert!(!req_id3.as_str().is_empty());
1189    }
1190
1191    #[test]
1192    fn test_role_serde_lowercase() {
1193        let json = serde_json::to_string(&Role::Agent).unwrap();
1194        assert_eq!(json, "\"agent\"");
1195
1196        let de: Role = serde_json::from_str("\"computer\"").unwrap();
1197        assert!(matches!(de, Role::Computer));
1198    }
1199
1200    #[test]
1201    fn test_session_info_a2c_version_serde() {
1202        // Some(version) → JSON 顶层含 "a2c_version"
1203        let with_ver = SessionInfo {
1204            sid: "sid-1".to_string(),
1205            name: "agent-1".to_string(),
1206            role: Role::Agent,
1207            office_id: "office-1".to_string(),
1208            a2c_version: Some("0.2.0".to_string()),
1209        };
1210        let json = serde_json::to_value(&with_ver).unwrap();
1211        assert_eq!(json["a2c_version"], "0.2.0");
1212
1213        // None → skip_serializing_if 省略键(必须缺键,而非输出 null;对齐 Python NotRequired[str])
1214        let without_ver = SessionInfo {
1215            sid: "sid-2".to_string(),
1216            name: "computer-1".to_string(),
1217            role: Role::Computer,
1218            office_id: "office-1".to_string(),
1219            a2c_version: None,
1220        };
1221        let json_none = serde_json::to_value(&without_ver).unwrap();
1222        assert!(
1223            json_none.get("a2c_version").is_none(),
1224            "None 时必须省略 a2c_version 键,实得: {json_none}"
1225        );
1226
1227        // 缺键 JSON(旧连接报文)→ default 反序列化为 None(容缺)
1228        let legacy = r#"{"sid":"sid-3","name":"c2","role":"computer","office_id":"office-1"}"#;
1229        let de: SessionInfo = serde_json::from_str(legacy).unwrap();
1230        assert!(de.a2c_version.is_none());
1231
1232        // 往返一致(含版本)
1233        let back: SessionInfo = serde_json::from_value(json).unwrap();
1234        assert_eq!(back.a2c_version.as_deref(), Some("0.2.0"));
1235    }
1236
1237    #[test]
1238    fn test_notification_serde() {
1239        let n = Notification::EnterOffice(EnterOfficeNotification {
1240            office_id: "office1".to_string(),
1241            computer: Some("c1".to_string()),
1242            agent: None,
1243        });
1244
1245        let json = serde_json::to_string(&n).unwrap();
1246        let de: Notification = serde_json::from_str(&json).unwrap();
1247        match de {
1248            Notification::EnterOffice(p) => {
1249                assert_eq!(p.office_id, "office1");
1250                assert_eq!(p.computer.as_deref(), Some("c1"));
1251                assert!(p.agent.is_none());
1252            }
1253            _ => panic!("unexpected notification"),
1254        }
1255    }
1256
1257    #[test]
1258    fn test_tool_call_ret_mcp_format() {
1259        // 测试成功的工具调用返回(MCP CallToolResult 格式)
1260        let success_ret = ToolCallRet {
1261            content: Some(vec![serde_json::json!({
1262                "type": "text",
1263                "text": "Operation completed successfully"
1264            })]),
1265            is_error: Some(false),
1266            req_id: Some(ReqId::from_string("test123".to_string())),
1267            meta: None,
1268        };
1269
1270        let json = serde_json::to_string(&success_ret).unwrap();
1271
1272        // 验证 JSON 包含正确的 MCP 字段
1273        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1274        assert!(parsed.get("content").is_some());
1275        assert!(parsed.get("isError").is_some());
1276        assert_eq!(parsed.get("isError").unwrap(), false);
1277        assert_eq!(parsed.get("req_id").unwrap().as_str().unwrap(), "test123");
1278
1279        // 验证字段名是 camelCase(isError 而不是 is_error)
1280        assert!(json.contains("isError"));
1281        assert!(!json.contains("is_error"));
1282        // 验证没有旧的 Rust 风格字段(检查字段名而不是整个字符串)
1283        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1284        assert!(parsed.get("success").is_none());
1285        assert!(parsed.get("result").is_none());
1286        assert!(parsed.get("error").is_none());
1287    }
1288
1289    #[test]
1290    fn test_tool_call_ret_error_format() {
1291        // 测试错误的工具调用返回
1292        let error_ret = ToolCallRet {
1293            content: Some(vec![serde_json::json!({
1294                "type": "text",
1295                "text": "Tool execution failed"
1296            })]),
1297            is_error: Some(true),
1298            req_id: None,
1299            meta: None,
1300        };
1301
1302        let json = serde_json::to_string(&error_ret).unwrap();
1303
1304        // 验证 JSON 格式
1305        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1306        assert!(parsed.get("content").is_some());
1307        assert_eq!(parsed.get("isError").unwrap(), true);
1308        assert!(parsed.get("req_id").is_none());
1309
1310        // 验证没有旧的 Rust 风格字段
1311        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1312        assert!(parsed.get("success").is_none());
1313        assert!(parsed.get("result").is_none());
1314        assert!(parsed.get("error").is_none());
1315    }
1316
1317    #[test]
1318    fn test_tool_call_ret_minimal() {
1319        // 测试最小化的工具调用返回
1320        let minimal_ret = ToolCallRet {
1321            content: None,
1322            is_error: None,
1323            req_id: None,
1324            meta: None,
1325        };
1326
1327        let json = serde_json::to_string(&minimal_ret).unwrap();
1328        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1329
1330        // 空对象应该序列化为 {}
1331        assert_eq!(parsed, serde_json::json!({}));
1332    }
1333
1334    #[test]
1335    fn test_tool_call_ret_roundtrip() {
1336        // 测试序列化和反序列化的往返一致性
1337        let original = ToolCallRet {
1338            content: Some(vec![serde_json::json!({
1339                "type": "text",
1340                "text": "Test result"
1341            })]),
1342            is_error: Some(false),
1343            req_id: Some(ReqId::new()),
1344            meta: None,
1345        };
1346
1347        let json = serde_json::to_string(&original).unwrap();
1348        let deserialized: ToolCallRet = serde_json::from_str(&json).unwrap();
1349
1350        assert_eq!(original.content, deserialized.content);
1351        assert_eq!(original.is_error, deserialized.is_error);
1352        assert_eq!(original.req_id, deserialized.req_id);
1353    }
1354
1355    #[test]
1356    fn test_error_payload_flat_serialization() {
1357        // flat 形态:顶层 code/message,无嵌套 "error" envelope;details 为 None 时不序列化
1358        let payload = ErrorPayload::new(404, "Resource not found");
1359        let v = serde_json::to_value(&payload).unwrap();
1360
1361        assert!(v.get("error").is_none(), "禁止嵌套 envelope"); // 顶层平铺
1362        assert_eq!(v.get("code").unwrap(), 404);
1363        assert_eq!(v.get("message").unwrap(), "Resource not found");
1364        assert!(v.get("details").is_none()); // 没有 details 时不序列化
1365    }
1366
1367    #[test]
1368    fn test_error_payload_with_details_serialization() {
1369        // details 以对象形式平铺在顶层 details 容器内
1370        let payload = ErrorPayload::new(4014, "boom")
1371            .with_detail("mcp_server_name", "srv-a")
1372            .with_detail("hint", "retry");
1373        let v = serde_json::to_value(&payload).unwrap();
1374
1375        assert_eq!(v["code"], 4014);
1376        assert_eq!(v["details"]["mcp_server_name"], "srv-a");
1377        assert_eq!(v["details"]["hint"], "retry");
1378    }
1379
1380    #[test]
1381    fn test_is_protocol_error_payload_flat_true() {
1382        // 顶层 code 为协议 ErrorCode 取值 → true
1383        for code in [404, 4006, 4007, 4008, 4014, 4015, 4016, 4017, 4018] {
1384            let v = serde_json::json!({ "code": code, "message": "x" });
1385            assert!(
1386                is_protocol_error_payload(&v),
1387                "code {code} 应判定为协议错误负载"
1388            );
1389        }
1390        // 构造器产出的 payload 同样应判定为 true
1391        let v = serde_json::to_value(build_computer_not_found_error("c1")).unwrap();
1392        assert!(is_protocol_error_payload(&v));
1393    }
1394
1395    #[test]
1396    fn test_is_protocol_error_payload_false() {
1397        // 嵌套 envelope → false(禁止二次 unwrap 的关键防线)
1398        let nested = serde_json::json!({ "error": { "code": 404, "message": "x" } });
1399        assert!(!is_protocol_error_payload(&nested));
1400
1401        // 非协议码(legacy 服务内部码 400 / 未知码 9999)→ false
1402        assert!(!is_protocol_error_payload(
1403            &serde_json::json!({ "code": 400 })
1404        ));
1405        assert!(!is_protocol_error_payload(
1406            &serde_json::json!({ "code": 9999 })
1407        ));
1408
1409        // 缺 code / code 非整数 / 非对象 → false
1410        assert!(!is_protocol_error_payload(
1411            &serde_json::json!({ "message": "x" })
1412        ));
1413        assert!(!is_protocol_error_payload(
1414            &serde_json::json!({ "code": "404" })
1415        ));
1416        assert!(!is_protocol_error_payload(&serde_json::json!([
1417            "code", 404
1418        ])));
1419        assert!(!is_protocol_error_payload(&serde_json::json!("nope")));
1420    }
1421
1422    #[test]
1423    fn test_build_computer_not_found_error() {
1424        let payload = build_computer_not_found_error("my-computer");
1425        assert_eq!(payload.code, 404);
1426        assert_eq!(payload.code, i64::from(ErrorCode::NotFound.code()));
1427        assert!(payload.message.contains("my-computer"));
1428        assert_eq!(
1429            payload
1430                .details
1431                .as_ref()
1432                .unwrap()
1433                .get("computer_name")
1434                .unwrap(),
1435            "my-computer"
1436        );
1437    }
1438
1439    #[test]
1440    fn test_build_computer_not_found_error_python_byte_compat() {
1441        // 与 Python build_computer_not_found_error 逐字节一致(同字段名 / 层级 / 取值)
1442        let v = serde_json::to_value(build_computer_not_found_error("c1")).unwrap();
1443        let expected = serde_json::json!({
1444            "code": 404,
1445            "message": "Computer with name 'c1' not found",
1446            "details": { "computer_name": "c1" }
1447        });
1448        assert_eq!(v, expected);
1449    }
1450
1451    #[test]
1452    fn test_error_payload_roundtrip() {
1453        // ErrorPayload 对任意 code 通用;序列化/反序列化往返一致
1454        let original = ErrorPayload::new(500, "Internal error").with_detail("trace_id", "abc123");
1455
1456        let json = serde_json::to_string(&original).unwrap();
1457        let deserialized: ErrorPayload = serde_json::from_str(&json).unwrap();
1458
1459        assert_eq!(original, deserialized);
1460    }
1461
1462    #[test]
1463    fn test_error_payload_from_error_code() {
1464        // from_error_code:code 来自协议 ErrorCode(杜绝手工整数字面量),且必被 is_protocol_error_payload 识别
1465        let payload = ErrorPayload::from_error_code(ErrorCode::McpServerNotFound, "boom");
1466        assert_eq!(payload.code, 4014);
1467        assert_eq!(payload.code, i64::from(ErrorCode::McpServerNotFound.code()));
1468        assert_eq!(payload.message, "boom");
1469
1470        let v = serde_json::to_value(&payload).unwrap();
1471        assert!(is_protocol_error_payload(&v));
1472        // 顶层分流字段未设置时不序列化
1473        assert!(v.get("mcp_server_name").is_none());
1474        assert!(v.get("capability").is_none());
1475        assert!(v.get("details").is_none());
1476    }
1477
1478    #[test]
1479    fn test_error_payload_4014_top_level_field() {
1480        // 4014:mcp_server_name 顶层平铺(非 details 子对象)
1481        let v = serde_json::to_value(
1482            ErrorPayload::from_error_code(ErrorCode::McpServerNotFound, "not found")
1483                .with_mcp_server_name("filesystem"),
1484        )
1485        .unwrap();
1486        assert_eq!(
1487            v,
1488            serde_json::json!({
1489                "code": 4014,
1490                "message": "not found",
1491                "mcp_server_name": "filesystem"
1492            })
1493        );
1494    }
1495
1496    #[test]
1497    fn test_error_payload_4015_top_level_fields_python_shape() {
1498        // 4015:mcp_server_name + capability 双顶层字段,与 Python smcp.py:484 total=False 形态一致
1499        let v = serde_json::to_value(
1500            ErrorPayload::from_error_code(ErrorCode::McpCapabilityNotSupported, "unsupported")
1501                .with_mcp_server_name("docs-server")
1502                .with_capability("resources"),
1503        )
1504        .unwrap();
1505        assert_eq!(
1506            v,
1507            serde_json::json!({
1508                "code": 4015,
1509                "message": "unsupported",
1510                "mcp_server_name": "docs-server",
1511                "capability": "resources"
1512            })
1513        );
1514    }
1515
1516    #[test]
1517    fn test_error_payload_captures_unknown_top_level_fields() {
1518        // 跨-SDK 前向兼容:协议未来非破坏新增的顶层字段被 extra 捕获并原样保留(不静默丢失)
1519        let wire = serde_json::json!({
1520            "code": 4014,
1521            "message": "x",
1522            "mcp_server_name": "srv",   // 已建模顶层字段 → 落入 typed field,不进 extra
1523            "future_string": "keep-me", // 未建模 → 落入 extra
1524            "future_object": { "nested": true },
1525            "future_number": 7
1526        });
1527        let payload: ErrorPayload = serde_json::from_value(wire.clone()).unwrap();
1528
1529        // 已建模字段不污染 extra
1530        assert_eq!(payload.mcp_server_name.as_deref(), Some("srv"));
1531        assert!(!payload.extra.contains_key("mcp_server_name"));
1532        assert!(!payload.extra.contains_key("code"));
1533        // 未建模字段被 extra 捕获
1534        assert_eq!(payload.extra.get("future_string").unwrap(), "keep-me");
1535        assert_eq!(payload.extra.get("future_number").unwrap(), 7);
1536
1537        // 往返后所有顶层字段(含未知)逐字节保留
1538        let round = serde_json::to_value(&payload).unwrap();
1539        assert_eq!(round, wire);
1540    }
1541
1542    #[test]
1543    fn test_protocol_version_constant() {
1544        // PROTOCOL_VERSION 锁定 MAJOR.MINOR = 0.2.0(SKILL/blob 为加性升级,不改主次版本)
1545        assert_eq!(PROTOCOL_VERSION, "0.2.0");
1546    }
1547
1548    #[test]
1549    fn test_error_code_values() {
1550        // 枚举值与协议 error-handling.md / Python ErrorCode 全表完全一致
1551        assert_eq!(ErrorCode::NotFound.code(), 404);
1552        assert_eq!(ErrorCode::ToolAuthorizationRequired.code(), 4006);
1553        assert_eq!(ErrorCode::ToolAuthorizationFailed.code(), 4007);
1554        assert_eq!(ErrorCode::ProtocolVersionMismatch.code(), 4008);
1555        assert_eq!(ErrorCode::McpServerNotFound.code(), 4014);
1556        assert_eq!(ErrorCode::McpCapabilityNotSupported.code(), 4015);
1557        assert_eq!(ErrorCode::SkillNameInvalid.code(), 4016);
1558        assert_eq!(ErrorCode::SkillResourceNotAccessible.code(), 4017);
1559        assert_eq!(ErrorCode::BlobNotAccessible.code(), 4018);
1560    }
1561
1562    #[test]
1563    fn test_error_code_serializes_as_int() {
1564        // 必须序列化为裸整数(而非字符串 / 标签对象)
1565        assert_eq!(
1566            serde_json::to_string(&ErrorCode::ProtocolVersionMismatch).unwrap(),
1567            "4008"
1568        );
1569        assert_eq!(serde_json::to_string(&ErrorCode::NotFound).unwrap(), "404");
1570
1571        // 作为结构体字段时同样是裸整数(对齐 ErrorPayload.code 线格式)
1572        let v = serde_json::json!({ "code": ErrorCode::BlobNotAccessible });
1573        assert_eq!(v["code"], serde_json::json!(4018));
1574    }
1575
1576    #[test]
1577    fn test_error_code_deserializes_from_int() {
1578        let c: ErrorCode = serde_json::from_str("4014").unwrap();
1579        assert_eq!(c, ErrorCode::McpServerNotFound);
1580        // 未知码值必须报错(解析方对已知集合是封闭的)
1581        assert!(serde_json::from_str::<ErrorCode>("9999").is_err());
1582        assert_eq!(ErrorCode::from_code(9999), None);
1583    }
1584
1585    #[test]
1586    fn test_error_code_int_roundtrip() {
1587        for code in [
1588            ErrorCode::NotFound,
1589            ErrorCode::ToolAuthorizationRequired,
1590            ErrorCode::ToolAuthorizationFailed,
1591            ErrorCode::ProtocolVersionMismatch,
1592            ErrorCode::McpServerNotFound,
1593            ErrorCode::McpCapabilityNotSupported,
1594            ErrorCode::SkillNameInvalid,
1595            ErrorCode::SkillResourceNotAccessible,
1596            ErrorCode::BlobNotAccessible,
1597        ] {
1598            let json = serde_json::to_string(&code).unwrap();
1599            let back: ErrorCode = serde_json::from_str(&json).unwrap();
1600            assert_eq!(code, back);
1601            assert_eq!(ErrorCode::from_code(code.code()), Some(code));
1602        }
1603    }
1604
1605    #[test]
1606    fn test_ws_close_code_distinct_from_protocol_mismatch() {
1607        // 4900(WS close code)与 4008(ErrorPayload.code)是不同命名空间的不同值,MUST NOT 混用
1608        assert_eq!(WS_VERSION_HANDSHAKE_REJECTED_CLOSE_CODE, 4900);
1609        assert_eq!(ErrorCode::ProtocolVersionMismatch.code(), 4008);
1610        assert_ne!(
1611            WS_VERSION_HANDSHAKE_REJECTED_CLOSE_CODE,
1612            ErrorCode::ProtocolVersionMismatch.code()
1613        );
1614    }
1615
1616    #[test]
1617    fn test_tool_lookup_vs_execution_boundary() {
1618        // 4001/4003 语义边界:调用前查找失败 vs 执行失败,必须是不同的码
1619        assert_eq!(error_codes::TOOL_NOT_FOUND, 4001);
1620        assert_eq!(error_codes::TOOL_EXECUTION_FAILED, 4003);
1621        assert_ne!(
1622            error_codes::TOOL_NOT_FOUND,
1623            error_codes::TOOL_EXECUTION_FAILED
1624        );
1625    }
1626
1627    // ── #39 通用二进制传输 / Generic binary transfer ──────────────────────
1628
1629    #[test]
1630    fn test_get_blob_event_constant() {
1631        // GET_BLOB_EVENT 字符串精确匹配,且与 events 模块同源
1632        assert_eq!(GET_BLOB_EVENT, "client:get_blob");
1633        assert_eq!(GET_BLOB_EVENT, events::CLIENT_GET_BLOB);
1634    }
1635
1636    fn sample_blob_req(chunk_offset: Option<u64>, max_chunk_bytes: Option<u64>) -> GetBlobReq {
1637        GetBlobReq {
1638            base: AgentCallData {
1639                agent: "agent-1".to_string(),
1640                req_id: ReqId::from_string("r1".to_string()),
1641            },
1642            computer: "comp-1".to_string(),
1643            blob_handle: "opaque-handle".to_string(),
1644            chunk_offset,
1645            max_chunk_bytes,
1646        }
1647    }
1648
1649    #[test]
1650    fn test_get_blob_req_with_offset_serde() {
1651        let req = sample_blob_req(Some(1024), Some(65536));
1652        let v = serde_json::to_value(&req).unwrap();
1653        // flatten base(agent / req_id)+ computer + blob_handle 平铺在顶层
1654        assert_eq!(v["agent"], "agent-1");
1655        assert_eq!(v["req_id"], "r1");
1656        assert_eq!(v["computer"], "comp-1");
1657        assert_eq!(v["blob_handle"], "opaque-handle");
1658        assert_eq!(v["chunk_offset"], 1024);
1659        assert_eq!(v["max_chunk_bytes"], 65536);
1660        let back: GetBlobReq = serde_json::from_value(v).unwrap();
1661        assert_eq!(back, req);
1662    }
1663
1664    #[test]
1665    fn test_get_blob_req_without_offset_omits_optional_keys() {
1666        let req = sample_blob_req(None, None);
1667        let v = serde_json::to_value(&req).unwrap();
1668        // 可选字段 None → skip_serializing_if 不出现在线格式
1669        assert!(
1670            v.get("chunk_offset").is_none(),
1671            "缺省 chunk_offset 不应序列化"
1672        );
1673        assert!(
1674            v.get("max_chunk_bytes").is_none(),
1675            "缺省 max_chunk_bytes 不应序列化"
1676        );
1677        // 反序列化缺省 → None(缺省 offset 语义 = 0,由消费方解释)
1678        let back: GetBlobReq = serde_json::from_value(v).unwrap();
1679        assert_eq!(back, req);
1680        assert!(back.chunk_offset.is_none());
1681    }
1682
1683    #[test]
1684    fn test_get_blob_ret_first_and_last_chunk_roundtrip() {
1685        // 首块(eof=false)
1686        let first = GetBlobRet {
1687            blob_handle: "h".to_string(),
1688            mime_type: Some("application/octet-stream".to_string()),
1689            total_size: 10,
1690            sha256: "abc123".to_string(),
1691            chunk_offset: 0,
1692            eof: false,
1693            blob: "aGVsbG8=".to_string(), // base64("hello")
1694            req_id: Some(ReqId::from_string("r1".to_string())),
1695        };
1696        let back: GetBlobRet =
1697            serde_json::from_str(&serde_json::to_string(&first).unwrap()).unwrap();
1698        assert_eq!(back, first);
1699        assert!(!back.eof);
1700
1701        // 末块(eof=true)+ 省略 mime_type / req_id(可选)
1702        let last = GetBlobRet {
1703            blob_handle: "h".to_string(),
1704            mime_type: None,
1705            total_size: 10,
1706            sha256: "abc123".to_string(),
1707            chunk_offset: 5,
1708            eof: true,
1709            blob: "d29ybGQ=".to_string(),
1710            req_id: None,
1711        };
1712        let v = serde_json::to_value(&last).unwrap();
1713        assert!(v.get("mime_type").is_none());
1714        assert!(v.get("req_id").is_none());
1715        assert_eq!(v["eof"], true);
1716        let back: GetBlobRet = serde_json::from_value(v).unwrap();
1717        assert_eq!(back, last);
1718    }
1719
1720    // ── #42 CallToolResult meta / _meta 标记 ─────────────────────────────
1721
1722    fn empty_ret() -> ToolCallRet {
1723        ToolCallRet {
1724            content: None,
1725            is_error: Some(true),
1726            req_id: None,
1727            meta: None,
1728        }
1729    }
1730
1731    #[test]
1732    fn test_mark_cancelled_default_and_custom_reason() {
1733        // 默认原因 = agent_requested
1734        let mut ret = empty_ret();
1735        ret.mark_cancelled(None);
1736        assert!(ret.is_cancelled());
1737        assert_eq!(ret.cancel_reason(), Some("agent_requested"));
1738        // 结果级 meta 顶层 key 为 "meta"
1739        let v = serde_json::to_value(&ret).unwrap();
1740        assert_eq!(v["meta"]["a2c_cancelled"], true);
1741        assert_eq!(v["meta"]["a2c_cancel_reason"], "agent_requested");
1742
1743        // 自定义原因
1744        let mut ret2 = empty_ret();
1745        ret2.mark_cancelled(Some("operator_abort"));
1746        assert_eq!(ret2.cancel_reason(), Some("operator_abort"));
1747    }
1748
1749    #[test]
1750    fn test_mark_timeout() {
1751        let mut ret = empty_ret();
1752        assert!(!ret.is_timeout());
1753        ret.mark_timeout();
1754        assert!(ret.is_timeout());
1755        assert!(!ret.is_cancelled()); // 超时 ≠ 取消
1756        let v = serde_json::to_value(&ret).unwrap();
1757        assert_eq!(v["meta"]["a2c_timeout"], true);
1758    }
1759
1760    #[test]
1761    fn test_content_blob_sideband_placement_and_roundtrip() {
1762        // 二进制旁路是**子级** _meta,落在 content item,而非结果级 meta
1763        let mut item = serde_json::json!({ "type": "text", "text": "" });
1764        set_content_blob_sideband(&mut item, "blob-xyz", Some(2048), Some("deadbeef"));
1765        assert_eq!(item["_meta"]["a2c_blob_handle"], "blob-xyz");
1766        assert_eq!(item["_meta"]["a2c_total_size"], 2048);
1767        assert_eq!(item["_meta"]["a2c_sha256"], "deadbeef");
1768
1769        let sb = read_content_blob_sideband(&item).expect("应读到旁路句柄");
1770        assert_eq!(sb.blob_handle, "blob-xyz");
1771        assert_eq!(sb.total_size, Some(2048));
1772        assert_eq!(sb.sha256.as_deref(), Some("deadbeef"));
1773
1774        // 无 _meta.a2c_blob_handle → None
1775        let plain = serde_json::json!({ "type": "text", "text": "hi" });
1776        assert!(read_content_blob_sideband(&plain).is_none());
1777    }
1778
1779    #[test]
1780    fn test_result_meta_vs_child_meta_separation() {
1781        // 取消标记落结果级 meta;blob 句柄落 content item _meta —— 两层不混淆
1782        let mut item = serde_json::json!({ "type": "text", "text": "" });
1783        set_content_blob_sideband(&mut item, "h", None, None);
1784        let mut ret = ToolCallRet {
1785            content: Some(vec![item]),
1786            is_error: Some(true),
1787            req_id: None,
1788            meta: None,
1789        };
1790        ret.mark_cancelled(None);
1791
1792        let v = serde_json::to_value(&ret).unwrap();
1793        // 结果级 meta 只有取消标记,没有 blob 句柄
1794        assert_eq!(v["meta"]["a2c_cancelled"], true);
1795        assert!(v["meta"].get("a2c_blob_handle").is_none());
1796        // content item 的 _meta 只有 blob 句柄,没有取消标记
1797        assert_eq!(v["content"][0]["_meta"]["a2c_blob_handle"], "h");
1798        assert!(v["content"][0]["_meta"].get("a2c_cancelled").is_none());
1799    }
1800
1801    #[test]
1802    fn test_tool_call_ret_deserialize_external_meta() {
1803        // 反向:消费外部形态 wire JSON(producer 把标记写在结果级 meta)→ reader 正确读取
1804        let cancelled_wire = r#"{"content":[{"type":"text","text":"cancelled"}],"isError":true,"meta":{"a2c_cancelled":true,"a2c_cancel_reason":"agent_requested"}}"#;
1805        let ret: ToolCallRet = serde_json::from_str(cancelled_wire).unwrap();
1806        assert!(ret.is_cancelled());
1807        assert_eq!(ret.cancel_reason(), Some("agent_requested"));
1808        assert!(!ret.is_timeout());
1809
1810        let timeout_wire = r#"{"isError":true,"meta":{"a2c_timeout":true}}"#;
1811        let ret2: ToolCallRet = serde_json::from_str(timeout_wire).unwrap();
1812        assert!(ret2.is_timeout());
1813        assert!(!ret2.is_cancelled());
1814        assert_eq!(ret2.cancel_reason(), None);
1815
1816        // 无 meta → 全 false(不 panic)
1817        let plain: ToolCallRet = serde_json::from_str(r#"{"isError":false}"#).unwrap();
1818        assert!(!plain.is_cancelled() && !plain.is_timeout());
1819    }
1820
1821    #[test]
1822    fn test_agent_call_data_cancel_carrier_shape() {
1823        // SRV-03 (#53):server:tool_call_cancel / notify:tool_call_cancel 复用 AgentCallData。
1824        // 协议 0.2.2 规定该载体**仅** {agent, req_id},且 req_id MUST==被取消的原 tool_call req_id,
1825        // **不含** computer 字段(唯一定位在途调用靠 req_id,不靠 computer)。
1826        let cancel = AgentCallData {
1827            agent: "agent-x".to_string(),
1828            req_id: ReqId::from_string("rid_tool".to_string()),
1829        };
1830        let v = serde_json::to_value(&cancel).unwrap();
1831        assert_eq!(v["agent"], "agent-x");
1832        assert_eq!(v["req_id"], "rid_tool");
1833        assert!(
1834            v.get("computer").is_none(),
1835            "取消载体 MUST NOT 含 computer 字段,实得: {v}"
1836        );
1837        // 恰两个键,杜绝未来误加字段
1838        assert_eq!(
1839            v.as_object().map(|m| m.len()),
1840            Some(2),
1841            "AgentCallData 在线形态应恰为 {{agent, req_id}}"
1842        );
1843        // 往返
1844        assert_eq!(serde_json::from_value::<AgentCallData>(v).unwrap(), cancel);
1845    }
1846
1847    // ── SMCP-02 get_resources (#26) ────────────────────────────────────
1848
1849    #[test]
1850    fn get_resources_event_const_exact() {
1851        assert_eq!(GET_RESOURCES_EVENT, "client:get_resources");
1852        assert_eq!(GET_RESOURCES_EVENT, events::CLIENT_GET_RESOURCES);
1853    }
1854
1855    #[test]
1856    fn get_resources_req_round_trip_with_and_without_cursor() {
1857        let base = AgentCallData {
1858            agent: "agent-x".to_string(),
1859            req_id: ReqId::from_string("rid-1".to_string()),
1860        };
1861        // 无 cursor → 不序列化 cursor 键;flatten base 平铺 agent/req_id 在顶层。
1862        let req = GetResourcesReq {
1863            base: base.clone(),
1864            computer: "comp-1".to_string(),
1865            mcp_server: "srv-1".to_string(),
1866            cursor: None,
1867        };
1868        let v = serde_json::to_value(&req).unwrap();
1869        assert_eq!(v["agent"], "agent-x");
1870        assert_eq!(v["req_id"], "rid-1");
1871        assert_eq!(v["computer"], "comp-1");
1872        assert_eq!(v["mcp_server"], "srv-1");
1873        assert!(v.get("cursor").is_none());
1874        assert_eq!(serde_json::from_value::<GetResourcesReq>(v).unwrap(), req);
1875
1876        // 有 cursor → 出现在线格式。
1877        let req2 = GetResourcesReq {
1878            base,
1879            computer: "comp-1".to_string(),
1880            mcp_server: "srv-1".to_string(),
1881            cursor: Some("page-2".to_string()),
1882        };
1883        let v2 = serde_json::to_value(&req2).unwrap();
1884        assert_eq!(v2["cursor"], "page-2");
1885        assert_eq!(serde_json::from_value::<GetResourcesReq>(v2).unwrap(), req2);
1886    }
1887
1888    #[test]
1889    fn get_resources_ret_empty_and_populated() {
1890        // 空 resources → 序列化为 `[]`,next_cursor 省略。
1891        let empty = GetResourcesRet::default();
1892        let v = serde_json::to_value(&empty).unwrap();
1893        assert_eq!(v["resources"], serde_json::json!([]));
1894        assert!(v.get("next_cursor").is_none());
1895
1896        // 含资源 + priority 边界 0.0/1.0 + audience + _meta + next_cursor。
1897        let ret = GetResourcesRet {
1898            resources: vec![
1899                A2CResource {
1900                    uri: Some("window://main".to_string()),
1901                    name: Some("main".to_string()),
1902                    annotations: Some(ResourceAnnotations {
1903                        audience: Some(vec![ResourceAudience::User, ResourceAudience::Assistant]),
1904                        priority: Some(0.0),
1905                        last_modified: Some("2026-06-01T00:00:00Z".to_string()),
1906                    }),
1907                    meta: Some(serde_json::json!({ "fullscreen": true })),
1908                    ..Default::default()
1909                },
1910                A2CResource {
1911                    uri: Some("window://side".to_string()),
1912                    annotations: Some(ResourceAnnotations {
1913                        priority: Some(1.0),
1914                        ..Default::default()
1915                    }),
1916                    ..Default::default()
1917                },
1918            ],
1919            next_cursor: Some("next".to_string()),
1920            req_id: Some(ReqId::from_string("rid-2".to_string())),
1921        };
1922        let v = serde_json::to_value(&ret).unwrap();
1923        // _meta 线字段名重命名生效。
1924        assert_eq!(v["resources"][0]["_meta"]["fullscreen"], true);
1925        assert!(v["resources"][0].get("meta").is_none());
1926        assert_eq!(v["resources"][0]["annotations"]["audience"][0], "user");
1927        assert_eq!(v["resources"][0]["annotations"]["priority"], 0.0);
1928        assert_eq!(v["resources"][1]["annotations"]["priority"], 1.0);
1929        assert_eq!(v["next_cursor"], "next");
1930        let back: GetResourcesRet = serde_json::from_value(v).unwrap();
1931        assert_eq!(back, ret);
1932    }
1933
1934    // ── SMCP-04 SKILL (#35) ────────────────────────────────────────────
1935
1936    #[test]
1937    fn skill_event_consts_exact() {
1938        assert_eq!(GET_SKILLS_EVENT, "client:get_skills");
1939        assert_eq!(GET_SKILL_EVENT, "client:get_skill");
1940        assert_eq!(UPDATE_SKILLS_EVENT, "server:update_skills");
1941        assert_eq!(UPDATE_SKILLS_NOTIFICATION, "notify:update_skills");
1942        assert_eq!(GET_SKILLS_EVENT, events::CLIENT_GET_SKILLS);
1943        assert_eq!(GET_SKILL_EVENT, events::CLIENT_GET_SKILL);
1944        assert_eq!(UPDATE_SKILLS_EVENT, events::SERVER_UPDATE_SKILLS);
1945        assert_eq!(UPDATE_SKILLS_NOTIFICATION, events::NOTIFY_UPDATE_SKILLS);
1946    }
1947
1948    #[test]
1949    fn skill_ref_three_sources_round_trip() {
1950        // 必选 4 字段恒存;可选字段 None 时不序列化。
1951        let user = A2CSkillRef {
1952            name: "my-helper".to_string(),
1953            source: "user".to_string(),
1954            uri: None,
1955            path: "/home/u/.skills/my-helper".to_string(),
1956            description: "a helper".to_string(),
1957            license: None,
1958            compatibility: None,
1959            allowed_tools: None,
1960            version: None,
1961            skill_metadata: None,
1962        };
1963        let v = serde_json::to_value(&user).unwrap();
1964        assert_eq!(v["name"], "my-helper");
1965        assert_eq!(v["source"], "user");
1966        assert!(v.get("uri").is_none());
1967        assert!(v.get("version").is_none());
1968        assert_eq!(serde_json::from_value::<A2CSkillRef>(v).unwrap(), user);
1969
1970        let marketplace = A2CSkillRef {
1971            name: "acme-audit:audit".to_string(),
1972            source: "marketplace:acme-skills".to_string(),
1973            ..user.clone()
1974        };
1975        assert_eq!(
1976            serde_json::from_value::<A2CSkillRef>(serde_json::to_value(&marketplace).unwrap())
1977                .unwrap(),
1978            marketplace
1979        );
1980
1981        // mcp 源 + 全量可选字段。
1982        let mcp = A2CSkillRef {
1983            name: "mcp:tfrobot-tools:code-review".to_string(),
1984            source: "mcp:tfrobot-tools".to_string(),
1985            uri: Some("skill://tfrobot-tools/code-review".to_string()),
1986            path: "/tmp/staging/code-review".to_string(),
1987            description: "review".to_string(),
1988            license: Some("MIT".to_string()),
1989            compatibility: Some(">=0.2".to_string()),
1990            allowed_tools: Some(vec!["read".to_string(), "grep".to_string()]),
1991            version: Some("1.2.0".to_string()),
1992            skill_metadata: Some(serde_json::json!({ "x": 1 })),
1993        };
1994        let v = serde_json::to_value(&mcp).unwrap();
1995        assert_eq!(v["uri"], "skill://tfrobot-tools/code-review");
1996        assert_eq!(v["allowed_tools"][1], "grep");
1997        assert_eq!(v["skill_metadata"]["x"], 1);
1998        assert_eq!(serde_json::from_value::<A2CSkillRef>(v).unwrap(), mcp);
1999    }
2000
2001    #[test]
2002    fn get_skills_req_ret_round_trip() {
2003        let req = GetSkillsReq {
2004            base: AgentCallData {
2005                agent: "a".to_string(),
2006                req_id: ReqId::from_string("r".to_string()),
2007            },
2008            computer: "c".to_string(),
2009        };
2010        let v = serde_json::to_value(&req).unwrap();
2011        assert_eq!(v["agent"], "a");
2012        assert_eq!(v["computer"], "c");
2013        assert_eq!(serde_json::from_value::<GetSkillsReq>(v).unwrap(), req);
2014
2015        let ret = GetSkillsRet::default();
2016        let v = serde_json::to_value(&ret).unwrap();
2017        assert_eq!(v["skills"], serde_json::json!([]));
2018        assert!(v.get("req_id").is_none());
2019        assert_eq!(serde_json::from_value::<GetSkillsRet>(v).unwrap(), ret);
2020    }
2021
2022    #[test]
2023    fn get_skill_req_rel_path_optional() {
2024        let base = AgentCallData {
2025            agent: "a".to_string(),
2026            req_id: ReqId::from_string("r".to_string()),
2027        };
2028        let req = GetSkillReq {
2029            base: base.clone(),
2030            computer: "c".to_string(),
2031            name: "my-helper".to_string(),
2032            rel_path: None,
2033        };
2034        let v = serde_json::to_value(&req).unwrap();
2035        assert!(v.get("rel_path").is_none());
2036        assert_eq!(serde_json::from_value::<GetSkillReq>(v).unwrap(), req);
2037
2038        let req2 = GetSkillReq {
2039            base,
2040            computer: "c".to_string(),
2041            name: "my-helper".to_string(),
2042            rel_path: Some("docs/usage.md".to_string()),
2043        };
2044        let v2 = serde_json::to_value(&req2).unwrap();
2045        assert_eq!(v2["rel_path"], "docs/usage.md");
2046        assert_eq!(serde_json::from_value::<GetSkillReq>(v2).unwrap(), req2);
2047    }
2048
2049    #[test]
2050    fn get_skill_ret_body_blob_handle_exclusive() {
2051        // body 分支 → Inline。
2052        let inline = GetSkillRet {
2053            name: Some("my-helper".to_string()),
2054            rel_path: Some("SKILL.md".to_string()),
2055            mime_type: Some("text/markdown".to_string()),
2056            total_size: Some(42),
2057            sha256: Some("abc".to_string()),
2058            body: Some("# Hello".to_string()),
2059            ..Default::default()
2060        };
2061        assert!(inline.is_exclusive());
2062        assert_eq!(inline.resource().unwrap(), SkillResource::Inline("# Hello"));
2063        let back: GetSkillRet =
2064            serde_json::from_value(serde_json::to_value(&inline).unwrap()).unwrap();
2065        assert_eq!(back, inline);
2066
2067        // blob_handle 分支 → Blob。
2068        let blob = GetSkillRet {
2069            name: Some("big".to_string()),
2070            blob_handle: Some("opaque-handle".to_string()),
2071            ..Default::default()
2072        };
2073        assert!(blob.is_exclusive());
2074        assert_eq!(
2075            blob.resource().unwrap(),
2076            SkillResource::Blob("opaque-handle")
2077        );
2078
2079        // 二者并存 → BothPresent。
2080        let both = GetSkillRet {
2081            body: Some("x".to_string()),
2082            blob_handle: Some("h".to_string()),
2083            ..Default::default()
2084        };
2085        assert!(!both.is_exclusive());
2086        assert_eq!(both.resource().unwrap_err(), SkillRetError::BothPresent);
2087
2088        // 二者皆缺 → NeitherPresent。
2089        let neither = GetSkillRet::default();
2090        assert!(!neither.is_exclusive());
2091        assert_eq!(
2092            neither.resource().unwrap_err(),
2093            SkillRetError::NeitherPresent
2094        );
2095    }
2096}