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 慢连接队列满后直接 kick,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 集成