MCP、exec 和沙盒线路模型
这一阶段像系统的“接线图”,不是亲自干活,而是规定各方怎么说话。exec-server 的连接入口把 WebSocket、认证连接、标准输入输出这些路统一包起来;进程编号用专门的小盒子,避免拿错。协议文件规定启动进程、读写文件、报错等消息长什么样;exec 事件规定运行过程怎么对外汇报。沙盒和提权部分则约好被拦截命令、环境变量、输入输出和退出结果怎么传,让普通进程、提权进程和外部客户端能安全配合。
Exec server 连接基础
这些文件建立 exec-server 客户端和服务器使用的共享标识符、连接设置和核心 JSON-RPC 架构。
exec-server/src/client_api.rs源码 ↗
exec-server 可以理解成一个在别处帮忙执行任务、发网络请求的服务。客户端要连接它,可能走普通 WebSocket,也可能走带身份校验的 Noise 通道(Noise 是一种加密握手协议,用来确认对方身份并保护通信),还可能直接启动一个本地命令后通过标准输入输出通信。这个文件就像一张“连接申请表”的模板集合:每种连接方式需要哪些信息、默认超时时间是多少、是否要恢复旧会话,都在这里说清楚。它还定义了 HttpClient 这个能力接口:调用方只说“我要发 HTTP 请求”,不用知道请求最后是通过哪种连接送出去的。比较重要的是,NoiseRendezvousConnectBundle 被明确当成“一次性整包材料”,里面的网址授权、执行器身份、公钥等不能和别次连接混用,否则安全边界会乱。
ExecServerTransportParams::fmt111–135 ↗
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
作用:这个函数决定 ExecServerTransportParams 被打印成调试文字时长什么样。它方便开发者看日志、排查问题,同时会避免把 Noise 连接里的敏感细节全打印出来。
数据流:进去的是一个连接参数对象和一个用于写调试文本的输出器 → 它先判断当前是哪种连接方式:WebSocket 就打印网址和超时时间,标准输入输出命令就打印命令和初始化超时,Noise 连接只打印一个不展开的名字 → 出来的是格式化成功或失败的结果,原对象本身不被修改。
调用关系:它是在 Rust 的调试打印流程里被自动用到的,比如代码写日志或格式化这个枚举时会触发它。函数内部把具体排版工作交给标准库的 debug_struct 这类调试构造工具来完成。
调用图:外部调用 1 个(debug_struct)。
ExecServerTransportParams::websocket_url139–145 ↗
fn websocket_url(websocket_url: String) -> Self
作用:这个函数是一个省事的构造器:只给它一个 WebSocket 地址,它就帮你生成一份带默认超时时间的远程连接参数。有人不想每次手写默认值时会用它。
数据流:进去的是一个 WebSocket 地址字符串 → 它把这个地址和文件里定义好的默认连接超时、默认初始化超时放在一起 → 出来的是一个 ExecServerTransportParams::WebSocketUrl 连接参数对象,没有额外副作用。
调用关系:它会被 remote_inner 这类建立远程连接的流程调用,用来快速准备 WebSocket 连接参数;测试 remote_file_system_sends_path_and_sandbox_cwd_uris_without_native_conversion 也会用它来搭建测试场景。它不继续调用别的项目函数,只是把输入包装成统一的连接参数。
调用图:被 2 处调用(remote_inner, remote_file_system_sends_path_and_sandbox_cwd_uris_without_native_conversion)。
exec-server/src/process_id.rs源码 ↗
ProcessId 本质上包着一个 String,但它给这段文字加了明确身份:这不是随便一串字,而是用来标记某个进程的编号。这样代码里看到 ProcessId,就知道这里要的是进程 ID,不容易误传用户名、路径之类的普通字符串。它还支持序列化和反序列化,也就是能方便地变成 JSON 等格式传出去,再读回来。文件里还做了很多“像字符串一样使用”的适配:可以拿到里面的 &str,可以打印,可以从 String、&str、&String 创建,也可以再变回 String。可以把它想成给钥匙贴了标签:钥匙还是钥匙,但标签告诉你这是哪扇门的钥匙。
ProcessId::new13–15 ↗
fn new(value: impl Into<String>) -> Self
作用:创建一个新的进程编号。调用者可以传入能变成字符串的东西,比如 String 或字符串字面量,它会统一包成 ProcessId。
数据流:进去的是一段可以转成字符串的值 → 函数调用 into 把它变成真正的 String → 出来的是一个带类型标签的 ProcessId,里面保存这段编号文字。
调用关系:它是最直接的造 ID 入口之一。比如测试里的 noise_environment_refreshes_bundle_for_each_connection_attempt 会用它准备进程编号,后续代码就能明确知道这个值代表进程,而不是普通文本。
调用图:被 1 处调用(noise_environment_refreshes_bundle_for_each_connection_attempt);外部调用 1 个(into)。
ProcessId::as_str17–19 ↗
fn as_str(&self) -> &str
作用:把 ProcessId 里的编号作为只读字符串借出来。别人想看 ID 内容,但不想拿走它时会用这个函数。
数据流:进去的是一个已有的 ProcessId → 函数不复制、不修改,只取出内部字符串的只读视图 → 出来的是 &str,也就是一段借用的字符串切片。
调用关系:它是几个适配函数共用的小出口。ProcessId::deref、ProcessId::borrow 和 ProcessId::as_ref 都把活交给它,这样所有“把 ProcessId 当字符串看”的行为都走同一条路。
ProcessId::into_inner21–23 ↗
fn into_inner(self) -> String
作用:把 ProcessId 拆开,拿出里面真正的 String。当调用者不再需要这个外壳,只想要原始字符串时会用它。
数据流:进去的是一个被消费掉的 ProcessId → 函数取走它内部保存的字符串 → 出来的是 String,原来的 ProcessId 不能再继续使用。
调用关系:它是从专用 ID 类型回到普通字符串的出口之一。和 String::from 类似,都是在需要交给只接受普通字符串的代码时使用。
ProcessId::deref29–31 ↗
fn deref(&self) -> &Self::Target
作用:让 ProcessId 在很多地方能像 str 一样被使用。Deref 可以理解成“自动借出里面的东西”的约定。
数据流:进去的是一个 ProcessId 的引用 → 函数调用 ProcessId::as_str 取出里面的只读字符串 → 出来的是 str 的引用,没有复制,也没有修改原对象。
调用关系:它依赖 ProcessId::as_str。有了它,很多只需要字符串引用的场景可以更自然地接收 ProcessId,不用每次手写取内部字符串。
调用图:调用 1 个内部函数(as_str)。
ProcessId::borrow35–37 ↗
fn borrow(&self) -> &str
作用:让 ProcessId 可以按字符串来“借用”。这常用于集合查找,比如用字符串去查一个以 ProcessId 为键的表。
数据流:进去的是一个 ProcessId 的引用 → 函数调用 ProcessId::as_str 拿到内部字符串的只读视图 → 出来的是 &str,方便别的代码用字符串方式比较或查找。
调用关系:它也把核心工作交给 ProcessId::as_str。它存在的意义是让 ProcessId 和标准库的查找、比较规则配合得更顺。
调用图:调用 1 个内部函数(as_str)。
ProcessId::as_ref41–43 ↗
fn as_ref(&self) -> &str
作用:把 ProcessId 转成字符串引用,供那些接受 AsRef<str> 的通用代码使用。AsRef 可以理解成“我能临时看成某种引用”。
数据流:进去的是一个 ProcessId 的引用 → 函数调用 ProcessId::as_str → 出来的是内部编号的 &str,原对象保持不变。
调用关系:它和 deref、borrow 一样,都是围绕 ProcessId::as_str 做适配。这样外部工具函数只要会处理字符串引用,也能处理 ProcessId。
调用图:调用 1 个内部函数(as_str)。
ProcessId::fmt47–49 ↗
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
作用:规定 ProcessId 被打印出来时长什么样。这里就是直接打印里面那段编号字符串,不额外加括号或前缀。
数据流:进去的是 ProcessId 和一个格式化输出器 → 函数把内部字符串交给输出器写入 → 出来的是格式化结果,通常用于日志、错误信息或用户可见文本。
调用关系:它是 Display 这个打印约定的一部分。凡是代码里用 {} 打印 ProcessId,都会走到这里。
ProcessId::from65–67 ↗
fn from(value: &String) -> Self
作用:把一个已有字符串值转换成 ProcessId。这一版接收的是字符串引用一类的输入,用来方便地从现成文本创建进程编号。
数据流:进去的是一段已有的字符串内容 → 函数把内容复制或接收进 ProcessId 的内部 String → 出来的是新的 ProcessId,用于后续按进程编号传递。
调用关系:很多测试和辅助代码会通过 From 这种标准转换方式快速造出进程编号,例如事件顺序、断线处理、会话唤醒、默认执行环境、测试进程启动等场景。它让这些地方不用关心包装细节,只要说“把这段文字当成进程 ID”。
调用图:被 17 处调用(process_events_are_delivered_in_seq_order_when_notifications_are_reordered, transport_disconnect_fails_sessions_and_rejects_new_sessions, wake_notifications_do_not_block_other_sessions, default_environment_has_ready_local_executor, spawn_test_process, test_exec_params, exec_params_with_argv, long_poll_read_fails_after_session_resume, output_and_exit_are_retained_after_notification_receiver_closes, terminate_reports_false_after_process_exit (+7 more))。
String::from71–73 ↗
fn from(value: ProcessId) -> Self
作用:把 ProcessId 转回普通 String。当某些接口只认识字符串、不认识专用的进程编号类型时会用它。
数据流:进去的是一个 ProcessId,并且这个值会被消费掉 → 函数取出里面的字符串 → 出来的是普通 String,原来的 ProcessId 外壳消失。
调用关系:它是 From<ProcessId> for String 这个转换约定的实现,和 ProcessId::into_inner 作用相近。它让标准的 String::from(process_id) 写法可以工作,方便接入只处理字符串的代码。
exec-server/src/protocol.rs源码 ↗
这个文件的核心作用是把网络上传来传去的 JSON-RPC 消息固定下来。JSON-RPC 可以理解成“用 JSON 写的远程调用格式”:客户端发一个方法名和参数,服务器按约定执行。这里先列出所有方法名,比如启动进程、读取进程输出、读文件、写文件、发 HTTP 请求。然后定义每种请求和响应长什么样。比如启动进程要带命令参数、工作目录、环境变量;读文件要带路径;HTTP 请求要带方法、网址、请求头和可选正文。因为 JSON 不能直接安全表达任意二进制数据,所以文件里用 ByteChunk 把字节转成 Base64 字符串。Base64 就是把原始字节包装成普通文本,方便放进 JSON。没有这个文件,客户端和服务器就很容易“鸡同鸭讲”:字段名不一致、路径格式不一致、二进制数据传坏,都会导致执行、文件访问或 HTTP 转发失败。
ByteChunk::into_inner44–46 ↗
fn into_inner(self) -> Vec<u8>
作用:把 ByteChunk 这个包装壳拆开,拿回里面真正的字节数组。别人收到协议里的二进制内容后,如果要实际使用这些字节,就会用它。
数据流:进去的是一个 ByteChunk,也就是被包装起来的一段字节 → 它不修改字节,只是把外面的包装去掉 → 出来的是 Vec<u8>,也就是 Rust 里的字节列表,同时原来的 ByteChunk 被消耗掉。
调用关系:ByteChunk 在这个协议里用来承载进程输出、标准输入、文件块、HTTP 正文等二进制内容。处理这些内容的上层代码拿到 ByteChunk 后,可以调用 ByteChunk::into_inner 进入真正读写字节的阶段。
ByteChunk::from50–52 ↗
fn from(value: Vec<u8>) -> Self
作用:把普通字节数组包成 ByteChunk,方便按协议格式发送出去。比如进程输出了一段原始字节,发送前就可以先包一下。
数据流:进去的是 Vec<u8> 原始字节 → 它把这些字节放进 ByteChunk 这个协议包装类型里 → 出来的是 ByteChunk,之后序列化成 JSON 时会自动按 Base64 文本表示。
调用关系:这是 Rust 的 From 转换实现,常用于需要把原始字节塞进协议消息的时候。它和 base64_bytes::serialize 配合:先包装成 ByteChunk,真正写成 JSON 时再转成 Base64 字符串。
base64_bytes::serialize477–482 ↗
fn serialize(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
作用:把一段原始字节编码成 Base64 字符串,好让它能安全放进 JSON。JSON 适合传文字,不适合直接传任意二进制,所以需要这一步。
数据流:进去的是字节切片和一个序列化器,序列化器可以理解成“把数据写成 JSON 的工具” → 它用标准 Base64 规则把字节变成文本 → 最后把这段文本交给 serializer.serialize_str(ext) 写出去,结果是 JSON 里的一个字符串。
调用关系:它是 ByteChunk 的自定义序列化规则。凡是协议结构里有 ByteChunk 字段,比如进程输出块、文件读取块、HTTP 请求体或响应体,写成 JSON 时都会走到这里。它把底层字节交给外部 serde 序列化机制完成最终输出。
调用图:外部调用 1 个(serialize_str)。
base64_bytes::deserialize484–492 ↗
fn deserialize(deserializer: D) -> Result<Vec<u8>, D::Error>
作用:把 JSON 里的 Base64 字符串还原成原始字节。这样接收方看到的不是一串编码文字,而是可以真正写入进程、文件或网络的字节。
数据流:进去的是一个反序列化器,反序列化器可以理解成“从 JSON 读数据的工具” → 它先通过 String::deserialize(ext) 读出字符串 → 再用标准 Base64 规则解码 → 成功时出来 Vec<u8> 字节列表;如果字符串不是合法 Base64,就返回一个反序列化错误。
调用关系:它是 ByteChunk 的自定义反序列化规则。客户端发来带 ByteChunk 的 JSON 时,比如 process/write 的输入内容或 HTTP 请求体,会通过这里变回字节。它把读字符串的工作交给 serde,再自己负责 Base64 解码。
调用图:外部调用 1 个(deserialize)。
tests::filesystem_protocol_accepts_legacy_absolute_paths_and_serializes_path_uris505–537 ↗
fn filesystem_protocol_accepts_legacy_absolute_paths_and_serializes_path_uris()
作用:这个测试确认文件系统协议既能读懂旧格式的普通绝对路径,也会在重新输出时写成新的 URI 路径格式。这样老客户端不会突然坏掉,新协议也能保持统一。
数据流:进去的是测试里临时构造的当前目录、一个旧式文件路径字符串,以及一个带工作目录的沙箱配置 → 测试把它们拼成 JSON,再用 from_value(ext) 读成 FsReadFileParams → 接着和预期结果比较,并把结果用 to_value(ext) 写回 JSON → 最后确认读进来时兼容旧格式,写出去时变成标准 PathUri 字符串。
调用关系:它位于测试模块里,不参与正常运行。它调用 current_dir(ext) 取得当前目录,用 from_permission_profile_with_cwd 建沙箱,用 default 和 from_path 准备路径数据,再通过 serde_json 的 from_value(ext)、to_value(ext)、json!(ext) 和 assert_eq!(ext) 检查协议格式是否符合预期。
调用图:调用 3 个内部函数(from_permission_profile_with_cwd, default, from_path);外部调用 5 个(assert_eq!, from_value, json!, to_value, current_dir)。
tests::http_request_timeout_treats_omitted_and_null_as_no_timeout540–577 ↗
fn http_request_timeout_treats_omitted_and_null_as_no_timeout()
作用:这个测试确认 HTTP 请求里的 timeoutMs 如果没写,或者明确写成 null,都表示“不设置超时”。只有写了数字,才表示具体的毫秒超时时间。
数据流:进去的是三份测试 JSON:一份不带 timeoutMs,一份 timeoutMs 是 null,一份 timeoutMs 是 1234 → 测试分别把它们用 from_value(ext) 读成 HttpRequestParams → 然后检查 request_id 和 timeout_ms → 出来的结论是前两种 timeout_ms 都是 None,第三种是 Some(1234)。
调用关系:它也是测试代码,不在正式服务流程中执行。它专门保护 HttpRequestParams 的反序列化行为,避免以后有人改字段规则时破坏“省略和 null 都等于无超时”这个约定;断言由 assert_eq!(ext) 完成,测试 JSON 由 json!(ext) 构造。
调用图:外部调用 3 个(assert_eq!, from_value, json!)。
执行事件流
此文件定义 exec 运行期间发出的结构化 JSONL 事件模型,供下游使用者使用。
exec/src/exec_events.rs源码 ↗
codex exec 运行时会不断产生日志式事件,比如新会话开始、用户这一轮提问开始、助手回答完成、执行了一条命令、改了文件、调用了工具、发生错误等。这个文件的作用,就是把这些事件都定义成固定的数据类型,方便程序自己生成,也方便外部工具读取。这里的 JSONL 是“一行一个 JSON”的日志格式;serde 用来把 Rust 数据和 JSON 互相转换;ts-rs 用来导出 TypeScript 类型,让前端或其他 JavaScript 工具也能按同一套规则理解这些事件。最核心的是 ThreadEvent,它把所有顶层事件分门别类。ThreadItem 则表示一条具体内容,比如助手消息、命令执行、文件变更、网页搜索、待办清单等。很多条目都有状态,例如 in_progress、completed、failed,外部读者就能像看快递进度一样知道一件事正在做、做完了,还是失败了。没有这个文件,各个模块可能各说各话,事件格式会乱,外部工具也很难稳定解析执行过程。
权限与沙盒 IPC
这些文件描述用于 Unix shell 提权和 Windows 提权沙盒消息交换的线缆契约。
shell-escalation/src/unix/escalate_protocol.rs源码 ↗
这个文件像一张“通关单”的格式说明。Unix 下有些命令会被沙箱拦住,需要判断:直接运行、换一种权限运行,还是拒绝运行。这里定义了客户端发给服务器的 EscalateRequest,里面有要执行的程序路径、参数、工作目录和环境变量;也定义了服务器回给客户端的 EscalateResponse,告诉它 Run、Escalate 或 Deny。文件里的常量是环境变量名,用来让 exec 包装器找到继承来的通信 socket,或让补丁过的 shell 知道该用哪个包装器。它还定义了 SuperExecMessage 和 SuperExecResult,用来转交打开的文件描述符(可以理解成已经打开的文件或管道的“号码牌”)并拿回退出码。整体上它不真正执行命令,而是保证各个进程之间传话时格式一致,避免“你说的升级”和“我理解的升级”不是一回事。
EscalationDecision::run55–57 ↗
fn run() -> Self
作用:这个函数快速造出一个“直接运行”的决定。调用方不用手写枚举细节,只要表达“别升级、照常跑”就行。
数据流:进去没有参数 → 它创建一个 EscalationDecision 的 Run 版本 → 出来的是一个表示“命令可以直接执行”的决定,不改动外部状态。
调用关系:当 process_decision、determine_action 或会话相关流程判断命令不需要额外权限时,会用它生成统一的结果。后续代码再根据这个结果告诉客户端直接运行命令,而不是把执行交给升级通道。
调用图:被 5 处调用(process_decision, determine_action, exec_closes_parent_socket_after_shell_spawn, handle_escalate_session_respects_run_in_sandbox_decision, start_session_exposes_wrapper_env_overlay)。
EscalationDecision::escalate59–61 ↗
fn escalate(execution: EscalationExecution) -> Self
作用:这个函数快速造出一个“需要升级执行”的决定,并带上具体怎么升级。比如完全脱离沙箱运行,或按本轮指定的权限运行。
数据流:进去一个 EscalationExecution,说明升级执行的方式 → 它把这个方式包进 EscalationDecision::Escalate → 出来的是一个表示“这条命令要交给更高权限流程处理”的决定,不直接执行命令。
调用关系:process_decision 以及处理升级会话的代码会在确认需要升级时调用它。它把“升级方式”打包好,后面的流程再据此把命令交给服务器或执行器,并可能传递权限配置、文件描述符等信息。
调用图:被 5 处调用(process_decision, dropping_session_aborts_intercept_workers_and_kills_spawned_child, handle_escalate_session_accepts_received_fds_that_overlap_destinations, handle_escalate_session_executes_escalated_command, handle_escalate_session_passes_permissions_to_executor);外部调用 1 个(Escalate)。
EscalationDecision::deny63–65 ↗
fn deny(reason: Option<String>) -> Self
作用:这个函数快速造出一个“拒绝执行”的决定。它还可以带上一句原因,方便用户或上层流程知道为什么不能跑。
数据流:进去一个可选的原因文字 → 它创建 EscalationDecision::Deny,并把原因放进去 → 出来的是一个表示“不要执行这条命令”的决定,不启动任何程序。
调用关系:process_decision 在判断命令不该被执行时会调用它。后续流程会把这个拒绝结果转换成给客户端看的响应,让客户端停止执行,并在有原因时把原因传回去。
调用图:被 1 处调用(process_decision)。
windows-sandbox-rs/src/elevated/ipc_framed.rs源码 ↗
这个文件解决的是“两个进程通过一条字节流通信时,怎么知道一条消息从哪里开始、到哪里结束”的问题。父进程像调度员,提权运行器像真正干活的人;它们需要传命令、标准输入、标准输出、错误、退出码等信息。这里先定义了一套 JSON 消息格式,也就是双方都看得懂的“表格”。因为管道里只能传字节,文件又规定每条 JSON 前面先放 4 个字节的长度,好比快递箱外面写着“里面有多大”。读的一方先看长度,再取对应大小的数据。输出和输入里的原始字节会用 Base64(一种把任意字节变成普通文本的编码)包起来,避免 JSON 不能直接表示某些字节。它还限制单条消息最大 8MB,防止坏数据或错误导致一次性占太多内存。
encode_bytes129–131 ↗
fn encode_bytes(data: &[u8]) -> String
作用:把一段原始字节变成 Base64 字符串,方便塞进 JSON 消息里传输。有人要发送标准输入或标准输出内容时会用它。
数据流:进去的是一串字节,比如命令输出的 hello → 它用 Base64 规则把这些字节转成普通文本 → 出来的是一个字符串,可以安全放进 IPC 消息的字段里。
调用关系:它是写消息前的小工具。测试里的 tests::framed_round_trip 会用它把输出内容编码后放进 Message::Output,真实流程里同类消息也会靠这种格式传递二进制数据。
调用图:被 1 处调用(framed_round_trip)。
decode_bytes134–136 ↗
fn decode_bytes(data: &str) -> Result<Vec<u8>>
作用:把 Base64 字符串还原成原始字节。接收方拿到输入或输出消息后,需要用它把文本重新变回真正的数据。
数据流:进去的是消息里的 Base64 文本 → 它尝试按 Base64 解码 → 成功时出来原始字节;如果文本格式不对,就返回错误,不会假装成功。
调用关系:它是读消息后的还原工具。tests::framed_round_trip 用它检查刚才编码的 hello 是否能还原;runner_stdin_writer_sends_close_stdin_after_input_eof 这类流程也会用它处理传来的标准输入数据。
调用图:被 2 处调用(framed_round_trip, runner_stdin_writer_sends_close_stdin_after_input_eof)。
write_frame139–149 ↗
fn write_frame(mut writer: W, msg: &FramedMessage) -> Result<()>
作用:把一条 FramedMessage 写进字节流里,并在前面加上长度。这样接收方不会把两条消息粘在一起,也不会只读到半条消息。
数据流:进去的是一个可写的目标,比如管道或内存缓冲区,以及一条消息 → 它先把消息转成 JSON 字节,检查是否超过 8MB,再写入 4 字节长度、写入正文、最后刷新 → 出来是写入成功或失败的结果;写入目标会多出一条完整消息。
调用关系:它是发送端的关键出口。send_spawn_request 会用它把启动请求发给提权运行器;tests::framed_round_trip 用它验证写出去的帧能被 read_frame 正确读回来。它内部依赖 JSON 序列化、write_all 写满数据、flush 确保数据推出去。
调用图:被 2 处调用(framed_round_trip, send_spawn_request);外部调用 4 个(flush, write_all, bail!, to_vec)。
read_frame152–167 ↗
fn read_frame(mut reader: R) -> Result<Option<FramedMessage>>
作用:从字节流里读出一条完整的消息。它先读长度,再按长度读正文,避免读多或读少。
数据流:进去的是一个可读来源,比如管道或内存缓冲区 → 它先尝试读 4 字节长度;如果一开始就到结尾,返回没有消息;如果有长度,就检查是否超过 8MB,再读取对应大小的 JSON 并解析 → 出来是一条 FramedMessage,或者表示正常结束的 None,或者错误。
调用关系:它是接收端的关键入口。read_spawn_ready 用它等运行器回报“子进程已启动”;wait_for_frame_count 用它统计收到的消息;tests::framed_round_trip 用它读回 write_frame 写出的内容。它和 write_frame 是一对,一个打包发送,一个拆包接收。
调用图:被 3 处调用(framed_round_trip, read_spawn_ready, wait_for_frame_count);外部调用 5 个(read_exact, bail!, from_slice, from_le_bytes, vec!)。
tests::framed_round_trip175–197 ↗
fn framed_round_trip()
作用:这个测试确认“写一条消息再读回来”不会变形。它验证分帧、JSON 编码、Base64 编码这几件事能连起来正常工作。
数据流:它先造一条 stdout 输出消息,里面的 hello 会被 encode_bytes 编成 Base64 → 再用 write_frame 写到内存缓冲区 → 接着用 read_frame 从同一块数据读回来 → 最后检查版本、消息类型、输出流和 decode_bytes 还原后的内容都正确。
调用关系:它是这个通信格式的基础验收测试。它同时覆盖 encode_bytes、decode_bytes、write_frame 和 read_frame,像一次小型演练:发送方打包,接收方拆包,最后确认内容没有丢也没有串。
调用图:调用 4 个内部函数(decode_bytes, encode_bytes, read_frame, write_frame);外部调用 3 个(new, assert_eq!, panic!)。
tests::spawn_request_serializes_permission_profile200–238 ↗
fn spawn_request_serializes_permission_profile()
作用:这个测试确认启动请求里的权限配置能被正确写成 JSON,再读回来也还是同一份意思。这样父进程和提权运行器才不会因为权限字段理解不同而出错。
数据流:它先构造一个 SpawnRequest,里面包括命令、工作目录、环境、权限配置、工作区路径等 → 把整条消息转成 JSON 值,检查字段名和权限类型是否符合协议预期,也检查一些旧字段没有被偷偷写进去 → 再把 JSON 反序列化回 FramedMessage,确认权限配置和工作区路径仍然一致。
调用关系:它守住 SpawnRequest 这类启动消息的协议边界。真实流程里 send_spawn_request 会发送这种消息;这个测试确保消息格式不会因为结构变化而破坏父进程和运行器之间的约定。
调用图:调用 1 个内部函数(read_only);外部调用 8 个(new, new, from, assert_eq!, panic!, from_value, to_value, vec!)。