Skip to content

App-server 架構

源码版本rust-v0.145.0

app-server 是 codex 的 JSON-RPC 伺服端,把 codex-coreThreadManagerAuthManagerConfigManager 包成一個可被 IDE、Web、TUI 共用的處理程序。它對外暴露 stdio / unix socket / websocket 三種 transport,對內用 MessageProcessor 做 request 路由,所有 RPC 走同一套 ClientRequest / ServerNotification 協議。

職責

  1. 入口 run_main_with_transport_options 裝載 config、auth、otel、state_db,啟動 transport acceptor 與 outbound router,最後 spawn MessageProcessor 進入主 select 迴圈 (codex-rs/app-server/src/lib.rs:449-460)。
  2. Transport 層把 stdio:// / unix:// / ws://IP:PORT / off 統一成 TransportEvent(ConnectionOpened / ConnectionClosed / IncomingMessage),所有連線共享一個 event channel (codex-rs/app-server-transport/src/transport/mod.rs:73-78)。
  3. MessageProcessor::process_request 反序列化 JSONRPCRequestClientRequest,按 Initialize / 已初始化分支派發,已初始化的請求經 dispatch_initialized_client_request 入對應 processor (codex-rs/app-server/src/message_processor.rs:518-570)。
  4. 每條已初始化請求按 serialization_scope 排隊或 spawn:RequestSerializationQueues 保證同 scope 請求串行(比如同 thread 的 turn 操作),跨 scope 才並發 (codex-rs/app-server/src/message_processor.rs:846-861)。
  5. 出站走 OutgoingEnvelope::ToConnection / Broadcast,廣播只發已 initialized 的連線;experimental 通知只發開了 experimental_api 的連線 (codex-rs/app-server/src/transport.rs:198-237)。

設計動機

app-server 之所以獨立成 crate,是因為 IDE(VSCode、JetBrains)、TUI、遠端配對 app 共用同一套協議但執行環境不同:IDE 跑 stdio,遠端配對走 websocket,daemon 模式需要 unix socket + pid 檔案。把 transport 抽到 app-server-transport crate,協議抽到 app-server-protocol crate,server crate 只關心「如何把 JSON-RPC 路由到 ThreadManager」,這樣加新 transport 不用動主邏輯。

ConnectionSessionStateOnceLock<InitializedConnectionSessionState> 而不是 Mutex,因為 initialize 是一次性操作——一旦設定不可改,後續請求只讀。這避免了「已初始化連線」用鎖的開銷,experimental_api_enabled / opted_out_notification_methods 都直接 atomic / RwLock 讀。

請求路由用 processor struct 集合而不是單個 match:每個 processor(ThreadRequestProcessorTurnRequestProcessorConfigRequestProcessor 等)持有自己的 state,handle_initialized_client_request 只做 match 派發。這樣加新方法不用動中心程式碼,processor 內部測試也好寫。

serialization_scope 是個有意思的設計:同 thread 的 turn 操作必須串行(否則兩個 Op::UserInput 會亂序),但不同 thread 的請求可以並發。RequestSerializationQueues(connection_id, scope) 分 key,同 key 請求排隊,跨 key 直接 tokio::spawn

outbound 路由的 try_send + 佇列滿就 disconnect 是防止慢客戶端拖死 server:websocket 慢連線佇列滿後直接踢,stdio/stdin 沒有斷流保護才用 send().await

關鍵檔案

codex-rs/app-server/src/main.rs:19-60AppServerArgs 命令列,--listen 解析 transport,--session-source 區分 vscode / cli / mcp。codex-rs/app-server/src/lib.rs:449-540run_main_with_transport_options,裝載 config、auth、otel 的入口。codex-rs/app-server/src/lib.rs:849-990MessageProcessor spawn 與主 select 迴圈,處理 transport event 與 outbound envelope。codex-rs/app-server/src/message_processor.rs:224-302MessageProcessor::new,建立 ThreadManagerThreadStateManagerSkillsWatcher、各 processor。codex-rs/app-server/src/message_processor.rs:799-862dispatch_initialized_client_request,serialization scope 派發邏輯。codex-rs/app-server/src/message_processor.rs:864-1000handle_initialized_client_request 大 match,把 ClientRequest 路由到各 processor。codex-rs/app-server/src/transport.rs:134-172send_message_to_connection,慢連線佇列滿後 disconnect。codex-rs/app-server-transport/src/transport/mod.rs:73-158AppServerTransport 列舉與 from_listen_url 解析。codex-rs/app-server-protocol/src/rpc.rs:34-88JSONRPCMessage / JSONRPCRequest / JSONRPCError,協議基礎型別(註解明確說不做真 JSON-RPC 2.0,不要求 jsonrpc 欄位)。codex-rs/app-server/src/request_processors/initialize_processor.rs:44-101InitializeRequestProcessor::initialize,設定 connection 的 experimental_api、attestation、originator 等全域身份。

主 select 是 transport event 與 outbound envelope 二選一,加 shutdown 訊號守衛。transport 分支把 IncomingMessage 交給 processor.process_request,outbound 分支調 route_outgoing_envelope 路由:

rust
// app-server/src/lib.rs:877-892 — 主循环 shutdown 判定
let exit_reason = loop {
    let running_turn_count = { *running_turn_count_rx.borrow() };
    if matches!(
        shutdown_state.update(running_turn_count, connections.len()),
        ShutdownAction::Finish
    ) {
        transport_shutdown_token.cancel();
        let _ = outbound_control_tx
            .send(OutboundControlEvent::DisconnectAll)
            .await;
        break "shutdown_requested";
    }
    tokio::select! { /* ... */ }
};

dispatch_initialized_client_request 把請求轉成 QueuedInitializedRequest 再決定排隊或 spawn。同 scope 必須串行,跨 scope 才並發——比如兩個 connection 各自發 turn 不會互相阻塞:

rust
// message_processor.rs:851-860 — scope 排队 vs 直接 spawn
if let Some(scope) = serialization_scope {
    let (key, access) = RequestSerializationQueueKey::from_scope(connection_id, scope);
    self.request_serialization_queues
        .enqueue(key, access, request)
        .await;
} else {
    tokio::spawn(async move { request.run().await; });
}

route_outgoing_envelope 區分定向發送與廣播:廣播時遍歷所有已 initialized 連線,但 experimental notification 只發開了 experimental_api 的連線,opted_out_notification_methods 讓客戶端可以按方法名退訂:

rust
// app-server/src/transport.rs:212-236 — Broadcast 路由
OutgoingEnvelope::Broadcast { message } => {
    let target_connections: Vec<ConnectionId> = connections
        .iter()
        .filter_map(|(connection_id, connection_state)| {
            if connection_state.initialized.load(Ordering::Acquire)
                && !should_skip_notification_for_connection(connection_state, &message)
            {
                Some(*connection_id)
            } else {
                None
            }
        })
        .collect();
    // ...
}

資料流

邊界與失敗

小結

app-server 是 codex 的協議層:transport 多形態、processor 各管一攤、serialization scope 保證同 thread 串行。ConnectionSessionStateOnceLock 表達「初始化一次即不可變」的語意。要繼續往下看,遠端配對、daemon 生命週期在 app-server-daemon crate,客戶端封裝在 app-server-client;MCP 工具呼叫如何經此進 core 見 MCP 整合