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
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
#[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::newcodex-rs/core/src/thread_manager.rs::ThreadManager::newcodex-rs/core/src/session/session.rs::Session::newcodex-rs/core/src/codex_delegate.rs::spawn_delegate
源码位置:codex-rs/app-server/src/message_processor.rs :: app_server_attestation_provider
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
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
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
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
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
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
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
#[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
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
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
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 | 客户端返回可解析响应 | 存在 |
| 1 | 100ms 内未完成 | 不存在 |
| 2 | 客户端返回 JSON-RPC error | 不存在 |
| 3 | callback channel 被取消 | 不存在 |
| 4 | response 不能解析为约定类型 | 不存在 |
失败 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
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
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
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
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
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
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
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
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
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 中逐个注入。
可执行的近场验证命令如下:
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 信号视为两条独立安全链路。
