mirror of
https://github.com/openai/codex.git
synced 2026-05-18 02:02:30 +00:00
## 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.  ## 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>
249 lines
8.5 KiB
Rust
249 lines
8.5 KiB
Rust
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
|
|
use codex_protocol::ThreadId;
|
|
use codex_protocol::config_types::ForcedLoginMethod;
|
|
use codex_protocol::config_types::ReasoningSummary;
|
|
use codex_protocol::config_types::SandboxMode;
|
|
use codex_protocol::config_types::Verbosity;
|
|
use codex_protocol::openai_models::ReasoningEffort;
|
|
use codex_protocol::parse_command::ParsedCommand;
|
|
use codex_protocol::protocol::AskForApproval;
|
|
use codex_protocol::protocol::FileChange;
|
|
pub use codex_protocol::protocol::GitSha;
|
|
use codex_protocol::protocol::ReviewDecision;
|
|
use codex_protocol::protocol::SandboxPolicy;
|
|
use codex_protocol::protocol::SessionSource;
|
|
use codex_protocol::protocol::TurnAbortReason;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use ts_rs::TS;
|
|
|
|
use crate::protocol::common::AuthMode;
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct InitializeParams {
|
|
pub client_info: ClientInfo,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub capabilities: Option<InitializeCapabilities>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ClientInfo {
|
|
pub name: String,
|
|
pub title: Option<String>,
|
|
pub version: String,
|
|
}
|
|
|
|
/// Client-declared capabilities negotiated during initialize.
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
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)]
|
|
pub opt_out_notification_methods: Option<Vec<String>>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct InitializeResponse {
|
|
pub user_agent: String,
|
|
/// Absolute path to the server's $CODEX_HOME directory.
|
|
pub codex_home: AbsolutePathBuf,
|
|
/// Platform family for the running app-server target, for example
|
|
/// `"unix"` or `"windows"`.
|
|
pub platform_family: String,
|
|
/// Operating system for the running app-server target, for example
|
|
/// `"macos"`, `"linux"`, or `"windows"`.
|
|
pub platform_os: String,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(untagged)]
|
|
pub enum GetConversationSummaryParams {
|
|
RolloutPath {
|
|
#[serde(rename = "rolloutPath")]
|
|
rollout_path: PathBuf,
|
|
},
|
|
ThreadId {
|
|
#[serde(rename = "conversationId")]
|
|
conversation_id: ThreadId,
|
|
},
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GetConversationSummaryResponse {
|
|
pub summary: ConversationSummary,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ConversationSummary {
|
|
pub conversation_id: ThreadId,
|
|
pub path: PathBuf,
|
|
pub preview: String,
|
|
pub timestamp: Option<String>,
|
|
pub updated_at: Option<String>,
|
|
pub model_provider: String,
|
|
pub cwd: PathBuf,
|
|
pub cli_version: String,
|
|
pub source: SessionSource,
|
|
pub git_info: Option<ConversationGitInfo>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct ConversationGitInfo {
|
|
pub sha: Option<String>,
|
|
pub branch: Option<String>,
|
|
pub origin_url: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct LoginApiKeyParams {
|
|
pub api_key: String,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GitDiffToRemoteResponse {
|
|
pub sha: GitSha,
|
|
pub diff: String,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ApplyPatchApprovalParams {
|
|
pub conversation_id: ThreadId,
|
|
/// Use to correlate this with [codex_protocol::protocol::PatchApplyBeginEvent]
|
|
/// and [codex_protocol::protocol::PatchApplyEndEvent].
|
|
pub call_id: String,
|
|
pub file_changes: HashMap<PathBuf, FileChange>,
|
|
/// Optional explanatory reason (e.g. request for extra write access).
|
|
pub reason: Option<String>,
|
|
/// When set, the agent is asking the user to allow writes under this root
|
|
/// for the remainder of the session (unclear if this is honored today).
|
|
pub grant_root: Option<PathBuf>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ApplyPatchApprovalResponse {
|
|
pub decision: ReviewDecision,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExecCommandApprovalParams {
|
|
pub conversation_id: ThreadId,
|
|
/// Use to correlate this with [codex_protocol::protocol::ExecCommandBeginEvent]
|
|
/// and [codex_protocol::protocol::ExecCommandEndEvent].
|
|
pub call_id: String,
|
|
/// Identifier for this specific approval callback.
|
|
pub approval_id: Option<String>,
|
|
pub command: Vec<String>,
|
|
pub cwd: PathBuf,
|
|
pub reason: Option<String>,
|
|
pub parsed_cmd: Vec<ParsedCommand>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
pub struct ExecCommandApprovalResponse {
|
|
pub decision: ReviewDecision,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GitDiffToRemoteParams {
|
|
pub cwd: PathBuf,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GetAuthStatusParams {
|
|
pub include_token: Option<bool>,
|
|
pub refresh_token: Option<bool>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExecOneOffCommandParams {
|
|
pub command: Vec<String>,
|
|
pub timeout_ms: Option<u64>,
|
|
pub cwd: Option<PathBuf>,
|
|
pub sandbox_policy: Option<SandboxPolicy>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GetAuthStatusResponse {
|
|
pub auth_method: Option<AuthMode>,
|
|
pub auth_token: Option<String>,
|
|
pub requires_openai_auth: Option<bool>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct UserSavedConfig {
|
|
pub approval_policy: Option<AskForApproval>,
|
|
pub sandbox_mode: Option<SandboxMode>,
|
|
pub sandbox_settings: Option<SandboxSettings>,
|
|
pub forced_chatgpt_workspace_id: Option<String>,
|
|
pub forced_login_method: Option<ForcedLoginMethod>,
|
|
pub model: Option<String>,
|
|
pub model_reasoning_effort: Option<ReasoningEffort>,
|
|
pub model_reasoning_summary: Option<ReasoningSummary>,
|
|
pub model_verbosity: Option<Verbosity>,
|
|
pub tools: Option<Tools>,
|
|
pub profile: Option<String>,
|
|
pub profiles: HashMap<String, Profile>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Profile {
|
|
pub model: Option<String>,
|
|
pub model_provider: Option<String>,
|
|
pub approval_policy: Option<AskForApproval>,
|
|
pub model_reasoning_effort: Option<ReasoningEffort>,
|
|
pub model_reasoning_summary: Option<ReasoningSummary>,
|
|
pub model_verbosity: Option<Verbosity>,
|
|
pub chatgpt_base_url: Option<String>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Tools {
|
|
pub web_search: Option<bool>,
|
|
pub view_image: Option<bool>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct SandboxSettings {
|
|
#[serde(default)]
|
|
pub writable_roots: Vec<AbsolutePathBuf>,
|
|
pub network_access: Option<bool>,
|
|
pub exclude_tmpdir_env_var: Option<bool>,
|
|
pub exclude_slash_tmp: Option<bool>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct InterruptConversationResponse {
|
|
pub abort_reason: TurnAbortReason,
|
|
}
|