Skip to content

Attestation流程

从 App Server capability 协商、thread 连接选择和即时 token 请求,追踪 x-oai-attestation 如何进入模型请求及其失败边界。

基于rust-v0.150.0
CodexRustSecurityIdentityAttestation

Attestation流程 ​

Codex 的 Attestation 不是 Agent Identity 注册的别名。Agent Identity 解决持久身份、task registration 和签名;这里的 Attestation 是模型请求发出前,由 host 向外部 App Server 客户端即时索取一个 opaque token,再把结果写入 x-oai-attestation header 的集成协议。

本文面向已经了解 Thread 和模型请求基本结构、但第一次阅读 App Server 反向请求的读者。建议先读AgentIdentity密钥模型区分两套身份材料,再读Keyring凭据存储理解认证状态的所有者;HTTP Client路由与中间件提供请求传输的更宽背景。本文只追踪 capability、连接选择、请求回调、header envelope 和消费者,不推断外部客户端如何访问硬件证明,也不把 opaque token 当作 Codex 已验证的证明。

读完后,读者应能从 InitializeCapabilities::request_attestation 走到 Responses、WebSocket、compaction 或 realtime 请求,解释没有客户端、超时、客户端错误和 malformed response 分别如何呈现。

这条链有两个所有权转换:App Server 保存“哪个连接愿意生成 token”,Core 保存“当前模型请求属于哪个 thread”。二者通过 AttestationContext.thread_id 汇合,因此 token 请求不会广播给所有客户端。

1. 协议边界 ​

Core 只定义 header 名、请求上下文和 provider trait。AttestationContext 当前只有 thread_id,没有 prompt、模型输入、nonce 或远端 challenge;具体 host 可以根据 thread 决定是否生成 header。

源码位置:codex-rs/core/src/attestation.rs :: AttestationContext、AttestationProvider

rust
pub(crate) const X_OAI_ATTESTATION_HEADER: &str = "x-oai-attestation";

pub type GenerateAttestationFuture<'a> =
    Pin<Box<dyn Future<Output = Option<HeaderValue>> + Send + 'a>>;

#[derive(Clone, Copy, Debug)]
pub struct AttestationContext {
    pub thread_id: ThreadId,
}

pub trait AttestationProvider: std::fmt::Debug + Send + Sync {
    fn header_for_request(&self, context: AttestationContext) -> GenerateAttestationFuture<'_>;
}

返回类型是 Option<HeaderValue>,因此“没有 attestation”不是请求错误。Core 不知道 token 的内部格式,也没有 attestation 验签接口;它只接受 host 返回的合法 HTTP header value。

App Server 协议进一步说明生成责任属于客户端。请求参数为空,响应只含一个 RedactedString token。空参数意味着 App Server 本身没有向客户端传递 challenge;“fresh”要求来自协议语义,而不是本文件中的 nonce 字段。

源码位置:codex-rs/app-server-protocol/src/protocol/v2/attestation.rs :: AttestationGenerateParams、AttestationGenerateResponse

rust
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, Default)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AttestationGenerateParams {}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AttestationGenerateResponse {
    /// Opaque client attestation token.
    #[ts(type = "string")]
    pub token: RedactedString,
}

RedactedString 保护 Rust Debug 输出,不改变 JSON/TypeScript wire 类型。客户端仍发送普通 JSON string,App Server 也仍会在内存中持有明文 token。

2. Provider所有权 ​

App Server 构造 ThreadManager 时注入一个 AppServerAttestationProvider。这个 provider 随 manager 进入新 session 的 service state,并在创建 ModelClient 时继续传入;subagent 和 Guardian reviewer 也继承父 session 的 provider。

相关源码:

  • codex-rs/app-server/src/message_processor.rs :: MessageProcessor::new
  • codex-rs/core/src/thread_manager.rs :: ThreadManager::new
  • codex-rs/core/src/session/session.rs :: Session::new
  • codex-rs/core/src/codex_delegate.rs :: spawn_delegate

源码位置:codex-rs/app-server/src/message_processor.rs :: app_server_attestation_provider

rust
Some(app_server_attestation_provider(
    outgoing.clone(),
    thread_state_manager.clone(),
)),
Some(app_server_time_provider(
    outgoing.clone(),
    thread_state_manager.clone(),
)),

源码位置:codex-rs/core/src/session/session.rs :: ModelClient::new

rust
model_client: ModelClient::new(
    Some(Arc::clone(&auth_manager)),
    if config.features.enabled(Feature::UseAgentIdentity) {
        AgentIdentityAuthPolicy::ChatGptAuth
    } else {
        AgentIdentityAuthPolicy::JwtOnly
    },
    thread_id,
    session_configuration.provider.info().clone(),
    session_configuration.session_source.clone(),
    session_configuration.originator.clone(),
    config.model_verbosity,
    config.features.enabled(Feature::ContentItemKinds),
    config.features.enabled(Feature::EnableRequestCompression),
    config.features.enabled(Feature::RuntimeMetrics),
    Self::build_model_client_beta_features_header(config.as_ref()),
    /*concurrent_reasoning_summaries_enabled*/ config
        .features
        .enabled(Feature::ConcurrentReasoningSummaries),
    attestation_provider,
    config.http_client_factory(),
)
.with_prompt_cache_key_override(
    crate::guardian::prompt_cache_key_override_for_review_session(
        &session_configuration.session_source,
        session_configuration.parent_thread_id,
    ),
),

Provider 的生命周期至少覆盖 session。App Server 实现内部把 OutgoingMessageSender 保存为 Weak,避免 provider 与消息发送器互相形成强引用环;如果发送器已经释放,header_for_request 直接返回 None。

源码位置:codex-rs/app-server/src/attestation.rs :: AppServerAttestationProvider::header_for_request

rust
fn header_for_request(&self, context: AttestationContext) -> GenerateAttestationFuture<'_> {
    let Some(outgoing) = self.outgoing.upgrade() else {
        return Box::pin(async { None });
    };
    let thread_state_manager = self.thread_state_manager.clone();
    Box::pin(async move {
        request_attestation_header_value_with_timeout(
            outgoing,
            thread_state_manager,
            context.thread_id,
            ATTESTATION_GENERATE_TIMEOUT,
        )
        .await
        .and_then(|value| HeaderValue::from_bytes(value.as_bytes()).ok())
    })
}

3. 启用条件 ​

是否请求 attestation 有两道独立开关。第一道位于模型 provider:默认 supports_attestation() 为 false,ConfiguredModelProvider 只有在 auth manager 当前缓存的是 ChatGPT auth 时返回 true。ModelClient::new 把结果复制到 include_attestation,这是构造期决定,不是每次请求重新查询 auth 类型。

源码位置:codex-rs/model-provider/src/provider.rs :: ConfiguredModelProvider::supports_attestation

rust
fn supports_attestation(&self) -> bool {
    self.auth_manager
        .as_ref()
        .and_then(|auth_manager| auth_manager.auth_cached())
        .is_some_and(|auth| auth.is_chatgpt_auth())
}

源码位置:codex-rs/core/src/client.rs :: ModelClient::new、ModelClient::generate_attestation_header_for

rust
let include_attestation = model_provider.supports_attestation();
Self {
    state: Arc::new(ModelClientState {
        thread_id,
        provider: model_provider,
        auth_env_telemetry,
        session_source,
        originator,
        model_verbosity,
        include_attestation,
        attestation_provider,
        http_client_factory,
        codex_api_key_env_enabled,
        content_item_kinds_enabled,
        enable_request_compression,
        include_timing_metrics,
        beta_features_header,
        concurrent_reasoning_summaries_enabled,
        prompt_cache_key_override: None,
    }),
}

第二道开关来自 App Server 客户端。客户端必须在 initialize 时设置 requestAttestation: true;字段默认 false,所以旧客户端不会收到未知的反向请求。

源码位置:codex-rs/app-server-protocol/src/protocol/v1.rs :: InitializeCapabilities

rust
pub struct InitializeCapabilities {
    #[serde(default)]
    pub experimental_api: bool,
    /// Opt into `attestation/generate` requests for upstream `x-oai-attestation`.
    #[serde(default)]
    pub request_attestation: bool,
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub mcp_server_openai_form_elicitation: bool,
    #[ts(optional = nullable)]
    pub opt_out_notification_methods: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[ts(optional = nullable)]
    pub extensions: Option<HashMap<String, serde_json::Value>>,
}

因此 ChatGPT auth 只表示 Core“愿意尝试”,client capability 表示“这个连接能够响应”。任一条件不成立,请求仍可继续,只是不带 attestation header。

4. 连接选择 ​

initialize processor 把 capability 保存到连接 session,随后 connection_initialized 将其写入 ThreadStateManager.live_connections。这里只登记连接能力,还没有与具体 thread 绑定;连接必须订阅或启动 thread,才会出现在该 thread 的 connection_ids 中。

源码位置:codex-rs/app-server/src/request_processors/initialize_processor.rs :: InitializeProcessor::process_initialize

rust
let capabilities = params.capabilities.unwrap_or_default();
let experimental_api_enabled = capabilities.experimental_api;
let request_attestation = capabilities.request_attestation;
let extensions = capabilities.extensions.as_ref();
let client_mcp_extensions = codex_mcp::client_mcp_extensions(
    extensions,
    capabilities.mcp_server_openai_form_elicitation,
);

源码位置:codex-rs/app-server/src/thread_state.rs :: ConnectionCapabilities、first_attestation_capable_connection_for_thread

rust
#[derive(Clone, Copy, Default)]
pub(crate) struct ConnectionCapabilities {
    pub(crate) request_attestation: bool,
}

pub(crate) async fn first_attestation_capable_connection_for_thread(
    &self,
    thread_id: ThreadId,
) -> Option<ConnectionId> {
    let state = self.state.lock().await;
    state
        .threads
        .get(&thread_id)?
        .connection_ids
        .iter()
        .filter_map(|connection_id| {
            state
                .live_connections
                .get(connection_id)?
                .request_attestation
                .then_some(*connection_id)
        })
        .min_by_key(|connection_id| connection_id.0)
}

选择器只考虑当前 thread 的订阅者,再取最小 ConnectionId。这提供了稳定的单连接选择:不会向所有支持者重复请求 token,也不会误用只订阅其他 thread 的客户端。如果 thread 不存在、没有订阅者或订阅者均未声明 capability,函数返回 None。

5. 反向请求 ​

App Server 选择连接后,通过 OutgoingMessageSender 发出 attestation/generate。请求只发给一个 ConnectionId,参数为空。发送器先把 oneshot callback 放进 request_id_to_callback,再投递 JSON-RPC request;客户端 response 或 error 会取走 callback 并唤醒等待者。

源码位置:codex-rs/app-server/src/attestation.rs :: request_attestation_header_value_with_timeout

rust
let connection_id = thread_state_manager
    .first_attestation_capable_connection_for_thread(thread_id)
    .await?;

let connection_ids = [connection_id];
let (request_id, rx) = outgoing
    .send_request_to_connections(
        Some(&connection_ids),
        ServerRequestPayload::AttestationGenerate(AttestationGenerateParams {}),
        /*thread_id*/ None,
    )
    .await;

源码位置:codex-rs/app-server/src/outgoing_message.rs :: OutgoingMessageSender::send_request_to_connections

rust
let (tx_approve, rx_approve) = oneshot::channel();
{
    let mut request_id_to_callback = self.request_id_to_callback.lock().await;
    request_id_to_callback.insert(
        id,
        PendingCallbackEntry {
            callback: tx_approve,
            thread_id,
            request: request.clone(),
            _diagnostics_guard: PENDING_SERVER_REQUESTS.track(),
        },
    );
}

let outgoing_message = OutgoingMessage::Request(request.clone());
let send_result = match connection_ids {
    None => {
        self.sender
            .send(OutgoingEnvelope::Broadcast {
                message: outgoing_message,
            })
            .await
    }
    Some(connection_ids) => {
        let mut send_error = None;
        for connection_id in connection_ids {
            if let Err(err) = self
                .sender
                .send(OutgoingEnvelope::ToConnection {
                    connection_id: *connection_id,
                    message: outgoing_message.clone(),
                    write_complete_tx: None,
                })
                .await
            {
                send_error = Some(err);
                break;
            } else {
                self.analytics_events_client
                    .track_server_request(connection_id.0, request.clone());
            }
        }
        match send_error {
            Some(err) => Err(err),
            None => Ok(()),
        }
    }
};

Attestation 请求传入的 thread_id 参数是 None。thread 只用于选择连接,不用于把 callback 登记成“随该 thread 状态变化取消”的请求;它由固定的短超时负责结束和清理。

6. Header封装 ​

客户端成功返回 token 后,App Server 不把原 token 直接作为 header,而是生成版本化 envelope:v 是格式版本,s 是 App Server 观察到的状态,t 只在成功时存在。

源码位置:codex-rs/app-server/src/attestation.rs :: AppServerAttestationStatus、app_server_attestation_header_value

rust
impl AppServerAttestationStatus {
    const fn code(self) -> u8 {
        match self {
            Self::Ok => 0,
            Self::Timeout => 1,
            Self::RequestFailed => 2,
            Self::RequestCanceled => 3,
            Self::MalformedResponse => 4,
        }
    }
}

#[derive(Serialize)]
struct AppServerAttestationEnvelope<'a> {
    v: u8,
    s: u8,
    #[serde(skip_serializing_if = "Option::is_none")]
    t: Option<&'a str>,
}

fn app_server_attestation_header_value(
    status: AppServerAttestationStatus,
    token: Option<&str>,
) -> Option<String> {
    serde_json::to_string(&AppServerAttestationEnvelope {
        v: 1,
        s: status.code(),
        t: token,
    })
    .map_err(|err| warn!("failed to serialize app-server attestation envelope: {err}"))
    .ok()
}

状态码表示本地生成链路结果,不是上游对证明有效性的判断:

s本地含义token 字段
0客户端返回可解析响应存在
1100ms 内未完成不存在
2客户端返回 JSON-RPC error不存在
3callback channel 被取消不存在
4response 不能解析为约定类型不存在

失败 envelope 仍会成为 x-oai-attestation,让 upstream 区分“尝试失败”和“完全没有可用 provider”。完全没有支持连接、sender 已释放、envelope 序列化失败或不能转换为 HeaderValue 时,Core 得到 None,请求不带该 header。

7. 超时与脱敏 ​

App Server 只等待 100ms。超时后调用 cancel_request 从 callback map 移除 pending entry,接收端随之关闭;这不是向客户端发送 JSON-RPC cancel,而是停止在服务端等待迟到响应,并记录 aborted analytics。

源码位置:codex-rs/app-server/src/attestation.rs :: request_attestation_header_value_with_timeout

rust
Err(_) => {
    let _canceled = outgoing.cancel_request(&request_id).await;
    warn!(
        timeout_seconds = timeout_duration.as_secs(),
        "attestation generation request timed out"
    );
    return app_server_attestation_header_value(
        AppServerAttestationStatus::Timeout,
        /*token*/ None,
    );
}

源码位置:codex-rs/app-server/src/outgoing_message.rs :: OutgoingMessageSender::cancel_request

rust
pub(crate) async fn cancel_request(&self, id: &RequestId) -> bool {
    let entry = self.take_request_callback(id).await;
    if let Some((request_id, _entry)) = entry {
        self.analytics_events_client
            .track_server_request_aborted(now_unix_timestamp_ms(), request_id);
        true
    } else {
        false
    }
}

错误日志刻意不记录客户端 error message 或 malformed response 内容,因为这些位置可能携带 token。成功响应使用 RedactedString,集成测试还会等待日志持久化屏障,再检查 SQLite 和 feedback logs 都不含 attestation token。

源码位置:codex-rs/app-server/src/attestation.rs :: request_attestation_header_value_with_timeout

rust
Ok(Ok(Err(err))) => {
    // Don't log err.message because it may contain a token.
    warn!(code = err.code, "attestation generation request failed");
    return app_server_attestation_header_value(
        AppServerAttestationStatus::RequestFailed,
        /*token*/ None,
    );
}

这个边界只约束现有日志调用点。Token 仍会进入 JSON-RPC response、envelope string、HTTP header 和进程内存,不能把日志脱敏等同于秘密从未以明文存在。

8. 请求消费者 ​

generate_attestation_header_for 先检查构造期的 include_attestation,再调用 provider,并始终携带当前 thread_id。它不是一次 session 缓存:每个需要 header 的请求点都会重新调用 provider,因此外部客户端可以生成即时 token。

源码位置:codex-rs/core/src/client.rs :: ModelClient::generate_attestation_header_for

rust
async fn generate_attestation_header_for(&self) -> Option<HeaderValue> {
    if !self.state.include_attestation {
        return None;
    }

    self.state
        .attestation_provider
        .as_ref()?
        .header_for_request(AttestationContext {
            thread_id: self.state.thread_id,
        })
        .await
}

当前消费者不只普通 Responses HTTP。至少包括:

  • /responses/compact 的远端压缩请求;
  • realtime WebRTC call 创建与 sideband WebSocket headers;
  • Responses WebSocket handshake;
  • 普通 Responses request options。

源码位置:codex-rs/core/src/client.rs :: ModelClient::compact_conversation_history

rust
extra_headers.extend(build_session_headers(
    Some(responses_metadata.session_id.to_string()),
    Some(responses_metadata.thread_id.to_string()),
));
if let Some(header_value) = self.generate_attestation_header_for().await {
    extra_headers.insert(X_OAI_ATTESTATION_HEADER, header_value);
}
if let Some(header_value) = self.build_routing_hint_header(
    client_setup.auth.as_ref(),
    &model,
    service_tier.as_deref(),
) {
    extra_headers.insert(X_CODEX_ROUTING_HINT_HEADER, header_value);
}

源码位置:codex-rs/core/src/client.rs :: ModelClient::build_websocket_headers

rust
if let Some(routing_hint) = &responses_metadata.routing_hint {
    headers.insert(X_CODEX_ROUTING_HINT_HEADER, routing_hint.clone());
}
if let Some(header_value) = self.generate_attestation_header_for().await {
    headers.insert(X_OAI_ATTESTATION_HEADER, header_value);
}
headers.insert(
    OPENAI_BETA_HEADER,
    HeaderValue::from_static(RESPONSES_WEBSOCKETS_V2_BETA_HEADER_VALUE),
);

因为生成发生在各传输的 header 构造阶段,同一 thread 的后续请求可能获得不同 token。Core 不做 token 复用、持久化或比较;这些都由外部客户端与 upstream 协议决定。

9. 失败状态 ​

所有分支最终都允许模型请求继续。区别只在 upstream 是否看到 header,以及 header 是成功 token 还是本地失败 envelope。因此 Attestation 在当前客户端路径中是附加信号,不是阻止请求发出的本地授权屏障。

10. 测试路径 ​

Core 的 counting provider 测试输入一个每次调用都返回不同值的 mock provider。WebSocket handshake 的断言同时检查 header 为 v1.header-1、调用次数为 1;另一个测试用 OSS provider 关闭 include_attestation,连续模拟 Responses、compaction 和 realtime 三处调用,断言 header 全部缺失且 provider 调用次数为 0。这证明 gating 在 provider 调用之前发生。

源码位置:codex-rs/core/src/client_tests.rs :: websocket_handshake_includes_attestation_for_chatgpt_codex_responses、non_chatgpt_codex_endpoints_omit_attestation_generation

rust
let headers = model_client
    .build_websocket_headers(&responses_metadata)
    .await;

assert_eq!(
    headers
        .get(crate::attestation::X_OAI_ATTESTATION_HEADER)
        .and_then(|value| value.to_str().ok()),
    Some("v1.header-1"),
);
assert_eq!(attestation_calls.load(Ordering::Relaxed), 1);

ThreadStateManager 测试创建四个连接:一个只订阅其他 thread,两个支持 attestation 并订阅目标 thread,一个订阅目标 thread 但不支持。断言目标 thread 选择较小的支持连接,其他 thread 选择自己的订阅者。它验证“capability + thread membership + deterministic minimum”三个条件,而不涉及网络请求。

源码位置:codex-rs/app-server/src/request_processors/thread_processor_tests.rs :: first_attestation_capable_connection_for_thread_only_uses_thread_subscribers

rust
assert_eq!(
    manager
        .first_attestation_capable_connection_for_thread(thread_id)
        .await,
    Some(earlier_supported_connection)
);
assert_eq!(
    manager
        .first_attestation_capable_connection_for_thread(other_thread_id)
        .await,
    Some(unrelated_supported_connection)
);

App Server 端到端测试用 requestAttestation: true 初始化连接,启动 thread 和 turn,接收 attestation/generate 后返回固定 token,最后读取 mock Responses WebSocket handshake。关键断言是 upstream 收到完整 envelope,而不是裸 token。

源码位置:codex-rs/app-server/tests/suite/v2/attestation.rs :: attestation_generate_round_trip_adds_header_to_responses_websocket_handshake

rust
let handshake = websocket_server.single_handshake();
assert_eq!(
    handshake.header("x-oai-attestation").as_deref(),
    Some(APP_SERVER_ATTESTATION_HEADER)
);

这些测试不覆盖真实平台 attestation API、远端 token 验证、nonce freshness 或硬件密钥隔离。100ms timeout、客户端 error、channel cancellation 和 malformed response 的状态编码有单元测试,但没有在本文所述端到端 fixture 中逐个注入。

可执行的近场验证命令如下:

text
cd codex-rs
cargo test -p codex-core --lib attestation -- --nocapture --test-threads=1
cargo test -p codex-app-server --lib attestation -- --nocapture --test-threads=1
cargo test -p codex-app-server --lib first_attestation_capable_connection -- --nocapture --test-threads=1
cargo test -p codex-app-server --test all attestation_generate_round_trip -- --nocapture --test-threads=1

补充 attestation 的请求闭环:连接能力决定是否反向请求,生成结果最终只进入支持该协议的 header。

11. 源码定位 ​

遇到“upstream 没有 x-oai-attestation”时,可以按所有权倒序定位:先在 core/src/client.rs 检查 provider gate 与具体传输是否调用 generate_attestation_header_for;再在 app-server/src/thread_state.rs 检查该 thread 是否存在 capable subscriber;最后确认客户端是否收到并响应 attestation/generate。

如果 upstream 收到 s=1..4,说明 provider 和连接选择已经成功,问题位于客户端响应阶段;如果 header 完全缺失,则优先检查 ChatGPT auth gate、capability、thread subscription、provider 生命周期或 header 转换。完成这组定位后,再阅读Guardian审查架构时,应把 Guardian 的模型审查与这里的 host attestation 信号视为两条独立安全链路。