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 翻譯成 PreparedExecRequest:codex-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 一起看。