CodeMode架构总览
Code Mode 不是让模型在 Core 进程里直接拿到一个 JavaScript eval。Core 暴露 custom exec/wait 工具并维护 Turn 权限;每个 Codex thread 的 CodeModeService 懒创建一个逻辑 CodeModeSession;session 通过本地子进程、WebSocket 或 gRPC 连接独立 host;host 内部才创建 InProcessCodeModeSession,并由 SessionRuntime → CellActor → V8 runtime 执行代码。JavaScript 发起 nested tool call 时,结果沿反向 delegate 回到当前 Turn 的 ToolCallRuntime,仍然经过原工具的审批、sandbox 和生命周期逻辑。
本文承接工具框架测试策略、Codex执行体系总览和 本地与远程执行Backend。范围是 provider、session、host、runtime、cell 和 nested dispatch 的所有权关系;协议字段、V8 初始化细节和 wait 算法由后续专题展开。读完后,你应能 判断一段状态属于 thread、host session 还是 cell,定位代码实际执行的进程,并解释 nested tool 为什么 不能绕过 Core 权限。
1. 进程分层
当前主链可分为七层:Core tool handler、thread-owned CodeModeService、共享 session provider、逻辑 CodeModeSession、独立 host、host 内 InProcessCodeModeSession/SessionRuntime、单次 CellActor/V8 执行。codex-code-mode-protocol 定义跨层稳定接口;它不拥有 V8 或 Core Session。
这个分层有两个重要结果。第一,V8 崩溃或 host 重启不等于 Core Session 必须退出;provider 可以重建 transport。第二,host 能执行 JavaScript,但没有 Core 的 Session、approval cache 或 filesystem owner;它只能通过 delegate 请求当前 Turn 调用工具。
2. Provider选择
2.1 Core默认路径
ThreadManager 持有一个共享 CodeModeSessionProvider。启用 CodeModeHost feature 时默认使用 ProcessOwnedCodeModeSessionProvider,否则使用 DisabledCodeModeSessionProvider。provider 被注入 每个新 thread,但每个 thread 的 CodeModeService 会创建自己的逻辑 session。
源码位置:codex-rs/core/src/thread_manager.rs :: ThreadManager::new 的 Code Mode provider 装配
let code_mode_session_provider: Arc<dyn CodeModeSessionProvider> =
if config.features.enabled(Feature::CodeModeHost)
|| config.code_mode.disable_in_process_fallback
{
Arc::new(ProcessOwnedCodeModeSessionProvider::default())
} else {
Arc::new(DisabledCodeModeSessionProvider)
};尽管配置字段仍带有历史上的 in_process_fallback 名称,这段代码没有把 Core 连接到 InProcessCodeModeSession。可执行 V8 runtime 位于 host crate;缺少 host binary 时 provider 的 availability 会返回错误,CodeModeOnly 模式按 fail-closed 行为处理。
2.2 App Server传输
App Server 可按启动参数注入 local、WebSocket 或 gRPC provider。remote provider 仍实现同一个 CodeModeSessionProvider,因此 Core handler 不需要识别 transport 类型。
源码位置:codex-rs/app-server/src/lib.rs :: CodeModeHostTransport provider 选择
match &runtime_options.code_mode_host_transport {
CodeModeHostTransport::Local => None,
CodeModeHostTransport::WebSocket(url) => Some(Arc::new(
WebSocketCodeModeSessionProvider::with_http_client_factory(
url.to_string(),
config.http_client_factory(),
),
)),
CodeModeHostTransport::Grpc(url) => Some(Arc::new(
GrpcCodeModeSessionProvider::with_http_client_factory(
url.to_string(),
config.http_client_factory(),
),
)),
}3. Thread会话
CodeModeService 属于一个 Codex thread。它保存 provider、懒初始化的 OnceCell<Arc<dyn CodeModeSession>>、dispatch broker、默认 yield 时间和 shutdown 状态。多个 Turn 复用同一个逻辑 session,因此 store/load 可以跨 Turn 保留;不同 thread 的 service/session 隔离,但可共享同一个 host process 或 remote transport。
源码位置:codex-rs/core/src/tools/code_mode/mod.rs :: CodeModeService
pub(crate) struct CodeModeService {
session: OnceCell<Arc<dyn CodeModeSession>>,
session_provider: Arc<dyn CodeModeSessionProvider>,
availability: Result<(), String>,
dispatch_broker: Arc<CodeModeDispatchBroker>,
default_exec_yield_time_ms: u64,
shutting_down: AtomicBool,
unavailable_warning_emitted: AtomicBool,
}session() 使用 get_or_try_init,并在创建前后检查 shutdown。若 shutdown 与首次创建竞态,刚创建的 session 会立即关闭,而不会泄漏 host session。shutdown() 不会为了关闭而启动一个从未使用的 host。
源码位置:codex-rs/core/src/tools/code_mode/mod.rs :: CodeModeService::session、shutdown
self.session
.get_or_try_init(|| async {
if self.shutting_down.load(Ordering::Acquire) {
return Err("code mode session is shutting down".to_string());
}
let session = self
.session_provider
.create_session(self.dispatch_broker.clone())
.await?;
if self.shutting_down.load(Ordering::Acquire) {
let _ = session.shutdown().await;
return Err("code mode session is shutting down".to_string());
}
Ok(session)
})
.await4. Exec入口
4.1 工具快照
CodeModeExecuteHandler 接受 raw custom payload,解析 // @exec pragma,并为本次 cell 构造 nested tool definition 快照。支持 cached definition 的 runtime 直接复用缓存;其他工具从当前 spec 转换。 handler 删除 input/output schema,按导出名排序去重,再发给 host。JavaScript 因此只看到当前 Turn 允许暴露的工具,不会动态读取整个 Core registry。
源码位置:codex-rs/core/src/tools/code_mode/execute_handler.rs :: CodeModeExecuteHandler::execute
let args = codex_code_mode::parse_exec_source(&code)
.map_err(FunctionCallError::RespondToModel)?;
let mut enabled_tools = Vec::with_capacity(self.nested_tool_specs.len());
for (spec, cached_runtime) in &self.nested_tool_specs {
if let Some(cached_definitions) = cached_runtime
.as_ref()
.and_then(|runtime| runtime.cached_code_mode_definitions())
{
enabled_tools.extend_from_slice(cached_definitions);
continue;
}
let definitions =
codex_tools::collect_code_mode_tool_definitions(std::iter::once(spec.as_ref()));
enabled_tools.extend(definitions.into_iter().map(|mut definition| {
definition.input_schema = None;
definition.output_schema = None;
definition
}));
}
enabled_tools.sort_by(|left, right| left.name.cmp(&right.name));
enabled_tools.dedup_by(|left, right| left.name == right.name);4.2 Cell登记屏障
handler 调用 service execute 后,先登记 analytics、executed-tool mapping 和 rollout trace,再调用 mark_cell_ready_for_dispatch。如果 JavaScript 极快地发起 nested call,broker 会在 gate 上等待,避免 nested result 先于外层 cell 元数据建立。
源码位置:codex-rs/core/src/tools/code_mode/execute_handler.rs :: CodeModeExecuteHandler::execute
let started_cell = exec.session.services.code_mode_service.execute(ExecuteRequest {
tool_call_id: call_id.clone(),
enabled_tools,
source: args.code.clone(),
yield_time_ms: args.yield_time_ms,
max_output_tokens: args.max_output_tokens,
}).await?;
let cell_id = started_cell.cell_id.clone();
// analytics, executed-tool mapping and trace registration happen here.
exec.session
.services
.code_mode_service
.mark_cell_ready_for_dispatch(&cell_id);
let response = started_cell.initial_response().await?;initial response 若是 Yielded,cell 继续存活,由后续 wait 继续观察;若是 Result/Terminated,handler 记录 terminal trace 并关闭 dispatch gate。response adapter 还负责输出预算和图片 detail 限制。
5. Host会话
5.1 Host连接
ProcessOwnedCodeModeSessionProvider 和 WebSocketCodeModeSessionProvider 都持有共享 OwnedCodeModeHost。host 用单 permit 串行协调首次连接,缓存仍存活的 connection,并为每个逻辑 session 分配单调 session-N ID。子进程丢失或 socket 关闭后,下一次正常使用可以创建新 connection。
源码位置:codex-rs/code-mode/src/remote_session.rs :: OwnedCodeModeHost::connection、allocate_session_id
async fn connection(&self) -> Result<Arc<Connection>, ConnectionError> {
if let Some(connection) = self.live_connection() {
return Ok(connection);
}
let _connect_permit = self.connect_permit.acquire().await.map_err(|_| {
ConnectionError::Other("code-mode host connection coordinator closed".into())
})?;
if let Some(connection) = self.live_connection() {
return Ok(connection);
}
let new_connection = match &self.endpoint {
HostEndpoint::Process(host_program) => Connection::spawn(host_program).await?,
HostEndpoint::WebSocket { websocket_url, http_client_factory } => {
Connection::connect_websocket(websocket_url, http_client_factory).await?
}
};
let new_connection = Arc::new(new_connection);
*self
.connection
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
Some(Arc::clone(&new_connection));
Ok(new_connection)
}5.2 gRPC
GrpcCodeModeSessionProvider 通过 HTTP/2 或 Unix socket 懒连接 host。每个 session 打开一条 lease stream, 再建立 tool-call subscription;ReconnectableSession 包装实际 binding,使 cached session 可在 remote host 重启后恢复新的 generation。资源限制随 OpenSession 一起发送。
源码位置:codex-rs/code-mode/src/grpc_session/mod.rs :: GrpcCodeModeSessionProvider::open_binding
let mut lease = deadline::startup(
"session opening",
client.open_session(grpc::OpenSessionRequest { cell_execution_limits }),
).await?.into_inner();
let first = deadline::startup("session lease opening", lease.message()).await?
.ok_or_else(|| "gRPC code-mode session lease ended before opening".to_string())?;
let request = grpc::SubscribeToToolCallsRequest {
session_id: opened.session_id.clone(),
tool_names: Vec::new(),
};
let response = deadline::startup(
"tool subscription",
client.subscribe_to_tool_calls(request),
).await?;6. Runtime所有权
host 的 HostState 持有 SessionId → Arc<InProcessCodeModeSession>。每个 in-process session 内部有一个 SessionRuntime:它拥有跨 cell stored_values、cell registry、任务 tracker、shutdown token 和递增 cell ID。创建 cell 时先复制当前 stored values,再注册 actor;cell 完成提交时才把本 cell 写入 merge 回共享 store。
源码位置:
codex-rs/code-mode-host/src/lib.rs::HostStatecodex-rs/code-mode-runtime/src/session_runtime/mod.rs::SessionRuntime
struct Inner<D: SessionRuntimeDelegate> {
stored_values: Mutex<HashMap<String, JsonValue>>,
cells: Mutex<HashMap<CellId, CellHandle>>,
cell_tasks: TaskTracker,
shutdown_token: CancellationToken,
delegate: Arc<D>,
task_failure_handler: Option<TaskFailureHandler>,
next_cell_id: AtomicU64,
}CellActor::prepare 为一次执行创建 runtime command channel、control channel、V8 isolate terminate handle、 observer 和 callback task 集合。V8 runtime 位于独立 runtime thread;actor 在 Tokio 侧管理 yield、wait、 termination、工具 Promise、通知与最终 store commit。
源码位置:codex-rs/code-mode-runtime/src/cell_actor/mod.rs :: CellActor::prepare、run_cell
let (runtime_tx, runtime_control_tx, runtime_terminate_handle) = spawn_runtime(
stored_values,
runtime_request(request),
event_tx,
PendingRuntimeMode::PauseUntilResumed,
task_failure_handler.clone(),
)?;
let task = run_cell(
host,
CellContext {
runtime_tx,
runtime_control_tx,
runtime_terminate_handle,
cell_state,
},
event_rx,
command_rx,
initial_observer,
task_failure_handler,
);7. Nested工具
runtime ProtocolDelegate 将内部 NestedToolCall 转成 CodeModeNestedToolCall,保留 cell ID、runtime call ID、逻辑 tool name/namespace、function/freeform kind 和 JSON input。transport 只搬运这个协议对象,不 执行 Core 工具。
源码位置:codex-rs/code-mode-runtime/src/service.rs :: ProtocolDelegate::invoke_tool
self.delegate.invoke_tool(CodeModeNestedToolCall {
cell_id: protocol_cell_id(&invocation.cell_id),
runtime_tool_call_id: invocation.runtime_tool_call_id,
tool_name: codex_protocol::ToolName {
name: invocation.tool_name.name,
namespace: invocation.tool_name.namespace,
},
tool_kind: match invocation.tool_kind {
runtime::ToolKind::Function => CodeModeToolKind::Function,
runtime::ToolKind::Freeform => CodeModeToolKind::Freeform,
},
input: invocation.input,
}, cancellation_token).awaitCore 的 CodeModeDispatchBroker 是 CodeModeSessionDelegate 实现。每个 Code Mode Turn 创建自己的 ToolCallRuntime worker;InvokeTool 消息通过 cell-ready gate 后另起 task,因此同一 cell 的多个 nested 工具可以并行。notification 则被注入当前 active Turn;取消会关闭 gate 或返回明确错误。
源码位置:codex-rs/core/src/tools/code_mode/delegate.rs :: CodeModeDispatchBroker::start_turn_worker、invoke_tool
let tool_runtime = ToolCallRuntime::new(
Arc::clone(&exec.session),
step_context,
tracker,
);
let host = Arc::new(CoreTurnHost { exec, tool_runtime });
// After the cell-ready gate:
tokio::spawn(async move {
let invocation = host.invoke_tool(invocation, cancellation_token.clone());
tokio::pin!(invocation);
let response = tokio::select! {
biased;
_ = cancellation_token.cancelled() => invocation.await,
response = &mut invocation => response,
};
let _ = response_tx.send(response);
});8. 信任边界
JavaScript 获得的是经过过滤、排序和去重的 tool definition 快照,而不是 Core router 引用。真正调用时 Core ToolCallRuntime 仍根据当前 Turn 解析 tool source、审批、sandbox、输出预算和 lifecycle event。 独立 host 不能直接读取 Core Session、environment filesystem 或 credential store。相反,Core 也不直接 操作 V8 isolate,只能通过 session execute/wait/terminate/shutdown。
失败边界也分层:缺少 host binary 是 provider availability 错误;transport 断开由 process/WebSocket/gRPC session 恢复或关闭;V8 task panic 由 host task failure handler 上报;cell terminate 取消 callbacks 并拒绝 未提交 store write;nested tool error 只拒绝对应 Promise,不必关闭整个 session。
9. 源码验证
runtime service 测试验证同步 exit() 只保留 exit 前输出,以及同一 session 的 store 跨 cell 可见、不同 session 隔离。session runtime 测试还验证 termination 会拒绝等待提交的 store write。
源码位置:
codex-rs/code-mode-runtime/src/service_tests.rs::synchronous_exit_returns_successfully、stored_values_are_shared_between_cells_but_not_sessionscodex-rs/code-mode-runtime/src/session_runtime/tests.rs::termination_rejects_a_waiting_store_commit_before_the_next_cell_can_load_it
cd codex-rs
cargo test -p codex-code-mode-runtime --lib synchronous_exit_returns_successfully -- --test-threads=1
cargo test -p codex-code-mode-runtime --lib stored_values_are_shared_between_cells_but_not_sessions -- --test-threads=1
cargo test -p codex-code-mode-runtime --lib termination_rejects_a_waiting_store_commit_before_the_next_cell_can_load_it -- --test-threads=1transport/host 测试验证 provider 复用共享 process host、stdio session 持久化 store 并转发 delegate,以及 gRPC cached session 在 host 重启后恢复。Core 集成则让 JavaScript 调用 nested tool,并验证 provider 在 多个 thread 间共享。
源码位置:
codex-rs/code-mode/src/remote_session_tests.rs::provider_reuses_its_live_process_hostcodex-rs/code-mode-host/tests/stdio.rs::remote_session_persists_values_forwards_delegates_and_controls_cellscodex-rs/code-mode-host/tests/grpc.rs::cached_session_recovers_after_a_remote_host_restartscodex-rs/core/tests/suite/code_mode.rs::code_mode_only_can_call_nested_toolscodex-rs/core/src/thread_manager_tests.rs::code_mode_session_provider_is_shared_across_threads
cargo test -p codex-code-mode --lib provider_reuses_its_live_process_host -- --test-threads=1
cargo test -p codex-code-mode-host --test stdio remote_session_persists_values_forwards_delegates_and_controls_cells -- --test-threads=1
cargo test -p codex-code-mode-host --test grpc cached_session_recovers_after_a_remote_host_restarts -- --test-threads=1
RUST_MIN_STACK=8388608 cargo test -p codex-core --test all code_mode_only_can_call_nested_tools -- --test-threads=1
cargo test -p codex-core --lib code_mode_session_provider_is_shared_across_threads -- --test-threads=1这些测试不证明所有远程网络故障或 V8 资源耗尽组合。它们分别验证 session/store 所有权、host transport 恢复和 nested permission 回路;gRPC、stdio 与 WebSocket 的完整协议兼容矩阵由后续专题承担。
下一篇CodeMode协议将从 ExecuteRequest、RuntimeResponse、host wire message 与 gRPC schema 展开跨进程契约。
