附录 B|unified_exec:管的是进程,不是一句 sh -c
先别把 exec_command 当成 std::process::Command 一次返回
B2 把 exec_command + write_stdin 标成「长生命周期 PTY」。正文没拆缓冲和二次审批。没有这一附录,会把后台 npm run dev 理解成一次 tool call,或把 Interrupt 理解成杀掉所有终端。
判断先说清楚:unified_exec 是带审批和沙箱的交互进程管理器。 模块头写死了职责(core/src/unified_exec/mod.rs:1-24):创建/复用进程、有顶的输出缓冲、可 completion-only 的一次性调用;spawn 走共享 ToolOrchestrator;沙箱拒绝后按政策升级,审批可缓存。PTY 生命周期在 process.rs,本地/远程共用 process_state.rs,编排在 process_manager.rs。
没有 Feature UnifiedExec 时,B2 说会退化成 one-shot。有 feature 时,模型看见的仍是 exec_command / write_stdin 两个工具,不是「shell」一个万能入口。
1. 打开进程:Orchestrator → 变沙箱 → spawn PTY
打开一条终端的顺序(同文件 :13-18):
- 拼很小的请求
{ command, cwd } - Orchestrator:审批(绕过/缓存/弹窗)→ 选沙箱 → run
- Runtime 把
SandboxTransformRequest变成ExecRequest,再 spawn PTY - 若像沙箱拒绝,升级
SandboxType::None再试(B3 的 exit 2/126/127 不算 拒绝) - 交回带流式输出的 process handle
exec_command 参数里有 yield_time_ms、timeout_ms、tty、max_output_tokens(handlers/unified_exec.rs:28-41)。yield 夹在 250ms–30s(MIN_YIELD_TIME_MS / MAX_YIELD_TIME_MS);空的 write_stdin 至少等 5s(MIN_EMPTY_YIELD_TIME_MS)。Windows 首次 exec 地板 10s。后台终端默认最长 300s(DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS)。
一次性跑完、调用方不要可恢复句柄:走 exec_command_to_completion(oneshot.rs:1-4, 26-30)。超时和取消杀的是 这条 刚发布的 handle,不是商店里所有 PTY。
同一 session 最多 64 个进程(MAX_UNIFIED_EXEC_PROCESSES,mod.rs:82)。满了按 last_used 和是否已退出 prune(process_manager.rs:1607-1618)。没有帽,模型开 200 个 npm test 会把机器打满。
exec_command 还会拦截长得像 apply_patch 的命令,改走补丁 Runtime(B2)。长进程管的是交互式程序,不是文件编辑协议。
2. write_stdin:往活着的 PTY 写,可能再审批
模型用 session_id(训练名)指向进程(write_stdin.rs:23-25)。Handler 声明 supports_parallel_tool_calls = true(:45-47)——不同终端 可以一起 poll。同一终端的读/写必须互斥:共享 draining 输出缓冲和生命周期(process_manager.rs:824-835)。先拿 interaction_lock,再 stdin_approval。
stdin 审批的纪律在 stdin_approval.rs:1-4:
- 权限快照 归 host
- 早先的一次 Allow,或后来政策变了,都不能改已经在跑的进程的沙箱
- Native 启动保留当时的 Windows sandbox level;executor 启动用 executor 默认
review_requirement(:106-139)比的是启动时冻住的 TerminalPolicy 和 现在 的政策:
- 环境网络限制变了、或当时是 bypass:不能给旧终端「补」上网络闸 → 报错,请开新终端
- denied-read 限制变了:审批 不能 把只读禁令改装到活进程上 → 同样请开新终端
- 设置仍匹配:才决定这次 stdin 是 UseDefault / 额外权限 / 要升级
序列化后的动作太大,无法安全送给 Guardian:直接拒,让模型改小输入或少授权再开终端(process_manager.rs:852-856)。这是 B4 有界注入在终端输入上的对应物。
没有「启动冻沙箱、stdin 再审」会怎样?用户把政策拧回 read-only 之后,已经在跑的 bash 仍是全盘写;或每次 write_stdin 都把 Seatbelt 拆掉重套——PTY 做不到。
3. 输出:头尾缓冲,中间丢掉
HeadTailBuffer(head_tail_buffer.rs:5-19):容量默认 1 MiB(UNIFIED_EXEC_OUTPUT_MAX_BYTES)。一半给稳定前缀(head),一半给后缀(tail);中间溢出计入 omitted_bytes。给模型的 token 另有顶(默认 10_000,DEFAULT_MAX_OUTPUT_TOKENS)。
长日志(测试、dev server)必须能被下一枪采样看见开头的配置和结尾的失败,不必看见中间十万行。这是上下文腐烂对策:进程输出不是无限 fragment。
流式输出和退出由 async_watcher 发 Exec 生命周期 Event,TUI /ps 列的就是这些后台终端(E10)。
4. 取消 turn ≠ 杀掉 PTY
A2 / B1:Op::Interrupt abort 当前 task,不终止后台 terminal。要杀长进程,用 CleanBackgroundTerminals(protocol.rs:601-607)。TUI /stop(别名 /clean)走这条。
UnifiedExecContext 带着这一枪的 cancellation_token(mod.rs:85-88)。取消的是 这次 yield/wait,不是 process store。dev server 跨 turn 活着,才能「先启动再测再看日志」。
/cd 在有活动后台终端时会挡住(E2):cwd 变了,旧 PTY 的启动 cwd 还冻着,模型和沙箱会对不齐。
5. Unix 上的 zsh-fork:升权走 socket,不一定换终端
普通路径是:Orchestrator 选好 SandboxAttempt,再 spawn 一条新 PTY。Unix 上若 Feature::ShellZshFork / UnifiedExecZshFork 打开,且命令是包装过的 zsh -c / zsh -lc,maybe_prepare_unified_exec(tools/runtimes/zsh_fork.rs 模块注释)会改走 shell-escalation:会话里留一个 escalation server,子进程通过 ESCALATE_SOCKET_ENV_VAR 要更高权限的 execve,而不是把已经在跑的 PTY 拆掉重套沙箱。对不上 zsh 路径就 Ok(None),回普通 spawn(unix_escalation.rs 的 prepare_unified_exec_zsh_fork)。非 Unix 恒为 None。
没有这条后端会怎样?每次「再要一点权限」都新开沙箱进程,可恢复 PTY 的「启动冻沙箱、stdin 再审」在交互壳上做不到;或者把升权焊进 exec_command 的字符串协议。zsh-fork 仍过 Orchestrator 和 ExecPolicy,只是 spawn 后端换了,不是第四套同意旋钮。
6. 和 shell 沙箱、MCP、Computer Use 的边界
| unified_exec | 普通你以为的 shell | MCP stdio | Computer Use | |
|---|---|---|---|---|
| 寿命 | 可跨 turn 的 PTY | 一次 Command | 服务进程 | GUI 会话 |
| 沙箱 | 启动时套上,stdin 不能改装 | 每次 spawn | 不套(D3) | OS 辅助功能 |
| 模型接口 | exec_command + write_stdin | 一个 tool 返回 stdout | mcp__… | 桌面插件 |
| 中断 | 默认不停 PTY | 进程一起死 | 连接还在 | 前台操作 |
远程 environment_id 上的 exec 仍走同一套 process_state,只是 ExecProcess 在 exec-server 上。决策仍是 Orchestrator,不是远程自己发明审批(B6)。
7. 结语:带走一句话
unified_exec 把「跑命令」收成可恢复的 PTY:打开时过 Orchestrator,活着时沙箱冻住、stdin 按政策再审;Unix 上 zsh-fork 用 escalation socket 升权,不拆终端——Interrupt 停的是这一轮采样,不是你的 dev server。