[codex] request desktop attestation from app (#20619)

## Summary

TL;DR: teaches `codex-rs` / app-server to request a desktop-provided
attestation token and attach it as `x-oai-attestation` on the scoped
ChatGPT Codex request paths.

![DeviceCheck attestation
interface](https://raw.githubusercontent.com/openai/codex/dev/jm/devicecheck-diagram-assets/pr-assets/devicecheck-attestation-interface.png)

## Details

This PR teaches the Codex app-server runtime how to request and attach
an attestation token. It does not generate DeviceCheck tokens directly;
instead, it relies on the connected desktop app to advertise that it can
generate attestation and then asks that app for a fresh header value
when needed.

The flow is:

1. The Codex desktop app connects to app-server.
2. During `initialize`, the app can advertise that it supports
`requestAttestation`.
3. Before app-server calls selected ChatGPT Codex endpoints, it sends
the internal server request `attestation/generate` to the app.
4. app-server receives a pre-encoded header value back.
5. app-server forwards that value as `x-oai-attestation` on the scoped
outbound requests.

The code in this repo is mostly protocol and runtime plumbing: it adds
the app-server request/response shape, introduces an attestation
provider in core, wires that provider into Responses / compaction /
realtime setup paths, and covers the intended scoping with tests. The
signed macOS DeviceCheck generation remains owned by the desktop app PR.

## Related PR

- Codex desktop app implementation:
https://github.com/openai/openai/pull/878649

## Validation

<details>
<summary>Tests run</summary>

```sh
cargo test -p codex-app-server-protocol
cargo test -p codex-core attestation --lib
cargo test -p codex-app-server --lib attestation
```

Also ran:

```sh
just fix -p codex-core
just fix -p codex-app-server
just fix -p codex-app-server-protocol
just fmt
just write-app-server-schema
```

</details>

<details>
<summary>E2E DeviceCheck validation</summary>

First validated the signed desktop app boundary directly: launched a
packaged signed `Codex.app`, sent `attestation/generate`, decoded the
returned `v1.` attestation header, and validated the extracted
DeviceCheck token with `personal/jm/verify_devicecheck_token.py` using
bundle ID `com.openai.codex`. Apple returned `status_code: 200` and
`is_ok: true`.

Then ran the fuller app + app-server flow. The packaged `Codex.app`
launched a current-branch app-server via `CODEX_CLI_PATH`, and a local
MITM proxy intercepted outbound `chatgpt.com` traffic. The app-server
requested `attestation/generate` from the real Electron app process, and
the intercepted `/backend-api/codex/responses` traffic included
`x-oai-attestation` on both routes:

```text
GET  /backend-api/codex/responses  Upgrade: websocket  x-oai-attestation: present
POST /backend-api/codex/responses  Upgrade: none       x-oai-attestation: present
```

The captured header decoded to a DeviceCheck token that also validated
with Apple for `com.openai.codex` (`status_code: 200`, `is_ok: true`,
team `2DC432GLL2`).

</details>

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Jiaming Zhang
2026-05-08 12:36:02 -07:00
committed by GitHub
parent 61142b6169
commit 5f4d0ec343
65 changed files with 1086 additions and 39 deletions

View File

@@ -0,0 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AttestationGenerateParams",
"type": "object"
}

View File

@@ -0,0 +1,14 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"token": {
"description": "Opaque client attestation token.",
"type": "string"
}
},
"required": [
"token"
],
"title": "AttestationGenerateResponse",
"type": "object"
}

View File

@@ -1259,6 +1259,11 @@
"array",
"null"
]
},
"requestAttestation": {
"default": false,
"description": "Opt into `attestation/generate` requests for upstream `x-oai-attestation`.",
"type": "boolean"
}
},
"type": "object"

View File

@@ -121,6 +121,9 @@
],
"type": "object"
},
"AttestationGenerateParams": {
"type": "object"
},
"ChatgptAuthTokensRefreshParams": {
"properties": {
"previousAccountId": {
@@ -1918,6 +1921,31 @@
"title": "Account/chatgptAuthTokens/refreshRequest",
"type": "object"
},
{
"description": "Generate a fresh upstream attestation result on demand.",
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"attestation/generate"
],
"title": "Attestation/generateRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/AttestationGenerateParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Attestation/generateRequest",
"type": "object"
},
{
"description": "DEPRECATED APIs below Request to approve a patch. This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).",
"properties": {

View File

@@ -83,6 +83,25 @@
"title": "ApplyPatchApprovalResponse",
"type": "object"
},
"AttestationGenerateParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AttestationGenerateParams",
"type": "object"
},
"AttestationGenerateResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"token": {
"description": "Opaque client attestation token.",
"type": "string"
}
},
"required": [
"token"
],
"title": "AttestationGenerateResponse",
"type": "object"
},
"ChatgptAuthTokensRefreshParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -2620,6 +2639,11 @@
"array",
"null"
]
},
"requestAttestation": {
"default": false,
"description": "Opt into `attestation/generate` requests for upstream `x-oai-attestation`.",
"type": "boolean"
}
},
"type": "object"
@@ -5207,6 +5231,31 @@
"title": "Account/chatgptAuthTokens/refreshRequest",
"type": "object"
},
{
"description": "Generate a fresh upstream attestation result on demand.",
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"attestation/generate"
],
"title": "Attestation/generateRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/AttestationGenerateParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Attestation/generateRequest",
"type": "object"
},
{
"description": "DEPRECATED APIs below Request to approve a patch. This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).",
"properties": {

View File

@@ -6409,6 +6409,11 @@
"array",
"null"
]
},
"requestAttestation": {
"default": false,
"description": "Opt into `attestation/generate` requests for upstream `x-oai-attestation`.",
"type": "boolean"
}
},
"type": "object"

View File

@@ -39,6 +39,11 @@
"array",
"null"
]
},
"requestAttestation": {
"default": false,
"description": "Opt into `attestation/generate` requests for upstream `x-oai-attestation`.",
"type": "boolean"
}
},
"type": "object"

View File

@@ -10,6 +10,10 @@ export type InitializeCapabilities = {
* Opt into receiving experimental API methods and fields.
*/
experimentalApi: boolean,
/**
* Opt into `attestation/generate` requests for upstream `x-oai-attestation`.
*/
requestAttestation: boolean,
/**
* Exact notification method names that should be suppressed for this
* connection (for example `thread/started`).

View File

@@ -4,6 +4,7 @@
import type { ApplyPatchApprovalParams } from "./ApplyPatchApprovalParams";
import type { ExecCommandApprovalParams } from "./ExecCommandApprovalParams";
import type { RequestId } from "./RequestId";
import type { AttestationGenerateParams } from "./v2/AttestationGenerateParams";
import type { ChatgptAuthTokensRefreshParams } from "./v2/ChatgptAuthTokensRefreshParams";
import type { CommandExecutionRequestApprovalParams } from "./v2/CommandExecutionRequestApprovalParams";
import type { DynamicToolCallParams } from "./v2/DynamicToolCallParams";
@@ -15,4 +16,4 @@ import type { ToolRequestUserInputParams } from "./v2/ToolRequestUserInputParams
/**
* Request initiated from the server and sent to the client.
*/
export type ServerRequest = { "method": "item/commandExecution/requestApproval", id: RequestId, params: CommandExecutionRequestApprovalParams, } | { "method": "item/fileChange/requestApproval", id: RequestId, params: FileChangeRequestApprovalParams, } | { "method": "item/tool/requestUserInput", id: RequestId, params: ToolRequestUserInputParams, } | { "method": "mcpServer/elicitation/request", id: RequestId, params: McpServerElicitationRequestParams, } | { "method": "item/permissions/requestApproval", id: RequestId, params: PermissionsRequestApprovalParams, } | { "method": "item/tool/call", id: RequestId, params: DynamicToolCallParams, } | { "method": "account/chatgptAuthTokens/refresh", id: RequestId, params: ChatgptAuthTokensRefreshParams, } | { "method": "applyPatchApproval", id: RequestId, params: ApplyPatchApprovalParams, } | { "method": "execCommandApproval", id: RequestId, params: ExecCommandApprovalParams, };
export type ServerRequest = { "method": "item/commandExecution/requestApproval", id: RequestId, params: CommandExecutionRequestApprovalParams, } | { "method": "item/fileChange/requestApproval", id: RequestId, params: FileChangeRequestApprovalParams, } | { "method": "item/tool/requestUserInput", id: RequestId, params: ToolRequestUserInputParams, } | { "method": "mcpServer/elicitation/request", id: RequestId, params: McpServerElicitationRequestParams, } | { "method": "item/permissions/requestApproval", id: RequestId, params: PermissionsRequestApprovalParams, } | { "method": "item/tool/call", id: RequestId, params: DynamicToolCallParams, } | { "method": "account/chatgptAuthTokens/refresh", id: RequestId, params: ChatgptAuthTokensRefreshParams, } | { "method": "attestation/generate", id: RequestId, params: AttestationGenerateParams, } | { "method": "applyPatchApproval", id: RequestId, params: ApplyPatchApprovalParams, } | { "method": "execCommandApproval", id: RequestId, params: ExecCommandApprovalParams, };

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AttestationGenerateParams = Record<string, never>;

View File

@@ -0,0 +1,9 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AttestationGenerateResponse = {
/**
* Opaque client attestation token.
*/
token: string, };

View File

@@ -28,6 +28,8 @@ export type { AppsDefaultConfig } from "./AppsDefaultConfig";
export type { AppsListParams } from "./AppsListParams";
export type { AppsListResponse } from "./AppsListResponse";
export type { AskForApproval } from "./AskForApproval";
export type { AttestationGenerateParams } from "./AttestationGenerateParams";
export type { AttestationGenerateResponse } from "./AttestationGenerateResponse";
export type { AutoReviewDecisionSource } from "./AutoReviewDecisionSource";
export type { ByteRange } from "./ByteRange";
export type { CancelLoginAccountParams } from "./CancelLoginAccountParams";

View File

@@ -1312,6 +1312,12 @@ server_request_definitions! {
response: v2::ChatgptAuthTokensRefreshResponse,
},
/// Generate a fresh upstream attestation result on demand.
AttestationGenerate => "attestation/generate" {
params: v2::AttestationGenerateParams,
response: v2::AttestationGenerateResponse,
},
/// DEPRECATED APIs below
/// Request to approve a patch.
/// This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).
@@ -1910,6 +1916,7 @@ mod tests {
},
capabilities: Some(v1::InitializeCapabilities {
experimental_api: true,
request_attestation: true,
opt_out_notification_methods: Some(vec![
"thread/started".to_string(),
"item/agentMessage/delta".to_string(),
@@ -1930,6 +1937,7 @@ mod tests {
},
"capabilities": {
"experimentalApi": true,
"requestAttestation": true,
"optOutNotificationMethods": [
"thread/started",
"item/agentMessage/delta"
@@ -1955,6 +1963,7 @@ mod tests {
},
"capabilities": {
"experimentalApi": true,
"requestAttestation": true,
"optOutNotificationMethods": [
"thread/started",
"item/agentMessage/delta"
@@ -1975,6 +1984,7 @@ mod tests {
},
capabilities: Some(v1::InitializeCapabilities {
experimental_api: true,
request_attestation: true,
opt_out_notification_methods: Some(vec![
"thread/started".to_string(),
"item/agentMessage/delta".to_string(),
@@ -2091,6 +2101,28 @@ mod tests {
Ok(())
}
#[test]
fn serialize_attestation_generate_request() -> Result<()> {
let params = v2::AttestationGenerateParams {};
let request = ServerRequest::AttestationGenerate {
request_id: RequestId::Integer(9),
params: params.clone(),
};
assert_eq!(
json!({
"method": "attestation/generate",
"id": 9,
"params": {}
}),
serde_json::to_value(&request)?,
);
let payload = ServerRequestPayload::AttestationGenerate(params);
assert_eq!(request.id(), &RequestId::Integer(9));
assert_eq!(payload.request_with_id(RequestId::Integer(9)), request);
Ok(())
}
#[test]
fn serialize_server_response() -> Result<()> {
let response = ServerResponse::CommandExecutionRequestApproval {

View File

@@ -46,6 +46,9 @@ pub struct InitializeCapabilities {
/// Opt into receiving experimental API methods and fields.
#[serde(default)]
pub experimental_api: bool,
/// Opt into `attestation/generate` requests for upstream `x-oai-attestation`.
#[serde(default)]
pub request_attestation: bool,
/// Exact notification method names that should be suppressed for this
/// connection (for example `thread/started`).
#[ts(optional = nullable)]

View File

@@ -0,0 +1,17 @@
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use ts_rs::TS;
#[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.
pub token: String,
}

View File

@@ -2,6 +2,7 @@ mod shared;
mod account;
mod apps;
mod attestation;
mod collaboration_mode;
mod command_exec;
mod config;
@@ -26,6 +27,7 @@ mod windows_sandbox;
pub use account::*;
pub use apps::*;
pub use attestation::*;
pub use collaboration_mode::*;
pub use command_exec::*;
pub use config::*;