App-server 架構
app-server 是 codex 的 JSON-RPC 伺服端,把 codex-core 的 ThreadManager、AuthManager、ConfigManager 包成一個可被 IDE、Web、TUI 共用的處理程序。它對外暴露 stdio / unix socket / websocket 三種 transport,對內用 MessageProcessor 做 request 路由,所有 RPC 走同一套 ClientRequest / ServerNotification 協議。
職責
- 入口
run_main_with_transport_options裝載 config、auth、otel、state_db,啟動 transport acceptor 與 outbound router,最後 spawnMessageProcessor進入主 select 迴圈 (codex-rs/app-server/src/lib.rs:449-460)。 - Transport 層把
stdio:///unix:///ws://IP:PORT/off統一成TransportEvent(ConnectionOpened / ConnectionClosed / IncomingMessage),所有連線共享一個 event channel (codex-rs/app-server-transport/src/transport/mod.rs:73-78)。 MessageProcessor::process_request反序列化JSONRPCRequest成ClientRequest,按Initialize/ 已初始化分支派發,已初始化的請求經dispatch_initialized_client_request入對應 processor (codex-rs/app-server/src/message_processor.rs:518-570)。- 每條已初始化請求按
serialization_scope排隊或 spawn:RequestSerializationQueues保證同 scope 請求串行(比如同 thread 的 turn 操作),跨 scope 才並發 (codex-rs/app-server/src/message_processor.rs:846-861)。 - 出站走
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 不用動主邏輯。
ConnectionSessionState 用 OnceLock<InitializedConnectionSessionState> 而不是 Mutex,因為 initialize 是一次性操作——一旦設定不可改,後續請求只讀。這避免了「已初始化連線」用鎖的開銷,experimental_api_enabled / opted_out_notification_methods 都直接 atomic / RwLock 讀。
請求路由用 processor struct 集合而不是單個 match:每個 processor(ThreadRequestProcessor、TurnRequestProcessor、ConfigRequestProcessor 等)持有自己的 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-60 — AppServerArgs 命令列,--listen 解析 transport,--session-source 區分 vscode / cli / mcp。codex-rs/app-server/src/lib.rs:449-540 — run_main_with_transport_options,裝載 config、auth、otel 的入口。codex-rs/app-server/src/lib.rs:849-990 — MessageProcessor spawn 與主 select 迴圈,處理 transport event 與 outbound envelope。codex-rs/app-server/src/message_processor.rs:224-302 — MessageProcessor::new,建立 ThreadManager、ThreadStateManager、SkillsWatcher、各 processor。codex-rs/app-server/src/message_processor.rs:799-862 — dispatch_initialized_client_request,serialization scope 派發邏輯。codex-rs/app-server/src/message_processor.rs:864-1000 — handle_initialized_client_request 大 match,把 ClientRequest 路由到各 processor。codex-rs/app-server/src/transport.rs:134-172 — send_message_to_connection,慢連線佇列滿後 disconnect。codex-rs/app-server-transport/src/transport/mod.rs:73-158 — AppServerTransport 列舉與 from_listen_url 解析。codex-rs/app-server-protocol/src/rpc.rs:34-88 — JSONRPCMessage / JSONRPCRequest / JSONRPCError,協議基礎型別(註解明確說不做真 JSON-RPC 2.0,不要求 jsonrpc 欄位)。codex-rs/app-server/src/request_processors/initialize_processor.rs:44-101 — InitializeRequestProcessor::initialize,設定 connection 的 experimental_api、attestation、originator 等全域身份。主 select 是 transport event 與 outbound envelope 二選一,加 shutdown 訊號守衛。transport 分支把 IncomingMessage 交給 processor.process_request,outbound 分支調 route_outgoing_envelope 路由:
// 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 不會互相阻塞:
// 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 讓客戶端可以按方法名退訂:
// 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();
// ...
}資料流
邊界與失敗
- 慢連線被踢:websocket 連線 outbound 佇列滿後
try_send回傳Full,直接disconnect_connection,防止 server 被慢客戶端拖死;stdio 沒有 disconnect_sender 時才send().await阻塞 (codex-rs/app-server/src/transport.rs:154-172)。 - 未初始化連線只接 Initialize:
dispatch_initialized_client_request第一行檢查session.initialized(),未初始化直接invalid_request("Not initialized")(codex-rs/app-server/src/message_processor.rs:806-808)。 - experimental 請求被 gate:帶
experimental_reason()的請求在未開 experimental_api 的連線上被拒,回傳experimental_required_message(codex-rs/app-server/src/message_processor.rs:810-814)。 - shutdown 等 turn 收尾:
ShutdownState::update看running_turn_count與connections.len(),只有兩者都為 0 才Finish;帶超時避免死等 (codex-rs/app-server/src/lib.rs:877-892)。 - transport
off也合法:不啟動任何 acceptor,只跑 remote control,這是 headless 場景下用 daemon 控制遠端 codex 的路徑 (codex-rs/app-server/src/lib.rs:712-740)。
小結
app-server 是 codex 的協議層:transport 多形態、processor 各管一攤、serialization scope 保證同 thread 串行。ConnectionSessionState 用 OnceLock 表達「初始化一次即不可變」的語意。要繼續往下看,遠端配對、daemon 生命週期在 app-server-daemon crate,客戶端封裝在 app-server-client;MCP 工具呼叫如何經此進 core 見 MCP 整合。