Skip to content

附录 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):

  1. 拼很小的请求 { command, cwd }
  2. Orchestrator:审批(绕过/缓存/弹窗)→ 选沙箱 → run
  3. Runtime 把 SandboxTransformRequest 变成 ExecRequest,再 spawn PTY
  4. 若像沙箱拒绝,升级 SandboxType::None 再试(B3 的 exit 2/126/127 不算 拒绝)
  5. 交回带流式输出的 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普通你以为的 shellMCP stdioComputer Use
寿命可跨 turn 的 PTY一次 Command服务进程GUI 会话
沙箱启动时套上,stdin 不能改装每次 spawn不套(D3)OS 辅助功能
模型接口exec_command + write_stdin一个 tool 返回 stdoutmcp__…桌面插件
中断默认不停 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。