English 书架

协议边界

阅读契约: 把本章当成 vocabulary gate。跟踪 client request 怎样变成 queued submission,runtime 怎样返回 correlated events,app-server 怎样把这些 events 投射成 client-facing items,以及 generated schemas 怎样让这个边界可审计。

协议边界分类图:覆盖 submissions、operations、events、items、app-server messages、generated schemas 和 compatibility
Protocol vocabulary 是共享语言:submissions 进入,operations 携带 intent,events 报告 facts,items 投射 state,schemas 保持 clients 对齐。

源码边界: 本章 direct claims 固定到 OpenAI Codex commit 569ff6a1c400bd514ff79f5f1050a684dc3afde3SubmissionOpEventEventMsg、app-server JSON-RPC envelope types、event-to-item mapping、app-server request macros 和 schema export behavior 在链接处属于 verified source。“protocol kernel”、“projection”、“client contract”和“governance gate”等术语,是从这些 source shapes 得出的 surrounding contract inference;它们不是关于 OpenAI 私有服务内部的断言。

第 3 章构造了 runtime envelope:configuration、auth、managed requirements、feature state 和 permission profiles 会在后续代码开始 work 前解析完成。下一个边界是语言。一旦某个 surface 拥有 valid envelope,它仍然需要一种有纪律的方式来请求 work 并观察发生了什么。

这条纪律就是 protocol boundary。Codex 不让 client 伸进 session 调用任意 private methods。它给 clients 一组 durable nouns:submission、operation、event、item、request、response、notification、schema。重点不是 serialization aesthetics,而是 ownership。

如果一个概念跨越 protocol boundary,其他 code 就能依赖它。Terminal UI 可以 render 它。App-server client 可以 replay 它。Schema export 可以锁住它。旧 client 可以继续发送它。因此 protocol design 是 runtime behavior,而不是 documentation exercise。

一、Core Runtime Queue

1.1 Submission 与 Event Correlation

最小 core loop 在 codex-rs/protocol/src/protocol.rs 中可见。Submission 是 queue entry。它携带 correlation id、Op 和 optional W3C trace context。

/// Submission Queue Entry - requests from user
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct Submission {
    /// Unique id for this Submission to correlate with Events
    pub id: String,
    /// Payload
    pub op: Op,
    /// Optional W3C trace carrier propagated across async submission handoffs.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub trace: Option<W3cTraceContext>,
}

匹配的 outbound shape 是 Event。它携带 submission id 和 EventMsg payload。这种对称性是第一个 protocol invariant:

/// Event Queue Entry - events from agent
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Event {
    /// Submission `id` that this event is correlated with.
    pub id: String,
    /// Payload
    pub msg: EventMsg,
}

Client 可以提交 intent,随后把 emitted facts 关联回造成它们的 intent。Runtime 可以 serialize、reject、transform 或 interrupt operations,而不暴露 private state。Observability 可以跟随 trace 穿过 async handoffs,而不让 trace data 成为每个 domain payload 的一部分。

Core protocol kernel showing a submission with id, op, and trace entering a runtime queue and correlated events leaving with id and EventMsg
Core protocol 是 queue-shaped:submissions 把 intent 带入 runtime;events 把 correlated facts 带回来。

这个 split 很重要,因为 agent work 不是一次 function call。一个 turn 可能 stream tokens、request approval、run commands、emit deltas、apply patches、handle interrupts,并以 usage information 完成。Caller 不能等待一个 return value,然后把它称为完整 interaction。

1.2 Operations 是 Typed Entrances,不是 Arbitrary Commands

Op enum 是 runtime 的 typed entrance list。它从 lifecycle 和 realtime operations 开始,随后是 user-input operations、approval answers、permission responses、context operations、background terminal controls 和其他影响 thread 的 requests。关键阅读是:每个 operation 都是 source-level variant,而不是 string command。下面第一个锚点覆盖 turn-entry variants;approval、permission 和 dynamic-tool response variants 在同一个 enum 后面继续,约在 ExecApproval 附近。

Op 的删节源码展示了 pattern:

pub enum Op {
    /// Abort current task without terminating background terminal processes.
    /// This server sends [`EventMsg::TurnAborted`] in response.
    Interrupt,

    /// Legacy user input.
    ///
    /// Prefer [`Op::UserTurn`] so the caller provides full turn context
    /// (cwd/approval/sandbox/model/etc.) for each turn.
    UserInput {
        /// User input items, see `InputItem`
        items: Vec<UserInput>,
        /// Optional turn-scoped environments.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        environments: Option<Vec<TurnEnvironmentSelection>>,
        /// Optional JSON Schema used to constrain the final assistant message for this turn.
        #[serde(skip_serializing_if = "Option::is_none")]
        final_output_json_schema: Option<Value>,
        /// Optional turn-scoped Responses API `client_metadata`.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        responsesapi_client_metadata: Option<HashMap<String, String>>,
    },

    /// Similar to [`Op::UserInput`], but first applies persistent turn-context
    /// overrides in the same queued operation. This preserves submission order
    /// and prevents the input from starting if the overrides are rejected.
    UserInputWithTurnContext {
        items: Vec<UserInput>,
        environments: Option<Vec<TurnEnvironmentSelection>>,
        final_output_json_schema: Option<Value>,
        responsesapi_client_metadata: Option<HashMap<String, String>>,
        cwd: Option<PathBuf>,
        approval_policy: Option<AskForApproval>,
        // ...
    },
}

UserInputWithTurnContext 的注释是设计线索。Turn-context overrides 与 input 在同一个 queued operation 中应用。这保留了 ordering,并在 overrides 被拒绝时阻止 turn 开始。

这就是为什么“configuration”和“protocol”不能干净分开。第 3 章解析 envelope。第 4 章展示 turn context 的后续变化仍然必须穿过 typed operations,而不是 out of band mutation。

Entrance typeSource-level consequence
InterruptRuntime 可以 emit typed aborted event,而不是依赖 signal side channel。
UserInputLegacy input 仍被接受,这是 compatibility obligation。
UserInputWithTurnContextInput 和 context changes 被一起排序。
Approval and permission responsesClient decision 以 typed operation 重新进入,而不是 terminal text。
Realtime and background-terminal operationsLong-running subsystems 仍跨越同一个 queue boundary。

Runtime 强大,是因为 entrance 窄。Client 可以请求许多种 work,但每个 request 都必须变成 Op

1.3 Events 是 Facts,并携带 Compatibility Debt

EventMsg 远比 chat message enum 大。它包含 errors、warnings、realtime lifecycle、model reroutes、compaction、rollback、turn start and completion、token usage、agent messages、reasoning、MCP、web search、image generation、shell execution、approvals、permission requests、patch application、plan updates、shutdown、review mode、raw response items、item lifecycle、hooks、deltas 和 collaboration。

这种广度不是偶然。不同 event families 有不同的 persistence、display、replay 和 compatibility rules。Command output delta 不是 assistant paragraph。Permission request 不是 final history。Patch update 不是 model-visible instruction。Thread rollback 不是普通 message。

这个 enum 还携带 explicit compatibility details。直接源码 excerpt 展示 turn events 保留 v1 wire names,同时接受 v2 aliases:

/// Agent has started a turn.
/// v1 wire format uses `task_started`; accept `turn_started` for v2 interop.
#[serde(rename = "task_started", alias = "turn_started")]
TurnStarted(TurnStartedEvent),

/// Agent has completed all actions.
/// v1 wire format uses `task_complete`; accept `turn_complete` for v2 interop.
#[serde(rename = "task_complete", alias = "turn_complete")]
TurnComplete(TurnCompleteEvent),

同一个 enum 后面的删节源码展示 side-effect 和 client-decision events 作为 first-class variants:

ExecCommandBegin(ExecCommandBeginEvent),
ExecCommandOutputDelta(ExecCommandOutputDeltaEvent),
TerminalInteraction(TerminalInteractionEvent),
ExecCommandEnd(ExecCommandEndEvent),

ExecApprovalRequest(ExecApprovalRequestEvent),
RequestPermissions(RequestPermissionsEvent),
RequestUserInput(RequestUserInputEvent),
DynamicToolCallRequest(DynamicToolCallRequest),
ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent),

另一个直接 excerpt 展示 item/delta vocabulary:

RawResponseItem(RawResponseItemEvent),

ItemStarted(ItemStartedEvent),
ItemCompleted(ItemCompletedEvent),
HookStarted(HookStartedEvent),
HookCompleted(HookCompletedEvent),

AgentMessageContentDelta(AgentMessageContentDeltaEvent),
PlanDelta(PlanDeltaEvent),
ReasoningContentDelta(ReasoningContentDeltaEvent),
ReasoningRawContentDelta(ReasoningRawContentDeltaEvent),

这些 source shapes 支撑中心论断:Codex 暴露的不是“assistant 说了什么”这一种 event,而是一个 typed event language,用于 long-running agent turn 能创造的不同 facts。

二、App-Server Projection Contract

2.1 Events 变成 Client Items

Core events 不是最终 client contract。App-server boundary 必须把 selected runtime events 变成 client-visible notifications 和 items。源码在 event_mapping.rs 中显式给出这个 mapping。下面的删节源码保留 helper 的 documented boundary,省略 body:

/// Build the v2 app-server notification that directly corresponds to a single core event.
///
/// This only covers the stateless event-to-notification projections that have a one-to-one
/// mapping. Callers remain responsible for any surrounding state checks or side effects before
/// invoking this helper.
pub fn item_event_to_server_notification(
    msg: EventMsg,
    thread_id: &str,
    turn_id: &str,
) -> ServerNotification {
    // ...
}

注释很重要。这个 helper 不是整个 app-server state machine。它覆盖 stateless one-to-one projections。Surrounding code 仍拥有 checks 和 side effects。但这个 helper 固定了 projection rule:core EventMsg 可以变成 ServerNotification,而不让 clients parse terminal text。

Event-to-item projection showing core EventMsg families flowing through a mapping helper into ServerNotification item started, item completed, delta, and ThreadItem view
App-server projection 把 selected runtime events 变成稳定的 client-facing item 与 delta notifications。

一个删节 source excerpt 展示三类重要 mapping。Message deltas 保持 deltas。Item lifecycle events 变成 item notifications。Exec events 被转换成 command-execution items:

match msg {
    EventMsg::AgentMessageContentDelta(event) => {
        let codex_protocol::protocol::AgentMessageContentDeltaEvent { item_id, delta, .. } =
            event;
        ServerNotification::AgentMessageDelta(AgentMessageDeltaNotification {
            thread_id,
            turn_id,
            item_id,
            delta,
        })
    }
    EventMsg::ItemStarted(item_started_event) => {
        ServerNotification::ItemStarted(ItemStartedNotification {
            thread_id,
            turn_id,
            item: item_started_event.item.into(),
            started_at_ms: item_started_event.started_at_ms,
        })
    }
    EventMsg::ExecCommandOutputDelta(exec_command_output_delta_event) => {
        let item_id = exec_command_output_delta_event.call_id;
        let delta = String::from_utf8_lossy(&exec_command_output_delta_event.chunk).to_string();
        ServerNotification::CommandExecutionOutputDelta(
            CommandExecutionOutputDeltaNotification {
                thread_id,
                turn_id,
                item_id,
                delta,
            },
        )
    }
    EventMsg::ExecCommandEnd(exec_command_end_event) => {
        ServerNotification::ItemCompleted(ItemCompletedNotification {
            thread_id,
            turn_id,
            item: build_command_execution_end_item(&exec_command_end_event),
            completed_at_ms: exec_command_end_event.completed_at_ms,
        })
    }
    _ => unreachable!("unsupported item event"),
}

这个 projection 是 UI independence 的来源。TUI cell、app-server client 和 SDK stream 都可以 consume 同一个 item language,而不需要从 raw stdout 重建 command,或从 paragraph 重建 patch。

2.2 JSON-RPC-Style Envelope

App-server 是围绕 threads、turns、files、processes、MCP、plugins、accounts、permissions 和 remote-control flows 的第二条边界。它的低层 wire envelope 位于 jsonrpc_lite.rs。源码 note 很精确:Codex 不发送也不期待 "jsonrpc": "2.0" field,尽管文件保留了 familiar request/notification/response split。下面的删节源码把这个 note 与 message enum 放在一起:

//! We do not do true JSON-RPC 2.0, as we neither send nor expect the
//! "jsonrpc": "2.0" field.

/// Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
#[serde(untagged)]
pub enum JSONRPCMessage {
    Request(JSONRPCRequest),
    Notification(JSONRPCNotification),
    Response(JSONRPCResponse),
    Error(JSONRPCError),
}

Request、notification、response 和 error structs 的删节源码省略 derive 与 serde attributes,但保留 field shapes:

/// A request that expects a response.
pub struct JSONRPCRequest {
    pub id: RequestId,
    pub method: String,
    pub params: Option<serde_json::Value>,
    /// Optional W3C Trace Context for distributed tracing.
    pub trace: Option<W3cTraceContext>,
}

/// A notification which does not expect a response.
pub struct JSONRPCNotification {
    pub method: String,
    pub params: Option<serde_json::Value>,
}

/// A successful (non-error) response to a request.
pub struct JSONRPCResponse {
    pub id: RequestId,
    pub result: Result,
}

/// A response to a request that indicates an error occurred.
pub struct JSONRPCError {
    pub error: JSONRPCErrorError,
    pub id: RequestId,
}
App-server message envelope separating request, notification, response, and error objects with id, method, params, trace, result, and error fields
App-server 使用 JSON-RPC-style envelope,同时保持 Codex-specific request、notification、response 和 error obligations 显式。

这就是为什么 app-server 不是“runtime 外面套一层 HTTP”。Core runtime queue 知道 submissions 和 events。App-server 知道 connection-facing requests、responses、notifications、server-to-client requests、resource serialization、experimental gates 和 schema export。

2.3 Request Definitions 携带 Serialization Scope

App-server request surface 由 common.rs 中的 macro definitions 生成。Macro 之前,源码定义了 serialization scope enum:

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ClientRequestSerializationScope {
    Global(&'static str),
    GlobalSharedRead(&'static str),
    Thread { thread_id: String },
    ThreadPath { path: PathBuf },
    CommandExecProcess { process_id: String },
    Process { process_handle: String },
    FuzzyFileSearchSession { session_id: String },
    FsWatch { watch_id: String },
    McpOauth { server_name: String },
}

这是 protocol design choice。Request 不只是“method plus params”。它可以说哪个 resource 必须被 serialized:thread、thread path、command process、MCP OAuth server、filesystem watch、global config,或没有 serialized resource。

Macro body 的删节源码展示 generated ClientRequest methods 怎样暴露这个 scope;$variant markers 是 Rust macro variables,不是 pseudocode:

impl ClientRequest {
    pub fn id(&self) -> &RequestId {
        match self {
            $(Self::$variant { request_id, .. } => request_id,)*
        }
    }

    pub fn method(&self) -> String {
        serde_json::to_value(self)
            .ok()
            .and_then(|value| {
                value
                    .get("method")
                    .and_then(serde_json::Value::as_str)
                    .map(str::to_owned)
            })
            .unwrap_or_else(|| "<unknown>".to_string())
    }

    pub fn serialization_scope(&self) -> Option<ClientRequestSerializationScope> {
        match self {
            $(
                Self::$variant { params, .. } => {
                    let _ = params;
                    serialization_scope_expr!(
                        params, $serialization $( ( $($serialization_args)* ) )?
                    )
                }
            )*
        }
    }
}

真实 macro 会扩展到许多 methods。同一个文件中的几个例子展示了 variety:

Method familySource serialization clueWhy it matters
thread/resume, thread/forkthread_or_path(...)Request 可能指向 loaded thread 或 disk path。
thread/archive, thread/readthread_id(...)Thread state operations 按 thread serialization。
skills/listglobal_shared_read("config")某些 reads 可以共享 global config access。
hooks/list, marketplace operationsglobal("config")Config mutation 是 globally serialized。
command/exec/*optional_command_process_id(...), command_process_id(...)Process control 不能与自己 race。

Protocol boundary 因此携带 concurrency policy。如果 serialization scope 只存在于 private handler,client-facing request types 就更难审计。

三、Schema Governance

3.1 Generated Schemas 把 Drift 变成 Build Problem

最后一块是 governance。App-server protocol types derive JsonSchemaTS,但 repository 仍需要 export code 写出 stable artifacts,并过滤 experimental surface area。

export.rs 中的 top-level generator 导出 TypeScript 和 JSON schemas:

type JsonSchemaEmitter = fn(&Path) -> Result<GeneratedSchema>;
pub fn generate_types(out_dir: &Path, prettier: Option<&Path>) -> Result<()> {
    generate_ts(out_dir, prettier)?;
    generate_json(out_dir)?;
    Ok(())
}

TypeScript path 的删节源码展示 requests、responses、notifications 和 server-side counterparts 被导出,然后在没有显式请求 experimental output 时过滤 experimental types:

pub fn generate_ts_with_options(
    out_dir: &Path,
    prettier: Option<&Path>,
    options: GenerateTsOptions,
) -> Result<()> {
    let v2_out_dir = out_dir.join("v2");
    ensure_dir(out_dir)?;
    ensure_dir(&v2_out_dir)?;

    ClientRequest::export_all_to(out_dir)?;
    export_client_responses(out_dir)?;
    ClientNotification::export_all_to(out_dir)?;

    ServerRequest::export_all_to(out_dir)?;
    export_server_responses(out_dir)?;
    ServerNotification::export_all_to(out_dir)?;

    if !options.experimental_api {
        filter_experimental_ts(out_dir)?;
    }
    // ...
}

JSON path 的删节源码展示 envelope schemas、stable/experimental filtering,以及 root 和 v2 bundle writes。省略的中间部分会先收集 parameter、response 和 notification schemas,再 bundle:

let envelope_emitters: Vec<JsonSchemaEmitter> = vec![
    |d| write_json_schema_with_return::<crate::RequestId>(d, "RequestId"),
    |d| write_json_schema_with_return::<crate::JSONRPCMessage>(d, "JSONRPCMessage"),
    |d| write_json_schema_with_return::<crate::JSONRPCRequest>(d, "JSONRPCRequest"),
    |d| write_json_schema_with_return::<crate::JSONRPCNotification>(d, "JSONRPCNotification"),
    |d| write_json_schema_with_return::<crate::JSONRPCResponse>(d, "JSONRPCResponse"),
    |d| write_json_schema_with_return::<crate::JSONRPCError>(d, "JSONRPCError"),
    |d| write_json_schema_with_return::<crate::JSONRPCErrorError>(d, "JSONRPCErrorError"),
    |d| write_json_schema_with_return::<crate::ClientRequest>(d, "ClientRequest"),
    |d| write_json_schema_with_return::<crate::ServerRequest>(d, "ServerRequest"),
    |d| write_json_schema_with_return::<crate::ClientNotification>(d, "ClientNotification"),
    |d| write_json_schema_with_return::<crate::ServerNotification>(d, "ServerNotification"),
];

// ...

let mut bundle = build_schema_bundle(schemas)?;
if !experimental_api {
    filter_experimental_schema(&mut bundle)?;
}
write_pretty_json(
    out_dir.join("codex_app_server_protocol.schemas.json"),
    &bundle,
)?;
let flat_v2_bundle = build_flat_v2_schema(&bundle)?;
write_pretty_json(
    out_dir.join("codex_app_server_protocol.v2.schemas.json"),
    &flat_v2_bundle,
)?;
Generated schema governance path from Rust protocol types through schema export, TypeScript, JSON Schema, experimental filter, clients, and drift checks
Generated protocol artifacts 让 drift 可见:Rust types 必须穿过 TypeScript、JSON Schema 和 experimental filters,clients 才能依赖它们。

这就是 local API 与 boundary 的区别。Private Rust helper 可以安静 refactor。Protocol field 会跨入 generated artifacts 和 client code。一旦跨越,compatibility 就成为 behavior。

四、常见误读

第一个误读,是把 EventMsg 当成 fancy chat transcript。它是 runtime event vocabulary。有些 events 是 model output,但许多是 tool lifecycles、approval requests、patch updates、permission requests、reasoning deltas、hooks 或 collaboration records。

第二个误读,是把 app-server 称为“the protocol”并忽略 core submission/event queue。至少有两个可见边界:core runtime queue 和 app-server client envelope。它们有重叠,但不拥有同一组 concerns。

第三个误读,是认为 generated schemas 是 passive documentation。它们是 executable governance。它们让 protocol drift 对 build checks、generated clients 和 compatibility filters 可见。

第四个误读,是把旧 aliases 当成死注释删掉。v1/v2 turn-event aliases 展示 compatibility 可以直接生活在 source types 上。一旦 client 依赖某个 boundary,cleanup 的成本就不再等同于重构 private helper。

五、应用到实践

  1. 在源码中命名 boundary nouns。 如果 clients 依赖某个概念,就给它 typed protocol shape,而不是泄漏 private runtime state。
  2. 为每个 async handoff 做 correlation。 Queue entries、events、responses 和 errors 都应该携带足够 identity,说明它们回答哪个 request。
  3. 先 translation,再 rendering。 UIs 和 SDKs 应消费 typed item 与 notification shapes,而不是 scrape terminal strings。
  4. 把 concurrency 放进 contract。 如果 requests 必须按 thread、path、process 或 global config serialize,就让这个 scope 在 protocol code 中可见。
  5. 生成 client boundary。 TypeScript 和 JSON Schema exports 把 protocol drift 变成 reviewable artifact,而不是 runtime surprise。

六、结语

Part I 现在已经从外向内建立 contract。Distribution 到达 Rust router。Router 只有在 constrained envelope 存在后才开始。Work 随后跨越 typed protocol boundaries,而不是 private method calls。

Part II 可以打开 runtime 本身了。第 5 章跟踪 thread 和 session model:durable state、input queues、turn context、history、resume、fork 和 rollback 怎样让 protocol vocabulary 落在真实 runtime 上。

源码地图

概念源码锚点
Core Submission queue entrycodex-rs/protocol/src/protocol.rs#L123-L133
Op variants and turn-context orderingcodex-rs/protocol/src/protocol.rs#L403-L470
Approval, permission, and dynamic tool response operationscodex-rs/protocol/src/protocol.rs#L662-L719
Core Event shapecodex-rs/protocol/src/protocol.rs#L1247-L1254
EventMsg enum and v1/v2 aliasescodex-rs/protocol/src/protocol.rs#L1256-L1305
Side-effect and approval eventscodex-rs/protocol/src/protocol.rs#L1350-L1376
Item lifecycle and delta eventscodex-rs/protocol/src/protocol.rs#L1417-L1427
Event-to-notification mapping helpercodex-rs/app-server-protocol/src/protocol/event_mapping.rs#L25-L34
Event-to-item projection casescodex-rs/app-server-protocol/src/protocol/event_mapping.rs#L345-L449
JSON-RPC-style envelope note and message enumcodex-rs/app-server-protocol/src/jsonrpc_lite.rs#L1-L42
Request, notification, response, and error structscodex-rs/app-server-protocol/src/jsonrpc_lite.rs#L44-L88
Client request serialization scopescodex-rs/app-server-protocol/src/protocol/common.rs#L77-L88
Client request macro and serialization scope accessorcodex-rs/app-server-protocol/src/protocol/common.rs#L157-L221
Request definitions and example scopescodex-rs/app-server-protocol/src/protocol/common.rs#L434-L620
Command execution request scopescodex-rs/app-server-protocol/src/protocol/common.rs#L897-L920
Generated type entry pointcodex-rs/app-server-protocol/src/export.rs#L75-L80
TypeScript export and experimental filteringcodex-rs/app-server-protocol/src/export.rs#L101-L124
JSON Schema export, bundle writing, and filteringcodex-rs/app-server-protocol/src/export.rs#L192-L238