Skip to content

命令执行与 exec-server

源码版本rust-v0.145.0

exec-server crate 是 codex 把命令执行抽出来的独立服务:它监听一个 URL(本地或远程),接收 JSON-RPC 风格的 ExecParams,在 sandbox 里 spawn 子进程,并把 stdout/stderr 流回 client。core::exec 是 client 侧入口,负责算 sandbox 类型、构造 ExecRequest,再交给 exec-server 实际跑。exec crate 是 codex exec 这个 CLI 命令的薄包装。

职责

  1. 提供 run_main 启动 exec-server 监听 URL,并暴露 telemetry span:codex-rs/exec-server/src/server.rs:17-35
  2. 定义 ExecBackend trait 与 ExecProcess trait,把"启动进程"与"和进程交互"解耦:codex-rs/exec-server/src/process.rs:179-208
  3. LocalProcess 是默认 backend,调用 codex_sandboxing::spawn_process 真正起进程:codex-rs/exec-server/src/local_process.rs:261-272
  4. prepare_exec_request 在 spawn 前把 sandbox context 翻译成 PreparedExecRequestcodex-rs/exec-server/src/process_sandbox.rs:69-130
  5. core::exec::execute_exec_request 是 client 侧调用入口,负责超时、IO cap、错误归类:codex-rs/core/src/exec.rs:437-460

设计动机

为什么把 exec 拆成独立 server?因为命令执行需要在 sandbox 内部进行,而 sandbox 启动有平台开销(seatbelt init、bubblewrap setup、Windows token 准备)。让 exec-server 长驻进程复用这些准备好的资源,client 只发 RPC 即可。同时,远程环境(SSH / 云沙箱)可以用同一套协议——EnvironmentProvider 抽象本地与远程,client 不用关心进程实际跑在哪台机器上。

为什么 ExecBackend 是 trait 而非具体类型?因为 LocalProcess 是默认实现,但测试时可以替换成 mock backend;远程环境下 RemoteProcess 走 noise relay。ExecProcess trait 把"进程已经启动后的操作"(read / write / signal / terminate)独立出来,与"如何启动"解耦。

prepare_exec_request 必须在 server 侧而非 client 侧做,因为 sandbox context 里的 cwdworkspace_roots 是 executor 本地路径,需要 native_path 转换。permission profile 还要 materialize_project_roots_with_workspace_roots 把远程传来的项目根映射到 executor 文件系统。

关键文件

ExecProcess trait 把"进程已启动后的操作"独立出来,read/write/signal/terminate 全是异步,跟"如何启动"解耦。

rust
pub trait ExecProcess: Send + Sync {
    fn process_id(&self) -> &ProcessId;
    fn subscribe_wake(&self) -> watch::Receiver<u64>;
    fn subscribe_events(&self) -> ExecProcessEventReceiver;
    fn read(&self, after_seq: Option<u64>, max_bytes: Option<usize>, wait_ms: Option<u64>) -> ExecProcessFuture<'_, ReadResponse>;
    fn write(&self, chunk: Vec<u8>) -> ExecProcessFuture<'_, WriteResponse>;
    fn signal(&self, signal: ProcessSignal) -> ExecProcessFuture<'_, ()>;
    fn terminate(&self) -> ExecProcessFuture<'_, ()>;
}

LocalProcess 是默认 backend,调用 codex_sandboxing::spawn_process 真正起进程:

rust
let spawned_result = codex_sandboxing::spawn_process(codex_sandboxing::SpawnRequest {
    command: &prepared.command,
    cwd: prepared.cwd.as_path(),
    env: &prepared.env,
    arg0: &prepared.arg0,
    sandbox: prepared.sandbox,
    windows_sandbox: prepared.windows_sandbox_spawn_request(),
    tty: params.tty,
    stdin_open: params.tty || params.pipe_stdin,
    inherited_fds: &[],
}).await;

core 侧 build_exec_request 算出 SandboxType 后,把 SandboxCommand 交给 SandboxManager::transform,得到带 wrapper argv 的 SandboxExecRequest

rust
let sandbox_type = select_process_exec_tool_sandbox_type(
    &file_system_sandbox_policy,
    network_sandbox_policy,
    windows_sandbox_level,
    enforce_managed_network,
);
let manager = SandboxManager::new();
let exec_req = manager
    .transform(SandboxTransformRequest {
        command, permissions: permission_profile, sandbox: sandbox_type,
        /* ... */
    })
    .map(|request| { /* ...ExecRequest::from_sandbox_exec_request */ })
    .map_err(CodexErr::from)?;

数据流

边界与失败

小结

exec-server 把命令执行从 core 里独立出来,ExecBackend trait 让本地与远程环境共享同一套 RPC 协议。core 侧只负责算 sandbox、构造 request、收 output;LocalProcess 默认 backend 走 codex_sandboxing::spawn_process 真正起进程。要追"命令为什么没跑起来"或"输出为什么被截断"的现场,配合 跨平台沙箱命令分类 execpolicy 一起看。