Skip to content

コマンド実行と exec-server

源码版本rust-v0.145.0

exec-server crate は codex がコマンド実行を切り出した独立サービスだ:URL(ローカルまたはリモート)を listen し、JSON-RPC 形式の ExecParams を受け取り、sandbox 内で子プロセスを spawn し、stdout/stderr をクライアントに流し戻す。core::exec はクライアント側入口で、sandbox タイプの算出、ExecRequest の構築を担い、exec-server に実際の実行を任せる。exec crate は codex exec CLI コマンドの薄いラッパーだ。

責務

  1. run_main で exec-server を起動し URL を listen し、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 はクライアント側呼び出し入口で、タイムアウト、IO 上限、エラー分類を担う:codex-rs/core/src/exec.rs:437-460

設計動機

なぜ exec を独立 server に分けるのか?コマンド実行は sandbox 内部で行う必要があり、sandbox 起動にはプラットフォームごとのオーバーヘッド(seatbelt init、bubblewrap setup、Windows token 準備)があるからだ。exec-server を長駐プロセスにして準備済みのリソースを再利用し、クライアントは RPC を送るだけで済む。同時に、リモート環境(SSH / クラウドサンドボックス)も同じプロトコルを使える——EnvironmentProvider がローカルとリモートを抽象化し、クライアントはプロセスが実際にどのマシンで走るかを気にしなくてよい。

なぜ ExecBackend が具体型ではなく trait なのか?LocalProcess はデフォルト実装だが、テスト時には mock backend に差し替え可能。リモート環境では RemoteProcess が noise relay を通す。ExecProcess trait が「プロセス起動後の操作」(read / write / signal / terminate)を独立させ、「起動方法」と切り離す。

prepare_exec_request がクライアント側ではなくサーバ側で行われなければならないのは、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_requestSandboxType を算出した後、SandboxCommandSandboxManager::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 を併せて参照。