命令执行与 exec-server
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 命令的薄包装。
职责
- 提供
run_main启动 exec-server 监听 URL,并暴露 telemetry span:codex-rs/exec-server/src/server.rs:17-35 - 定义
ExecBackendtrait 与ExecProcesstrait,把"启动进程"与"和进程交互"解耦:codex-rs/exec-server/src/process.rs:179-208 LocalProcess是默认 backend,调用codex_sandboxing::spawn_process真正起进程:codex-rs/exec-server/src/local_process.rs:261-272prepare_exec_request在 spawn 前把 sandbox context 翻译成PreparedExecRequest:codex-rs/exec-server/src/process_sandbox.rs:69-130core::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 里的 cwd、workspace_roots 是 executor 本地路径,需要 native_path 转换。permission profile 还要 materialize_project_roots_with_workspace_roots 把远程传来的项目根映射到 executor 文件系统。
关键文件
codex-rs/exec-server/src/lib.rs:36-100— crate 入口,re-exportExecServerClient、Environment、ExecBackend等。codex-rs/exec-server/src/server.rs:17-35—run_main/run_main_with_telemetry启动监听。codex-rs/exec-server/src/process.rs:179-208—ExecProcess与ExecBackend两个 trait。codex-rs/exec-server/src/local_process.rs:261-272—LocalProcess调spawn_process起子进程。codex-rs/exec-server/src/local_process.rs:610-614—ExecBackend for LocalProcess的 impl。codex-rs/exec-server/src/process_sandbox.rs:34-67—PreparedExecRequest与PreparedWindowsSandboxRequest。codex-rs/exec-server/src/process_sandbox.rs:69-130—prepare_exec_request把 sandbox context 落到 executor 本地路径。codex-rs/core/src/exec.rs:117-130— core 侧select_process_exec_tool_sandbox_type。codex-rs/core/src/exec.rs:350-413—build_exec_request调SandboxManager::transform得到SandboxExecRequest。
ExecProcess trait 把"进程已启动后的操作"独立出来,read/write/signal/terminate 全是异步,跟"如何启动"解耦。
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 真正起进程:
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:
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)?;数据流
边界与失败
prepare_exec_request里sandbox_context为None时直接退化为SandboxType::None,跳过所有沙箱准备:codex-rs/exec-server/src/process_sandbox.rs:80-90LocalProcess启动失败时要从processesmap 里清掉Starting占位,避免泄露:codex-rs/exec-server/src/local_process.rs:273-285- core 在 exec 完成后用
is_likely_sandbox_denied看 stderr 判断是不是被 sandbox 拦了,给模型可读错误:codex-rs/core/src/exec.rs:804-815 - 单次 exec 输出有
EXEC_OUTPUT_MAX_BYTES硬上限,避免子进程把 stdout 灌爆 OOM:codex-rs/core/src/exec.rs:72-76 - IO drain 有 2s 超时,子进程被 kill 后 grandchild 还持着 pipe 不会让 agent 永远挂住:
codex-rs/core/src/exec.rs:82-89
小结
exec-server 把命令执行从 core 里独立出来,ExecBackend trait 让本地与远程环境共享同一套 RPC 协议。core 侧只负责算 sandbox、构造 request、收 output;LocalProcess 默认 backend 走 codex_sandboxing::spawn_process 真正起进程。要追"命令为什么没跑起来"或"输出为什么被截断"的现场,配合 跨平台沙箱 与 命令分类 execpolicy 一起看。