From 5ea3740de3c2ba46be2acfa697213e7f7dce85ec Mon Sep 17 00:00:00 2001 From: Zyy Date: Thu, 27 Aug 2026 13:27:10 +0800 Subject: [PATCH] refactor(openai): remove agent identity support Co-authored-by: Codex --- README.md | 4 +- backend/Cargo.lock | 179 ------- backend/crates/providers/openai/Cargo.toml | 2 - .../providers/openai/src/credential/admin.rs | 470 +++------------- .../openai/src/credential/agent_identity.rs | 500 ------------------ .../openai/src/credential/catalog.rs | 48 +- .../providers/openai/src/credential/mod.rs | 15 +- .../openai/src/credential/quota/mod.rs | 249 +++------ .../openai/src/credential/security.rs | 89 +--- .../openai/src/credential/selector.rs | 22 +- .../providers/openai/src/credential/types.rs | 64 +-- backend/crates/providers/openai/src/lib.rs | 21 +- .../crates/providers/openai/src/provider.rs | 121 +---- .../openai/tests/credential/agent_identity.rs | 256 --------- .../openai/tests/credential/catalog.rs | 3 +- .../openai/tests/credential/contract.rs | 6 +- .../providers/openai/tests/credential/mod.rs | 1 - .../openai/tests/credential/quota/mod.rs | 1 - .../openai/tests/provider/contract.rs | 8 +- .../crates/providers/openai/tests/support.rs | 32 +- docs/api.md | 22 +- docs/architecture.md | 4 +- frontend/src/components/ProviderIconGroup.vue | 9 +- .../AccountCreateModal/presenter.ts | 10 - skills/codex-proxy-rs/SKILL.md | 2 +- .../codex-proxy-rs/references/repo-guide.md | 2 +- 26 files changed, 199 insertions(+), 1941 deletions(-) delete mode 100644 backend/crates/providers/openai/src/credential/agent_identity.rs delete mode 100644 backend/crates/providers/openai/tests/credential/agent_identity.rs diff --git a/README.md b/README.md index d3fccaeaa..cc1323ab3 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ | Provider | 固定 `openai`、`xai` 两个 Provider,各自拥有 OAuth、credential、额度、目录与 transport | | 路由 | Client Key 限定账号分组;按模型能力编译 Provider 候选;支持全局精确模型映射、会话亲和和安全 fallback | | 透明边界 | OpenAI 保留未知请求字段及字段顺序,并原样转发 SSE、WebSocket 与 Images 业务字节;xAI 在 Provider 内转换协议 | -| 账号 | OAuth、OpenAI AT/RT、账号文件与 Agent Identity 导入,credential/额度刷新、账号恢复及主动额度重置卡 | +| 账号 | OAuth、OpenAI AT/RT 与账号文件导入,credential/额度刷新、账号恢复及主动额度重置卡 | | 管理 | 账号与分组、Client Key、运行设置、用量/错误观测、S3/R2 备份、系统更新与回滚 | | 计量 | Token、图片、费用、延迟、账号与 Provider 归因,并记录上游响应确认的实际 `service_tier` | @@ -90,7 +90,7 @@ curl -i http://127.0.0.1:8080/healthz ### 3. 初始化网关 1. 使用 `admin@cpr.local` 与初始密码登录。 -2. 导入或授权 Provider 账号:OpenAI 支持 OAuth、逐行 AT/RT、OAuth JSON 与 Agent Identity; +2. 导入或授权 Provider 账号:OpenAI 支持 OAuth、逐行 AT/RT 与 OAuth JSON; xAI 支持 OAuth 和账号 JSON。 3. 创建账号分组并按需配置全局模型映射。未命中的模型名原样交给候选 Provider。 4. 创建 `sk_...` Client Key,选择可用账号分组,并设置速率和并发限制;空分组范围表示全部账号。 diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 9df43a2d4..5d18d8c79 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -8,16 +8,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common 0.1.7", - "generic-array", -] - [[package]] name = "aho-corasick" version = "1.1.4" @@ -721,17 +711,6 @@ dependencies = [ "phf 0.12.1", ] -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common 0.1.7", - "inout", - "zeroize", -] - [[package]] name = "cmake" version = "0.1.58" @@ -961,7 +940,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", - "rand_core 0.6.4", "typenum", ] @@ -976,36 +954,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "crypto_box" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16182b4f39a82ec8a6851155cc4c0cda3065bb1db33651726a29e1951de0f009" -dependencies = [ - "aead", - "blake2", - "crypto_secretbox", - "curve25519-dalek 4.1.3", - "salsa20", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto_secretbox" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" -dependencies = [ - "aead", - "cipher", - "generic-array", - "poly1305", - "salsa20", - "subtle", - "zeroize", -] - [[package]] name = "ctutils" version = "0.4.2" @@ -1016,48 +964,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "curve25519-dalek-derive", - "fiat-crypto 0.2.9", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "curve25519-dalek-derive", - "digest 0.11.3", - "fiat-crypto 0.3.0", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "data-encoding" version = "2.11.0" @@ -1169,31 +1075,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ed25519" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" -dependencies = [ - "pkcs8", - "signature 3.0.0", -] - -[[package]] -name = "ed25519-dalek" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" -dependencies = [ - "curve25519-dalek 5.0.0", - "ed25519", - "serde", - "sha2 0.11.0", - "signature 3.0.0", - "subtle", - "zeroize", -] - [[package]] name = "either" version = "1.16.0" @@ -1295,18 +1176,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - -[[package]] -name = "fiat-crypto" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" - [[package]] name = "filetime" version = "0.2.29" @@ -1631,7 +1500,6 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", - "zeroize", ] [[package]] @@ -2073,15 +1941,6 @@ dependencies = [ "hashbrown 0.17.1", ] -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - [[package]] name = "ipnet" version = "2.12.0" @@ -2318,12 +2177,6 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - [[package]] name = "openssl-probe" version = "0.2.1" @@ -2498,17 +2351,6 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" -[[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash", -] - [[package]] name = "potential_utf" version = "0.1.5" @@ -2579,8 +2421,6 @@ dependencies = [ "bytes", "chrono", "cookie", - "crypto_box", - "ed25519-dalek", "flate2", "futures", "gateway-admin", @@ -3049,15 +2889,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "salsa20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" -dependencies = [ - "cipher", -] - [[package]] name = "schannel" version = "0.1.29" @@ -4046,16 +3877,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common 0.1.7", - "subtle", -] - [[package]] name = "untrusted" version = "0.7.1" diff --git a/backend/crates/providers/openai/Cargo.toml b/backend/crates/providers/openai/Cargo.toml index a5e1192f5..b46d94b0a 100644 --- a/backend/crates/providers/openai/Cargo.toml +++ b/backend/crates/providers/openai/Cargo.toml @@ -14,8 +14,6 @@ base64 = "0.23.0" bytes = "1.11.1" chrono = { version = "0.4.45", features = ["clock", "serde"] } cookie = "0.18.1" -crypto_box = { version = "0.9.1", features = ["seal"] } -ed25519-dalek = { version = "3.0.0", features = ["alloc", "pkcs8"] } futures = "0.3.32" flate2 = "1.1.9" gateway-admin = { path = "../../gateway-admin" } diff --git a/backend/crates/providers/openai/src/credential/admin.rs b/backend/crates/providers/openai/src/credential/admin.rs index 6469603ac..23d9bbd1d 100644 --- a/backend/crates/providers/openai/src/credential/admin.rs +++ b/backend/crates/providers/openai/src/credential/admin.rs @@ -12,7 +12,7 @@ use chrono::{DateTime, FixedOffset, Utc}; use gateway_core::engine::credential::{ AccountErrorReason, CredentialCasUpdate, CredentialRevision, CredentialState, LoadedCredential, NewProviderAccount, ProviderAccount, ProviderAccountId, ProviderAccountIdentity, - ProviderAccountUpdate, QuotaEvidence, QuotaState, + ProviderAccountUpdate, QuotaState, }; use gateway_core::provider_ports::{ ProviderLeaseAcquisition, ProviderLeaseGuard, ProviderLeasePort, ProviderLeaseRequest, @@ -28,14 +28,12 @@ use super::recovery_log::{CodexOAuthRecoveryOperation, record_oauth_recovery}; use super::security::CodexCredentialCodec; use super::token_client::{RefreshFailure, TokenRefresher}; use super::types::{ - CODEX_AUTHENTICATION_KIND_AGENT_IDENTITY, CODEX_AUTHENTICATION_KIND_OAUTH, CodexAccountProfile, - CodexAgentIdentityAuthMode, CodexAgentIdentityCredentialData, CodexCredentialData, + CODEX_AUTHENTICATION_KIND_OAUTH, CodexAccountProfile, CodexCredentialData, CodexCredentialPrincipal, CodexOAuthMetadata, CodexOAuthSecret, parse_access_token_expiration, parse_chatgpt_jwt_claims, }; const PROVIDER_NAME: &str = "openai"; -const CODEX_CREDENTIAL_SCHEMA_VERSION: u32 = 1; const MAX_BATCH: usize = 200; const MAX_IMPORT_DOCUMENT_BYTES: usize = 64 * 1024 * 1024; @@ -121,56 +119,31 @@ impl fmt::Debug for PreparedCodexAccountImport { } struct ParsedCodexImportAccount { - id: Option, name: Option, email: Option, - plan_type: Option, - chatgpt_account_id: Option, - chatgpt_user_id: Option, authentication: ParsedCodexAuthentication, - status: Option, } -enum ParsedCodexAuthentication { - OAuth { - access_token: Option, - refresh_token: Option, - id_token: Option, - }, - AgentIdentity { - runtime_id: String, - private_key: String, - task_id: Option, - }, +struct ParsedCodexAuthentication { + access_token: Option, + refresh_token: Option, + id_token: Option, } impl fmt::Debug for ParsedCodexAuthentication { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::OAuth { - access_token, - refresh_token, - id_token, - } => formatter - .debug_struct("OAuth") - .field("access_token", &access_token.as_ref().map(|_| "")) - .field( - "refresh_token", - &refresh_token.as_ref().map(|_| ""), - ) - .field("id_token", &id_token.as_ref().map(|_| "")) - .finish(), - Self::AgentIdentity { - runtime_id: _, - private_key: _, - task_id, - } => formatter - .debug_struct("AgentIdentity") - .field("runtime_id", &"") - .field("private_key", &"") - .field("task_id", &task_id.as_ref().map(|_| "")) - .finish(), - } + formatter + .debug_struct("OAuth") + .field( + "access_token", + &self.access_token.as_ref().map(|_| ""), + ) + .field( + "refresh_token", + &self.refresh_token.as_ref().map(|_| ""), + ) + .field("id_token", &self.id_token.as_ref().map(|_| "")) + .finish() } } @@ -178,20 +151,9 @@ impl fmt::Debug for ParsedCodexImportAccount { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("ParsedCodexImportAccount") - .field("id", &self.id) .field("name", &self.name) .field("email", &self.email.as_ref().map(|_| "")) - .field("plan_type", &self.plan_type) - .field( - "chatgpt_account_id", - &self.chatgpt_account_id.as_ref().map(|_| ""), - ) - .field( - "chatgpt_user_id", - &self.chatgpt_user_id.as_ref().map(|_| ""), - ) .field("authentication", &self.authentication) - .field("status", &self.status) .finish() } } @@ -222,7 +184,7 @@ impl fmt::Debug for ExportManagedCodexCredential { #[serde(rename_all = "camelCase")] pub struct CodexCprExportDocument { source_format: &'static str, - accounts: Vec, + accounts: Vec, } impl CodexCprExportDocument { @@ -277,25 +239,6 @@ struct CodexCprOAuthExportAccount { access_token_expires_at: Option, } -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct CodexCprAgentIdentityExportAccount { - #[serde(flatten)] - common: CodexCprExportCommon, - auth_mode: &'static str, - agent_runtime_id: String, - agent_private_key: String, - #[serde(skip_serializing_if = "Option::is_none")] - task_id: Option, -} - -#[derive(Serialize)] -#[serde(untagged)] -enum CodexCprExportAccount { - OAuth(CodexCprOAuthExportAccount), - AgentIdentity(CodexCprAgentIdentityExportAccount), -} - /// App 已从 Store 读取的当前账号、revision 与明文 Provider JSON。 pub struct RotateManagedCodexCredential { pub current: LoadedCredential, @@ -567,7 +510,7 @@ impl CodexCredentialAdmin { { return Err(CodexCredentialAdminError::InvalidCredential); } - CodexCprExportAccount::OAuth(CodexCprOAuthExportAccount { + CodexCprOAuthExportAccount { common, access_token: data.access_token, refresh_token: data.refresh_token, @@ -576,22 +519,7 @@ impl CodexCredentialAdmin { .access_token_expires_at() .map(DateTime::::from) .map(|value| value.to_rfc3339()), - }) - } - CodexCredentialData::AgentIdentity(data) => { - if account.authentication_kind() != CODEX_AUTHENTICATION_KIND_AGENT_IDENTITY - || account.has_refresh_token() - || account.access_token_expires_at().is_some() - { - return Err(CodexCredentialAdminError::InvalidCredential); } - CodexCprExportAccount::AgentIdentity(CodexCprAgentIdentityExportAccount { - common, - auth_mode: "agentIdentity", - agent_runtime_id: data.agent_runtime_id, - agent_private_key: data.agent_private_key, - task_id: data.task_id, - }) } }; accounts.push(exported); @@ -861,7 +789,7 @@ impl CodexCredentialAdminService { Ok(prepared) } - /// 归一导入 OAuth/agent-identity 凭据到唯一 `NewProviderAccount` 写入路径。 + /// 归一导入 OAuth 凭据到唯一 `NewProviderAccount` 写入路径。 /// /// OAuth 导入先取得 access token(直接提供或 RT exchange),再按官方 /// `parse_chatgpt_jwt_claims` 从 ID token/access token 本地投影账号资料。 @@ -880,101 +808,57 @@ impl CodexCredentialAdminService { if candidates.is_empty() || candidates.len() > MAX_BATCH { return Err(CodexCredentialAdminError::InvalidInput); } - let mut account_ids = BTreeSet::new(); - let mut upstream_identities = BTreeSet::new(); let mut accounts = Vec::with_capacity(candidates.len()); for candidate in candidates { - let prepared = match &candidate.authentication { - ParsedCodexAuthentication::OAuth { - access_token, - refresh_token, - id_token, - } => { - let account_id = format!("acct_{}", uuid::Uuid::now_v7().simple()); - if !account_ids.insert(account_id.clone()) { - return Err(CodexCredentialAdminError::InvalidInput); - } - let typed_account_id = ProviderAccountId::new(account_id.clone()) - .map_err(|_| CodexCredentialAdminError::InvalidInput)?; - let (secret, access_token_expires_at) = self - .resolve_import_tokens( - &typed_account_id, - access_token.clone(), - refresh_token.clone(), - id_token.clone(), - ) - .await?; - // 与官方 `parse_chatgpt_jwt_claims` 一致:先读 ID token,缺失字段 - // 再由 access token 补齐。解析失败不阻塞 token 入库。 - let id_metadata = secret - .id_token - .as_ref() - .and_then(|token| parse_chatgpt_jwt_claims(token.expose_secret()).ok()) - .unwrap_or_default(); - let access_metadata = - parse_chatgpt_jwt_claims(secret.access_token.expose_secret()) - .unwrap_or_default(); - let metadata = CodexOAuthMetadata { - email: id_metadata.email.or(access_metadata.email), - chatgpt_plan_type: id_metadata - .chatgpt_plan_type - .or(access_metadata.chatgpt_plan_type), - chatgpt_user_id: id_metadata - .chatgpt_user_id - .or(access_metadata.chatgpt_user_id), - chatgpt_account_id: id_metadata - .chatgpt_account_id - .or(access_metadata.chatgpt_account_id), - }; - CodexCredentialAdmin.prepare_unresolved_oauth( - UnresolvedCodexOAuthCredential { - account_id, - name: candidate - .name - .clone() - .or_else(|| candidate.email.clone()) - .or_else(|| metadata.email.clone()) - .filter(|name| !name.trim().is_empty()) - .unwrap_or_else(|| "Codex OAuth".to_owned()), - installation_id: uuid::Uuid::new_v4().to_string(), - secret, - metadata, - access_token_expires_at, - next_refresh_at: None, - enabled: true, - }, - )? - } - ParsedCodexAuthentication::AgentIdentity { .. } => { - let account_id = candidate - .id - .clone() - .filter(|id| ProviderAccountId::new(id.clone()).is_ok()) - .unwrap_or_else(|| format!("acct_{}", uuid::Uuid::now_v7().simple())); - if !account_ids.insert(account_id.clone()) { - return Err(CodexCredentialAdminError::InvalidInput); - } - let facts = - parse_cpr_account_facts(candidate.status.as_deref(), SystemTime::now())?; - let mut account = - self.prepare_agent_identity_import(&candidate, account_id, facts.enabled)?; - let upstream_user_id = account - .account - .upstream_user_id() - .ok_or(CodexCredentialAdminError::InvalidInput)? - .to_owned(); - let upstream_account_id = account - .account - .upstream_account_id() - .ok_or(CodexCredentialAdminError::InvalidInput)? - .to_owned(); - if !upstream_identities.insert((upstream_user_id, upstream_account_id)) { - return Err(CodexCredentialAdminError::InvalidInput); - } - account.account = facts.apply(account.account); - account - } + let account_id = format!("acct_{}", uuid::Uuid::now_v7().simple()); + let typed_account_id = ProviderAccountId::new(account_id.clone()) + .map_err(|_| CodexCredentialAdminError::InvalidInput)?; + let (secret, access_token_expires_at) = self + .resolve_import_tokens( + &typed_account_id, + candidate.authentication.access_token.clone(), + candidate.authentication.refresh_token.clone(), + candidate.authentication.id_token.clone(), + ) + .await?; + // 与官方 `parse_chatgpt_jwt_claims` 一致:先读 ID token,缺失字段 + // 再由 access token 补齐。解析失败不阻塞 token 入库。 + let id_metadata = secret + .id_token + .as_ref() + .and_then(|token| parse_chatgpt_jwt_claims(token.expose_secret()).ok()) + .unwrap_or_default(); + let access_metadata = + parse_chatgpt_jwt_claims(secret.access_token.expose_secret()).unwrap_or_default(); + let metadata = CodexOAuthMetadata { + email: id_metadata.email.or(access_metadata.email), + chatgpt_plan_type: id_metadata + .chatgpt_plan_type + .or(access_metadata.chatgpt_plan_type), + chatgpt_user_id: id_metadata + .chatgpt_user_id + .or(access_metadata.chatgpt_user_id), + chatgpt_account_id: id_metadata + .chatgpt_account_id + .or(access_metadata.chatgpt_account_id), }; + let prepared = + CodexCredentialAdmin.prepare_unresolved_oauth(UnresolvedCodexOAuthCredential { + account_id, + name: candidate + .name + .clone() + .or_else(|| candidate.email.clone()) + .or_else(|| metadata.email.clone()) + .filter(|name| !name.trim().is_empty()) + .unwrap_or_else(|| "Codex OAuth".to_owned()), + installation_id: uuid::Uuid::new_v4().to_string(), + secret, + metadata, + access_token_expires_at, + next_refresh_at: None, + enabled: true, + })?; accounts.push(prepared); } Ok(PreparedCodexAccountImport { accounts }) @@ -1044,79 +928,6 @@ impl CodexCredentialAdminService { .map(ExposeSecret::expose_secret), ); } - - fn prepare_agent_identity_import( - &self, - candidate: &ParsedCodexImportAccount, - account_id: String, - enabled: bool, - ) -> Result { - let ParsedCodexAuthentication::AgentIdentity { - runtime_id, - private_key, - task_id, - } = &candidate.authentication - else { - return Err(CodexCredentialAdminError::InvalidCredential); - }; - let upstream_account_id = candidate - .chatgpt_account_id - .clone() - .ok_or(CodexCredentialAdminError::InvalidInput)?; - let upstream_user_id = candidate - .chatgpt_user_id - .clone() - .ok_or(CodexCredentialAdminError::InvalidInput)?; - let account_id = ProviderAccountId::new(account_id) - .map_err(|_| CodexCredentialAdminError::InvalidInput)?; - let provider = ProviderKind::new(PROVIDER_NAME) - .map_err(|_| CodexCredentialAdminError::InvalidInput)?; - let revision = - CredentialRevision::new(1).map_err(|_| CodexCredentialAdminError::InvalidCredential)?; - let credential = - CodexCredentialCodec::encode_agent_identity(CodexAgentIdentityCredentialData { - schema_version: CODEX_CREDENTIAL_SCHEMA_VERSION, - auth_mode: CodexAgentIdentityAuthMode::AgentIdentity, - installation_id: uuid::Uuid::new_v4().to_string(), - agent_runtime_id: runtime_id.clone(), - agent_private_key: private_key.clone(), - task_id: task_id.clone(), - cookies: Vec::new(), - }) - .map_err(|_| CodexCredentialAdminError::InvalidCredential)?; - let name = candidate - .name - .clone() - .or_else(|| candidate.email.clone()) - .filter(|name| !name.trim().is_empty()) - .unwrap_or_else(|| "OpenAI Agent Identity".to_owned()); - let account = ProviderAccount::new( - account_id, - provider, - name, - Some(upstream_user_id), - CODEX_AUTHENTICATION_KIND_AGENT_IDENTITY.to_owned(), - revision, - None, - ) - .with_profile( - candidate.email.clone(), - Some(upstream_account_id), - candidate.plan_type.clone(), - ) - .with_account_facts( - enabled, - CredentialState::Ready, - QuotaState::unknown(), - None, - None, - ) - .with_refresh_schedule(false, None); - Ok(NewProviderAccount { - account, - credential, - }) - } } fn optional_time(value: Option>) -> Option { @@ -1177,10 +988,6 @@ fn parse_import_document( ) -> Result, CodexCredentialAdminError> { let mut accounts = Vec::new(); for value in import_account_values(payload)? { - if looks_like_agent_identity_account(value) { - accounts.push(parse_agent_identity_account(value)?); - continue; - } if !is_openai_oauth_candidate(value) { continue; } @@ -1194,72 +1001,9 @@ fn parse_oauth_import_account( ) -> Result { let credentials = value.get("credentials").unwrap_or(value); Ok(ParsedCodexImportAccount { - id: first_string(value, &["id"]), name: first_string(value, &["name", "label"]), email: first_string(value, &["email"]).or_else(|| first_string(credentials, &["email"])), - plan_type: first_string(value, &["plan_type", "planType"]) - .or_else(|| first_string(credentials, &["plan_type", "planType"])), - chatgpt_account_id: None, - chatgpt_user_id: None, authentication: parse_oauth_import_tokens(value)?, - status: first_string(value, &["status"]), - }) -} - -fn parse_agent_identity_account( - value: &Value, -) -> Result { - let account = value - .as_object() - .ok_or(CodexCredentialAdminError::InvalidInput)?; - if account - .get("platform") - .and_then(Value::as_str) - .is_some_and(|platform| !platform.eq_ignore_ascii_case("openai")) - || account - .get("type") - .and_then(Value::as_str) - .is_some_and(|kind| !kind.eq_ignore_ascii_case("oauth")) - { - return Err(CodexCredentialAdminError::InvalidInput); - } - let identity = value - .get("agent_identity") - .or_else(|| value.get("credentials")) - .unwrap_or(value); - let auth_mode = first_string(value, &["auth_mode", "authMode"]) - .or_else(|| first_string(identity, &["auth_mode", "authMode"])); - if auth_mode.as_deref() != Some("agentIdentity") { - return Err(CodexCredentialAdminError::InvalidCredential); - } - let runtime_id = first_string(identity, &["agent_runtime_id", "agentRuntimeId"]) - .ok_or(CodexCredentialAdminError::InvalidCredential)?; - let private_key = first_string(identity, &["agent_private_key", "agentPrivateKey"]) - .ok_or(CodexCredentialAdminError::InvalidCredential)?; - Ok(ParsedCodexImportAccount { - id: first_string(value, &["id"]), - name: first_string(value, &["name", "label"]), - email: first_string(identity, &["email"]), - plan_type: first_string(identity, &["plan_type", "planType"]), - chatgpt_account_id: first_string( - identity, - &[ - "chatgpt_account_id", - "chatgptAccountId", - "account_id", - "accountId", - ], - ), - chatgpt_user_id: first_string( - identity, - &["chatgpt_user_id", "chatgptUserId", "user_id", "userId"], - ), - authentication: ParsedCodexAuthentication::AgentIdentity { - runtime_id, - private_key, - task_id: first_string(identity, &["task_id", "taskId"]), - }, - status: first_string(value, &["status"]), }) } @@ -1308,24 +1052,13 @@ fn parse_oauth_import_tokens( if access_token.is_none() && refresh_token.is_none() { return Err(CodexCredentialAdminError::InvalidCredential); } - Ok(ParsedCodexAuthentication::OAuth { + Ok(ParsedCodexAuthentication { access_token, refresh_token, id_token, }) } -fn looks_like_agent_identity_account(value: &Value) -> bool { - let identity = value - .get("agent_identity") - .or_else(|| value.get("credentials")) - .unwrap_or(value); - first_string(value, &["auth_mode", "authMode"]) - .or_else(|| first_string(identity, &["auth_mode", "authMode"])) - .is_some_and(|mode| mode == "agentIdentity") - || first_string(identity, &["agent_runtime_id", "agentRuntimeId"]).is_some() -} - fn is_openai_oauth_candidate(value: &Value) -> bool { let Some(account) = value.as_object() else { return false; @@ -1354,66 +1087,3 @@ fn first_string(value: &Value, keys: &[&str]) -> Option { .filter(|value| !value.is_empty()) .map(str::to_owned) } - -struct ImportedCprAccountFacts { - enabled: bool, - credential_state: CredentialState, - quota: QuotaState, - error_reason: Option, -} - -impl ImportedCprAccountFacts { - fn apply(self, account: ProviderAccount) -> ProviderAccount { - account.with_account_facts( - self.enabled, - self.credential_state, - self.quota, - self.error_reason, - None, - ) - } -} - -fn parse_cpr_account_facts( - status: Option<&str>, - observed_at: SystemTime, -) -> Result { - match status - .map(str::trim) - .unwrap_or("active") - .to_ascii_lowercase() - .as_str() - { - "active" => Ok(ImportedCprAccountFacts { - enabled: true, - credential_state: CredentialState::Ready, - quota: QuotaState::unknown(), - error_reason: None, - }), - "disabled" => Ok(ImportedCprAccountFacts { - enabled: false, - credential_state: CredentialState::Ready, - quota: QuotaState::unknown(), - error_reason: None, - }), - "expired" => Ok(ImportedCprAccountFacts { - enabled: true, - credential_state: CredentialState::Expired, - quota: QuotaState::unknown(), - error_reason: Some(AccountErrorReason::CredentialExpired), - }), - "quota_exhausted" => Ok(ImportedCprAccountFacts { - enabled: true, - credential_state: CredentialState::Ready, - quota: QuotaState::exhausted(QuotaEvidence::ProviderDenied, observed_at, None), - error_reason: None, - }), - "banned" => Ok(ImportedCprAccountFacts { - enabled: true, - credential_state: CredentialState::Banned, - quota: QuotaState::unknown(), - error_reason: Some(AccountErrorReason::AccountBanned), - }), - _ => Err(CodexCredentialAdminError::InvalidInput), - } -} diff --git a/backend/crates/providers/openai/src/credential/agent_identity.rs b/backend/crates/providers/openai/src/credential/agent_identity.rs deleted file mode 100644 index 272e17274..000000000 --- a/backend/crates/providers/openai/src/credential/agent_identity.rs +++ /dev/null @@ -1,500 +0,0 @@ -//! OpenAI Agent Identity 签名、task 注册与 revision-fenced 恢复。 - -use std::collections::HashMap; -use std::fmt; -use std::sync::Arc; - -use async_trait::async_trait; -use base64::Engine as _; -use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}; -use chrono::{DateTime, SecondsFormat, Utc}; -use crypto_box::SecretKey as BoxSecretKey; -use ed25519_dalek::pkcs8::DecodePrivateKey as _; -use ed25519_dalek::{Signer as _, SigningKey}; -use gateway_core::engine::credential::{ProviderAccount, ProviderAccountId}; -use reqwest::StatusCode; -use secrecy::SecretString; -use serde::{Deserialize, Serialize}; -use sha2::{Digest as _, Sha512}; -use thiserror::Error; -use tokio::sync::Mutex; -use url::Url; - -use super::repository::{CodexCredentialRepository, CredentialRepositoryError}; -use super::security::{CodexRuntimeAuthentication, CodexRuntimeCredential}; -use crate::transport::headers::build_codex_profile_headers; -use crate::transport::profile::CodexWireProfileState; -use crate::transport::websocket::CodexWebSocketExchangeError; -use crate::transport::{CodexClientError, CodexWebSocketPool}; - -const OFFICIAL_AGENT_IDENTITY_AUTH_BASE_URL: &str = "https://auth.openai.com/api/accounts/"; -const MAX_TASK_RESPONSE_BYTES: usize = 64 * 1024; - -/// 已验证且只存在于 OpenAI Provider 运行时的 Agent Identity 密钥。 -pub struct CodexAgentIdentitySecret { - runtime_id: String, - signing_key: SigningKey, - task_id: Option, -} - -impl CodexAgentIdentitySecret { - pub fn from_pkcs8( - runtime_id: String, - encoded_private_key: &str, - task_id: Option, - ) -> Result { - if !valid_identifier(&runtime_id) - || task_id - .as_deref() - .is_some_and(|value| !valid_identifier(value)) - { - return Err(CodexAgentIdentityError::InvalidCredential); - } - let der = STANDARD - .decode(encoded_private_key.trim()) - .map_err(|_| CodexAgentIdentityError::InvalidCredential)?; - let signing_key = SigningKey::from_pkcs8_der(&der) - .map_err(|_| CodexAgentIdentityError::InvalidCredential)?; - Ok(Self { - runtime_id, - signing_key, - task_id, - }) - } - - #[must_use] - pub fn runtime_id(&self) -> &str { - &self.runtime_id - } - - #[must_use] - pub fn task_id(&self) -> Option<&str> { - self.task_id.as_deref() - } - - pub fn authorization_header( - &self, - now: DateTime, - ) -> Result { - let task_id = self.task_id().ok_or(CodexAgentIdentityError::MissingTask)?; - let timestamp = timestamp(now); - let payload = format!("{}:{task_id}:{timestamp}", self.runtime_id); - let signature = self.signing_key.sign(payload.as_bytes()); - let envelope = AgentAssertionEnvelope { - agent_runtime_id: &self.runtime_id, - task_id, - timestamp: ×tamp, - signature: STANDARD.encode(signature.to_bytes()), - }; - let encoded = serde_json::to_vec(&envelope) - .map_err(|_| CodexAgentIdentityError::InvalidCredential)?; - Ok(SecretString::from(format!( - "AgentAssertion {}", - URL_SAFE_NO_PAD.encode(encoded) - ))) - } - - fn registration(&self, now: DateTime) -> AgentTaskRegistration { - let timestamp = timestamp(now); - let payload = format!("{}:{timestamp}", self.runtime_id); - let signature = self.signing_key.sign(payload.as_bytes()); - AgentTaskRegistration { - timestamp, - signature: STANDARD.encode(signature.to_bytes()), - } - } - - fn decrypt_task_id(&self, encoded: &str) -> Result { - let ciphertext = STANDARD - .decode(encoded.trim()) - .map_err(|_| CodexAgentIdentityError::InvalidTaskResponse)?; - let digest = Sha512::digest(self.signing_key.to_bytes()); - let mut curve_key = [0_u8; 32]; - curve_key.copy_from_slice(&digest[..32]); - let secret = BoxSecretKey::from(curve_key); - let plaintext = secret - .unseal(&ciphertext) - .map_err(|_| CodexAgentIdentityError::InvalidTaskResponse)?; - let task_id = String::from_utf8(plaintext) - .map_err(|_| CodexAgentIdentityError::InvalidTaskResponse)?; - let task_id = task_id.trim().to_owned(); - if !valid_identifier(&task_id) { - return Err(CodexAgentIdentityError::InvalidTaskResponse); - } - Ok(task_id) - } -} - -impl fmt::Debug for CodexAgentIdentitySecret { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter - .debug_struct("CodexAgentIdentitySecret") - .field("runtime_id", &"") - .field("signing_key", &"") - .field("task_id", &self.task_id.as_ref().map(|_| "")) - .finish() - } -} - -#[derive(Serialize)] -struct AgentAssertionEnvelope<'a> { - agent_runtime_id: &'a str, - task_id: &'a str, - timestamp: &'a str, - signature: String, -} - -#[derive(Serialize)] -struct AgentTaskRegistration { - timestamp: String, - signature: String, -} - -#[derive(Deserialize)] -struct AgentTaskRegistrationResponse { - #[serde(default, alias = "taskId")] - task_id: Option, - #[serde(default, alias = "encryptedTaskId")] - encrypted_task_id: Option, -} - -#[async_trait] -pub trait CodexAgentIdentityTaskRegistrar: Send + Sync + 'static { - async fn register( - &self, - credential: &CodexAgentIdentitySecret, - ) -> Result; -} - -pub struct OfficialCodexAgentIdentityTaskRegistrar { - client: reqwest::Client, - base_url: Url, - profile: CodexWireProfileState, -} - -impl OfficialCodexAgentIdentityTaskRegistrar { - pub fn new( - client: reqwest::Client, - profile: CodexWireProfileState, - ) -> Result { - let base_url = Url::parse(OFFICIAL_AGENT_IDENTITY_AUTH_BASE_URL) - .map_err(|_| CodexAgentIdentityError::InvalidConfiguration)?; - Ok(Self { - client, - base_url, - profile, - }) - } -} - -impl fmt::Debug for OfficialCodexAgentIdentityTaskRegistrar { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter - .debug_struct("OfficialCodexAgentIdentityTaskRegistrar") - .field("base_url", &self.base_url) - .finish_non_exhaustive() - } -} - -#[async_trait] -impl CodexAgentIdentityTaskRegistrar for OfficialCodexAgentIdentityTaskRegistrar { - async fn register( - &self, - credential: &CodexAgentIdentitySecret, - ) -> Result { - let mut endpoint = self.base_url.clone(); - endpoint - .path_segments_mut() - .map_err(|_| CodexAgentIdentityError::InvalidConfiguration)? - .extend(["v1", "agent", credential.runtime_id(), "task", "register"]); - let profile = self.profile.snapshot(); - let headers = build_codex_profile_headers(&profile) - .map_err(|_| CodexAgentIdentityError::InvalidConfiguration)?; - let response = self - .client - .post(endpoint) - .headers(headers) - .header(reqwest::header::ACCEPT, "application/json") - .json(&credential.registration(Utc::now())) - .send() - .await - .map_err(|_| CodexAgentIdentityError::TaskRegistrationUnavailable)?; - if !response.status().is_success() { - return Err(CodexAgentIdentityError::TaskRegistrationRejected); - } - if response - .content_length() - .is_some_and(|length| length > MAX_TASK_RESPONSE_BYTES as u64) - { - return Err(CodexAgentIdentityError::InvalidTaskResponse); - } - let body = response - .bytes() - .await - .map_err(|_| CodexAgentIdentityError::TaskRegistrationUnavailable)?; - if body.len() > MAX_TASK_RESPONSE_BYTES { - return Err(CodexAgentIdentityError::InvalidTaskResponse); - } - let result = serde_json::from_slice::(&body) - .map_err(|_| CodexAgentIdentityError::InvalidTaskResponse)?; - if let Some(task_id) = result.task_id.map(|value| value.trim().to_owned()) - && valid_identifier(&task_id) - { - return Ok(task_id); - } - let encrypted = result - .encrypted_task_id - .as_deref() - .ok_or(CodexAgentIdentityError::InvalidTaskResponse)?; - credential.decrypt_task_id(encrypted) - } -} - -pub struct PreparedCodexRuntimeCredential { - pub account: ProviderAccount, - pub credential: CodexRuntimeCredential, -} - -/// Agent task 的唯一注册与恢复 owner。 -pub struct CodexAgentIdentityTaskService { - repository: CodexCredentialRepository, - registrar: Arc, - websocket_pool: Arc, - account_locks: Mutex>>>, -} - -impl CodexAgentIdentityTaskService { - #[must_use] - pub fn new( - repository: CodexCredentialRepository, - registrar: Arc, - websocket_pool: Arc, - ) -> Self { - Self { - repository, - registrar, - websocket_pool, - account_locks: Mutex::new(HashMap::new()), - } - } - - pub async fn prepare( - &self, - account: &ProviderAccount, - ) -> Result { - let prepared = self.load_current(account.id()).await?; - if !matches!( - &prepared.credential.authentication, - CodexRuntimeAuthentication::AgentIdentity(secret) if secret.task_id().is_none() - ) { - return Ok(prepared); - } - self.ensure_task(account.id(), None).await - } - - pub async fn recover( - &self, - account_id: &ProviderAccountId, - expected_task_id: &str, - ) -> Result { - self.ensure_task(account_id, Some(expected_task_id)).await - } - - pub async fn recover_after_rejected_task( - &self, - account_id: &ProviderAccountId, - authentication: &CodexRuntimeAuthentication, - error: &CodexClientError, - ) -> Result, CodexAgentIdentityError> { - let CodexRuntimeAuthentication::AgentIdentity(secret) = authentication else { - return Ok(None); - }; - let Some(task_id) = secret.task_id() else { - return Ok(None); - }; - if !is_agent_identity_task_invalid_client_error(error) { - return Ok(None); - } - self.recover(account_id, task_id).await.map(Some) - } - - async fn ensure_task( - &self, - account_id: &ProviderAccountId, - expected_task_id: Option<&str>, - ) -> Result { - let lock = { - let mut locks = self.account_locks.lock().await; - Arc::clone( - locks - .entry(account_id.clone()) - .or_insert_with(|| Arc::new(Mutex::new(()))), - ) - }; - let _guard = lock.lock().await; - let current = self.load_current(account_id).await?; - let CodexRuntimeAuthentication::AgentIdentity(secret) = ¤t.credential.authentication - else { - return Ok(current); - }; - match (expected_task_id, secret.task_id()) { - (None, Some(_)) => return Ok(current), - (Some(expected), Some(actual)) if actual != expected => return Ok(current), - _ => {} - } - let task_id = self.registrar.register(secret).await?; - let mut data = self.repository.load_complete_data(¤t.account).await?; - let agent = data - .agent_identity_mut() - .ok_or(CodexAgentIdentityError::InvalidCredential)?; - match (expected_task_id, agent.task_id.as_deref()) { - (None, Some(_)) => return self.load_current(account_id).await, - (Some(expected), Some(actual)) if actual != expected => { - return self.load_current(account_id).await; - } - _ => agent.task_id = Some(task_id), - } - self.repository - .compare_and_swap_data(¤t.account, data) - .await?; - self.websocket_pool.evict_account(account_id.as_str()).await; - self.load_current(account_id).await - } - - pub(crate) async fn load_current( - &self, - account_id: &ProviderAccountId, - ) -> Result { - let account = self - .repository - .store() - .get_account(account_id) - .await? - .filter(|account| account.provider().as_str() == "openai") - .ok_or(CodexAgentIdentityError::NotFound)?; - let credential = self.repository.load_runtime_credential(&account).await?; - Ok(PreparedCodexRuntimeCredential { - account, - credential, - }) - } -} - -impl fmt::Debug for CodexAgentIdentityTaskService { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter - .debug_struct("CodexAgentIdentityTaskService") - .field("repository", &"CodexCredentialRepository") - .field("registrar", &"CodexAgentIdentityTaskRegistrar") - .finish_non_exhaustive() - } -} - -#[derive(Debug, Error)] -pub enum CodexAgentIdentityError { - #[error("OpenAI Agent Identity credential is invalid")] - InvalidCredential, - #[error("OpenAI Agent Identity task is unavailable")] - MissingTask, - #[error("OpenAI Agent Identity task registration is unavailable")] - TaskRegistrationUnavailable, - #[error("OpenAI Agent Identity task registration was rejected")] - TaskRegistrationRejected, - #[error("OpenAI Agent Identity task registration response is invalid")] - InvalidTaskResponse, - #[error("OpenAI Agent Identity configuration is invalid")] - InvalidConfiguration, - #[error("OpenAI Agent Identity account was not found")] - NotFound, - #[error("OpenAI Agent Identity credential store is unavailable")] - Repository, -} - -impl From for CodexAgentIdentityError { - fn from(_: CredentialRepositoryError) -> Self { - Self::Repository - } -} - -impl From for CodexAgentIdentityError { - fn from(_: gateway_core::error::StoreError) -> Self { - Self::Repository - } -} - -#[must_use] -pub fn is_agent_identity_task_invalid_response(status: StatusCode, body: &str) -> bool { - if status != StatusCode::UNAUTHORIZED { - return false; - } - let lower = body.to_ascii_lowercase(); - let compact = lower - .chars() - .filter(|character| !character.is_ascii_whitespace()) - .collect::(); - [ - "\"code\":\"invalid_task_id\"", - "\"code\":\"task_not_found\"", - "\"code\":\"task_expired\"", - "\"error\":\"invalid_task_id\"", - ] - .iter() - .any(|marker| compact.contains(marker)) - || [ - "invalid task_id", - "invalid task id", - "task_id is invalid", - "task id is invalid", - "task not found", - "task expired", - "unknown task_id", - "unknown task id", - ] - .iter() - .any(|marker| lower.contains(marker)) -} - -fn is_agent_identity_task_invalid_client_error(error: &CodexClientError) -> bool { - match error { - CodexClientError::Upstream { status, body, .. } => { - is_agent_identity_task_invalid_response(*status, body) - } - CodexClientError::WebSocket(CodexWebSocketExchangeError::Upstream(upstream)) => { - StatusCode::from_u16(upstream.status_code) - .ok() - .is_some_and(|status| { - is_agent_identity_task_invalid_response(status, &upstream.body) - }) - } - CodexClientError::WebSocket(CodexWebSocketExchangeError::PostSendAmbiguous { - source: Some(source), - .. - }) => is_agent_identity_task_invalid_websocket_error(source), - _ => false, - } -} - -fn is_agent_identity_task_invalid_websocket_error(error: &CodexWebSocketExchangeError) -> bool { - match error { - CodexWebSocketExchangeError::Upstream(upstream) => { - StatusCode::from_u16(upstream.status_code) - .ok() - .is_some_and(|status| { - is_agent_identity_task_invalid_response(status, &upstream.body) - }) - } - CodexWebSocketExchangeError::PostSendAmbiguous { - source: Some(source), - .. - } => is_agent_identity_task_invalid_websocket_error(source), - _ => false, - } -} - -fn timestamp(now: DateTime) -> String { - now.to_rfc3339_opts(SecondsFormat::Secs, true) -} - -fn valid_identifier(value: &str) -> bool { - !value.is_empty() && value.len() <= 2_048 && !value.chars().any(char::is_control) -} diff --git a/backend/crates/providers/openai/src/credential/catalog.rs b/backend/crates/providers/openai/src/credential/catalog.rs index bf97bad20..0571eb84a 100644 --- a/backend/crates/providers/openai/src/credential/catalog.rs +++ b/backend/crates/providers/openai/src/credential/catalog.rs @@ -19,7 +19,6 @@ use thiserror::Error; use tokio::sync::Notify; use uuid::Uuid; -use super::agent_identity::CodexAgentIdentityTaskService; use super::repository::{CodexCredentialRepository, CredentialRepositoryError}; use crate::transport::profile::CodexWireProfileState; use crate::transport::{CodexBackendClient, CodexCatalogModel, CodexRequestContext}; @@ -182,7 +181,6 @@ pub struct CodexCredentialCatalogService { profile: CodexWireProfileState, http: reqwest::Client, base_url: String, - agent_identity: Arc, plan_catalog_cache: Arc, cache: Arc>, etags: Arc>, @@ -195,7 +193,6 @@ impl CodexCredentialCatalogService { profile: CodexWireProfileState, http: reqwest::Client, base_url: String, - agent_identity: Arc, plan_catalog_cache: Arc, ) -> Self { Self { @@ -203,7 +200,6 @@ impl CodexCredentialCatalogService { profile, http, base_url, - agent_identity, plan_catalog_cache, cache: Arc::new(RwLock::new(CatalogCacheState::default())), etags: Arc::new(Mutex::new(CatalogEtagState::default())), @@ -429,52 +425,24 @@ impl CodexCredentialCatalogService { client: &CodexBackendClient, account: &ProviderAccount, ) -> Result { - let mut prepared = self - .agent_identity - .prepare(account) + let credential = self + .repository + .load_runtime_credential(account) .await .map_err(|_| CodexCredentialCatalogError::InvalidCredentialData)?; - let mut request_id = format!("catalog_{}", Uuid::now_v7().simple()); - let mut authorization = prepared - .credential + let request_id = format!("catalog_{}", Uuid::now_v7().simple()); + let authorization = credential .authentication - .authorization_header(chrono::Utc::now()) + .authorization_header() .map_err(|_| CodexCredentialCatalogError::InvalidCredentialData)?; - let mut result = client + let result = client .fetch_models_with_context(CodexRequestContext::auxiliary( authorization.expose_secret(), - prepared.account.upstream_account_id(), + account.upstream_account_id(), &request_id, None, )) .await; - if let Err(error) = &result - && let Some(recovered) = self - .agent_identity - .recover_after_rejected_task( - prepared.account.id(), - &prepared.credential.authentication, - error, - ) - .await - .map_err(|_| CodexCredentialCatalogError::InvalidCredentialData)? - { - prepared = recovered; - request_id = format!("catalog_{}", Uuid::now_v7().simple()); - authorization = prepared - .credential - .authentication - .authorization_header(chrono::Utc::now()) - .map_err(|_| CodexCredentialCatalogError::InvalidCredentialData)?; - result = client - .fetch_models_with_context(CodexRequestContext::auxiliary( - authorization.expose_secret(), - prepared.account.upstream_account_id(), - &request_id, - None, - )) - .await; - } let snapshot = result.map_err(|error| CodexCredentialCatalogError::Upstream { detail: error.to_string(), })?; diff --git a/backend/crates/providers/openai/src/credential/mod.rs b/backend/crates/providers/openai/src/credential/mod.rs index 004fda2c0..fe831c05a 100644 --- a/backend/crates/providers/openai/src/credential/mod.rs +++ b/backend/crates/providers/openai/src/credential/mod.rs @@ -2,7 +2,6 @@ mod admin; mod affinity; -mod agent_identity; mod catalog; mod cookie; mod oauth; @@ -21,12 +20,6 @@ pub(crate) use affinity::{ pub(crate) use oauth::oauth_owner_ref; pub(crate) use types::parse_access_token_expiration; -pub use agent_identity::{ - CodexAgentIdentityError, CodexAgentIdentitySecret, CodexAgentIdentityTaskRegistrar, - CodexAgentIdentityTaskService, OfficialCodexAgentIdentityTaskRegistrar, - PreparedCodexRuntimeCredential, is_agent_identity_task_invalid_response, -}; - pub use admin::{ CodexCprExportDocument, CodexCredentialAdmin, CodexCredentialAdminError, CodexCredentialAdminService, ExportManagedCodexCredential, ImportCodexOAuthCredential, @@ -67,9 +60,7 @@ pub use selector::{ }; pub(crate) use selector::{CodexCyberPolicyScope, SelectCodexProviderEndpointCredential}; pub use types::{ - CODEX_AUTHENTICATION_KIND_AGENT_IDENTITY, CODEX_AUTHENTICATION_KIND_OAUTH, CodexAccountProfile, - CodexAgentIdentityAuthMode, CodexAgentIdentityCredentialData, CodexCookie, - CodexCookieCaptureOutcome, CodexCredentialData, CodexCredentialPrincipal, - CodexOAuthCredentialData, CodexOAuthSecret, RotateCodexCredential, RuntimeCodexCookie, - UpsertCodexCookie, + CODEX_AUTHENTICATION_KIND_OAUTH, CodexAccountProfile, CodexCookie, CodexCookieCaptureOutcome, + CodexCredentialData, CodexCredentialPrincipal, CodexOAuthCredentialData, CodexOAuthSecret, + RotateCodexCredential, RuntimeCodexCookie, UpsertCodexCookie, }; diff --git a/backend/crates/providers/openai/src/credential/quota/mod.rs b/backend/crates/providers/openai/src/credential/quota/mod.rs index c58413449..3054851aa 100644 --- a/backend/crates/providers/openai/src/credential/quota/mod.rs +++ b/backend/crates/providers/openai/src/credential/quota/mod.rs @@ -48,9 +48,8 @@ use crate::transport::{ CodexRateLimitResetCreditsConsumeResult, CodexRequestContext, }; -use super::agent_identity::{CodexAgentIdentityTaskService, PreparedCodexRuntimeCredential}; use super::repository::{CodexCredentialRepository, CredentialRepositoryError}; -use super::types::CODEX_AUTHENTICATION_KIND_OAUTH; +use super::security::CodexRuntimeCredential; use document::{ DEFAULT_CODEX_LIMIT_ID, RATE_LIMITS_BY_LIMIT_ID, RateLimitSnapshotsByLimitId, canonicalize_rate_limit_document, @@ -242,7 +241,6 @@ pub struct CodexCredentialQuotaService { profile: CodexWireProfileState, http: Client, base_url: String, - agent_identity: Arc, cooldowns: Arc, scheduling: CodexQuotaSchedulingProjection, reset_consume_locks: Mutex>>>, @@ -286,6 +284,11 @@ struct FetchedCodexQuota { value: Value, } +struct PreparedCodexRuntimeCredential { + account: ProviderAccount, + credential: CodexRuntimeCredential, +} + enum CodexQuotaFetchAttemptError { InvalidCredential, Upstream(CodexClientError), @@ -293,7 +296,6 @@ enum CodexQuotaFetchAttemptError { enum CodexQuotaFetchError { InvalidCredential, - Recovery, Upstream { account: Box, error: CodexClientError, @@ -312,11 +314,7 @@ enum DailyUsageAttemptError { enum DailyUsageFetchError { InvalidCredential, - Recovery, - Upstream { - prepared: Box, - error: CodexClientError, - }, + Upstream(CodexClientError), } struct FetchedDailyUsage { @@ -536,7 +534,6 @@ impl CodexCredentialQuotaService { profile: CodexWireProfileState, http: Client, base_url: String, - agent_identity: Arc, cooldowns: Arc, ) -> Self { Self { @@ -545,7 +542,6 @@ impl CodexCredentialQuotaService { profile, http, base_url, - agent_identity, cooldowns, scheduling: CodexQuotaSchedulingProjection::default(), reset_consume_locks: Mutex::new(HashMap::new()), @@ -563,28 +559,19 @@ impl CodexCredentialQuotaService { self.base_url.clone(), self.profile.clone(), ); - let mut prepared = self - .agent_identity - .prepare(&account) + let credential = self + .repository + .load_runtime_credential(&account) .await .map_err(|_| CodexResetCreditsError::InvalidCredentialData)?; + let prepared = PreparedCodexRuntimeCredential { + account, + credential, + }; let request_id = format!("reset_credits_{}", Uuid::now_v7().simple()); - let mut result = list_reset_credits_once(&client, &prepared, &request_id).await; - if let Err(ResetCreditAttemptError::Upstream(error)) = &result - && let Some(recovered) = self - .agent_identity - .recover_after_rejected_task( - prepared.account.id(), - &prepared.credential.authentication, - error, - ) - .await - .map_err(|_| CodexResetCreditsError::InvalidCredentialData)? - { - prepared = recovered; - result = list_reset_credits_once(&client, &prepared, &request_id).await; - } - result.map_err(|error| map_reset_credit_attempt_error(error, &prepared, false)) + list_reset_credits_once(&client, &prepared, &request_id) + .await + .map_err(|error| map_reset_credit_attempt_error(error, false)) } /// 查询当前账号的官方每日用量报表,并把报表范围锚定到最接近七天的额度窗口。 @@ -624,7 +611,7 @@ impl CodexCredentialQuotaService { self.profile.clone(), ); let FetchedCodexQuota { account, value } = self - .fetch_usage_with_recovery(&client, &account) + .fetch_usage(&client, &account) .await .map_err(map_statistics_quota_fetch_error)?; let snapshot = parse_account_quota_snapshot( @@ -672,7 +659,7 @@ impl CodexCredentialQuotaService { ) .ok_or(CodexUsageStatisticsError::InvalidRequest)?; let fetched = self - .fetch_daily_usage_with_recovery( + .fetch_daily_usage( &client, &account, period.query_start_date, @@ -716,45 +703,25 @@ impl CodexCredentialQuotaService { self.base_url.clone(), self.profile.clone(), ); - let mut prepared = self - .agent_identity - .prepare(&account) + let credential = self + .repository + .load_runtime_credential(&account) .await .map_err(|_| CodexResetCreditsError::InvalidCredentialData)?; + let prepared = PreparedCodexRuntimeCredential { + account, + credential, + }; let request_id = format!("reset_credit_consume_{}", Uuid::now_v7().simple()); - let mut result = consume_reset_credit_once( + consume_reset_credit_once( &client, &prepared, &request_id, credit_id, redeem_request_id, ) - .await; - // Agent Identity task recovery is the only automatic retry below. It keeps the exact - // official credit selection and redeem_request_id; generic HTTP/transport failures are - // never retried because the debit result may already be committed upstream. - if let Err(ResetCreditAttemptError::Upstream(error)) = &result - && let Some(recovered) = self - .agent_identity - .recover_after_rejected_task( - prepared.account.id(), - &prepared.credential.authentication, - error, - ) - .await - .map_err(|_| CodexResetCreditsError::InvalidCredentialData)? - { - prepared = recovered; - result = consume_reset_credit_once( - &client, - &prepared, - &request_id, - credit_id, - redeem_request_id, - ) - .await; - } - result.map_err(|error| map_reset_credit_attempt_error(error, &prepared, true)) + .await + .map_err(|error| map_reset_credit_attempt_error(error, true)) } async fn reset_credit_account( @@ -903,7 +870,7 @@ impl CodexCredentialQuotaService { ); for account in accounts { let observed_at = SystemTime::now(); - match self.fetch_usage_with_recovery(&client, &account).await { + match self.fetch_usage(&client, &account).await { Ok(FetchedCodexQuota { account, value }) => { // 单账号解析或落库失败只影响该账号;其余账号继续同步。 if let Err(error) = self @@ -921,13 +888,6 @@ impl CodexCredentialQuotaService { Err(CodexQuotaFetchError::InvalidCredential) => { summary.stale += 1; } - Err(CodexQuotaFetchError::Recovery) => { - summary.transient += 1; - tracing::warn!( - account_id = %account.id(), - "OpenAI quota credential recovery failed; refresh cycle will retry later" - ); - } Err(CodexQuotaFetchError::Upstream { account, error }) => { match classify_quota_endpoint_failure(&error) { Some(QuotaEndpointFailure::Exhausted(evidence)) => { @@ -1286,34 +1246,28 @@ impl CodexCredentialQuotaService { self.base_url.clone(), self.profile.clone(), ); - let FetchedCodexQuota { account, value } = - match self.fetch_usage_with_recovery(&client, &account).await { - Ok(fetched) => fetched, - Err(CodexQuotaFetchError::InvalidCredential) => { - return Err(CodexCredentialQuotaError::InvalidCredentialData); - } - Err(CodexQuotaFetchError::Recovery) => { - return Err(CodexCredentialQuotaError::Upstream { - detail: "credential recovery failed".to_owned(), - }); - } - Err(CodexQuotaFetchError::Upstream { account, error }) => { - match classify_quota_endpoint_failure(&error) { - Some(QuotaEndpointFailure::Exhausted(evidence)) => { - self.record_confirmed_exhaustion(&account, evidence, None, observed_at) - .await?; - } - Some(QuotaEndpointFailure::Credential { state, reason }) => { - self.persist_credential_failure(&account, state, reason, observed_at) - .await; - } - None => {} + let FetchedCodexQuota { account, value } = match self.fetch_usage(&client, &account).await { + Ok(fetched) => fetched, + Err(CodexQuotaFetchError::InvalidCredential) => { + return Err(CodexCredentialQuotaError::InvalidCredentialData); + } + Err(CodexQuotaFetchError::Upstream { account, error }) => { + match classify_quota_endpoint_failure(&error) { + Some(QuotaEndpointFailure::Exhausted(evidence)) => { + self.record_confirmed_exhaustion(&account, evidence, None, observed_at) + .await?; } - return Err(CodexCredentialQuotaError::Upstream { - detail: error.to_string(), - }); + Some(QuotaEndpointFailure::Credential { state, reason }) => { + self.persist_credential_failure(&account, state, reason, observed_at) + .await; + } + None => {} } - }; + return Err(CodexCredentialQuotaError::Upstream { + detail: error.to_string(), + }); + } + }; let object = normalize_quota_window_placeholders( value .as_object() @@ -1406,31 +1360,21 @@ impl CodexCredentialQuotaService { } } - async fn fetch_usage_with_recovery( + async fn fetch_usage( &self, client: &CodexBackendClient, account: &ProviderAccount, ) -> Result { - let mut prepared = self - .agent_identity - .prepare(account) + let credential = self + .repository + .load_runtime_credential(account) .await .map_err(|_| CodexQuotaFetchError::InvalidCredential)?; - let mut result = fetch_usage_with_5xx_retry(client, &prepared).await; - if let Err(CodexQuotaFetchAttemptError::Upstream(error)) = &result - && let Some(recovered) = self - .agent_identity - .recover_after_rejected_task( - prepared.account.id(), - &prepared.credential.authentication, - error, - ) - .await - .map_err(|_| CodexQuotaFetchError::Recovery)? - { - prepared = recovered; - result = fetch_usage_with_5xx_retry(client, &prepared).await; - } + let prepared = PreparedCodexRuntimeCredential { + account: account.clone(), + credential, + }; + let result = fetch_usage_with_5xx_retry(client, &prepared).await; match result { Ok(value) => Ok(FetchedCodexQuota { account: prepared.account, @@ -1448,42 +1392,31 @@ impl CodexCredentialQuotaService { } } - async fn fetch_daily_usage_with_recovery( + async fn fetch_daily_usage( &self, client: &CodexBackendClient, account: &ProviderAccount, start_date: chrono::NaiveDate, end_date: chrono::NaiveDate, ) -> Result { - let mut prepared = self - .agent_identity - .prepare(account) + let credential = self + .repository + .load_runtime_credential(account) .await .map_err(|_| DailyUsageFetchError::InvalidCredential)?; - let mut result = fetch_daily_usage_once(client, &prepared, start_date, end_date).await; - if let Err(DailyUsageAttemptError::Upstream(error)) = &result - && let Some(recovered) = self - .agent_identity - .recover_after_rejected_task( - prepared.account.id(), - &prepared.credential.authentication, - error, - ) - .await - .map_err(|_| DailyUsageFetchError::Recovery)? - { - prepared = recovered; - result = fetch_daily_usage_once(client, &prepared, start_date, end_date).await; - } + let prepared = PreparedCodexRuntimeCredential { + account: account.clone(), + credential, + }; + let result = fetch_daily_usage_once(client, &prepared, start_date, end_date).await; match result { Ok(value) => Ok(value), Err(DailyUsageAttemptError::InvalidCredential) => { Err(DailyUsageFetchError::InvalidCredential) } - Err(DailyUsageAttemptError::Upstream(error)) => Err(DailyUsageFetchError::Upstream { - prepared: Box::new(prepared), - error, - }), + Err(DailyUsageAttemptError::Upstream(error)) => { + Err(DailyUsageFetchError::Upstream(error)) + } } } } @@ -1497,7 +1430,7 @@ async fn fetch_daily_usage_once( let authorization = prepared .credential .authentication - .authorization_header(Utc::now()) + .authorization_header() .map_err(|_| DailyUsageAttemptError::InvalidCredential)?; let model_request_id = format!("usage_statistics_{}", Uuid::now_v7().simple()); let totals_request_id = format!("usage_statistics_{}", Uuid::now_v7().simple()); @@ -1534,31 +1467,21 @@ async fn fetch_daily_usage_once( fn map_statistics_quota_fetch_error(error: CodexQuotaFetchError) -> CodexUsageStatisticsError { match error { CodexQuotaFetchError::InvalidCredential => CodexUsageStatisticsError::InvalidCredentialData, - CodexQuotaFetchError::Recovery => CodexUsageStatisticsError::TransportUnavailable, - CodexQuotaFetchError::Upstream { account, error } => map_statistics_client_error( - error, - account.authentication_kind() == CODEX_AUTHENTICATION_KIND_OAUTH, - ), + CodexQuotaFetchError::Upstream { error, .. } => map_statistics_client_error(error), } } fn map_daily_usage_fetch_error(error: DailyUsageFetchError) -> CodexUsageStatisticsError { match error { DailyUsageFetchError::InvalidCredential => CodexUsageStatisticsError::InvalidCredentialData, - DailyUsageFetchError::Recovery => CodexUsageStatisticsError::TransportUnavailable, - DailyUsageFetchError::Upstream { prepared, error } => { - map_statistics_client_error(error, prepared.credential.authentication.oauth().is_some()) - } + DailyUsageFetchError::Upstream(error) => map_statistics_client_error(error), } } -fn map_statistics_client_error( - error: CodexClientError, - credential_can_refresh: bool, -) -> CodexUsageStatisticsError { +fn map_statistics_client_error(error: CodexClientError) -> CodexUsageStatisticsError { match error { CodexClientError::Upstream { status, body, .. } - if status == reqwest::StatusCode::UNAUTHORIZED && credential_can_refresh => + if status == reqwest::StatusCode::UNAUTHORIZED => { CodexUsageStatisticsError::CredentialRefreshRequired { upstream_body: Some(body), @@ -1586,7 +1509,7 @@ async fn list_reset_credits_once( let authorization = prepared .credential .authentication - .authorization_header(Utc::now()) + .authorization_header() .map_err(|_| ResetCreditAttemptError::InvalidCredential)?; client .list_rate_limit_reset_credits(CodexRequestContext::auxiliary( @@ -1609,7 +1532,7 @@ async fn consume_reset_credit_once( let authorization = prepared .credential .authentication - .authorization_header(Utc::now()) + .authorization_header() .map_err(|_| ResetCreditAttemptError::InvalidCredential)?; client .consume_rate_limit_reset_credit( @@ -1628,22 +1551,15 @@ async fn consume_reset_credit_once( fn map_reset_credit_attempt_error( error: ResetCreditAttemptError, - prepared: &PreparedCodexRuntimeCredential, consume: bool, ) -> CodexResetCreditsError { match error { ResetCreditAttemptError::InvalidCredential => CodexResetCreditsError::InvalidCredentialData, - ResetCreditAttemptError::Upstream(error) => { - map_reset_credit_client_error(error, prepared, consume) - } + ResetCreditAttemptError::Upstream(error) => map_reset_credit_client_error(error, consume), } } -fn map_reset_credit_client_error( - error: CodexClientError, - prepared: &PreparedCodexRuntimeCredential, - consume: bool, -) -> CodexResetCreditsError { +fn map_reset_credit_client_error(error: CodexClientError, consume: bool) -> CodexResetCreditsError { match error { CodexClientError::Upstream { status, @@ -1651,8 +1567,7 @@ fn map_reset_credit_client_error( diagnostics, .. } if status == reqwest::StatusCode::UNAUTHORIZED - && reset_credit_response_was_explicit_rejection(status, &diagnostics) - && prepared.credential.authentication.oauth().is_some() => + && reset_credit_response_was_explicit_rejection(status, &diagnostics) => { CodexResetCreditsError::CredentialRefreshRequired { upstream_body: Some(body), @@ -1735,7 +1650,7 @@ async fn fetch_usage_once( let authorization = prepared .credential .authentication - .authorization_header(Utc::now()) + .authorization_header() .map_err(|_| CodexQuotaFetchAttemptError::InvalidCredential)?; let request_id = format!("quota_{}", Uuid::now_v7().simple()); client diff --git a/backend/crates/providers/openai/src/credential/security.rs b/backend/crates/providers/openai/src/credential/security.rs index 4f9b28389..cae008945 100644 --- a/backend/crates/providers/openai/src/credential/security.rs +++ b/backend/crates/providers/openai/src/credential/security.rs @@ -5,15 +5,13 @@ use secrecy::{ExposeSecret, SecretString}; use serde_json::Value; use thiserror::Error; -use super::agent_identity::{CodexAgentIdentityError, CodexAgentIdentitySecret}; use super::types::{ - CodexAccountProfile, CodexAgentIdentityCredentialData, CodexCookie, CodexCredentialData, - CodexCredentialPrincipal, CodexOAuthCredentialData, CodexOAuthSecret, RuntimeCodexCookie, + CodexAccountProfile, CodexCookie, CodexCredentialData, CodexCredentialPrincipal, + CodexOAuthCredentialData, CodexOAuthSecret, RuntimeCodexCookie, }; const CODEX_CREDENTIAL_SCHEMA_VERSION: u32 = 1; const MAX_CREDENTIAL_BYTES: usize = 256 * 1024; -const MAX_TOKEN_BYTES: usize = 128 * 1024; const MAX_COOKIES: usize = 128; /// 已解析且只在 Provider 内可见的认证材料。 @@ -42,27 +40,15 @@ impl std::fmt::Debug for CodexRuntimeCredential { pub enum CodexRuntimeAuthentication { OAuth(CodexOAuthSecret), - AgentIdentity(Box), } impl CodexRuntimeAuthentication { - #[must_use] - pub const fn is_agent_identity(&self) -> bool { - matches!(self, Self::AgentIdentity(_)) - } - - pub fn authorization_header( - &self, - now: chrono::DateTime, - ) -> Result { + pub fn authorization_header(&self) -> Result { match self { Self::OAuth(secret) => Ok(SecretString::from(format!( "Bearer {}", secret.access_token.expose_secret() ))), - Self::AgentIdentity(secret) => secret - .authorization_header(now) - .map_err(|_| CodexCredentialDataError::Invalid), } } @@ -70,7 +56,6 @@ impl CodexRuntimeAuthentication { pub const fn oauth(&self) -> Option<&CodexOAuthSecret> { match self { Self::OAuth(secret) => Some(secret), - Self::AgentIdentity(_) => None, } } } @@ -79,7 +64,6 @@ impl std::fmt::Debug for CodexRuntimeAuthentication { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::OAuth(secret) => secret.fmt(formatter), - Self::AgentIdentity(secret) => secret.fmt(formatter), } } } @@ -147,12 +131,6 @@ impl CodexCredentialCodec { })) } - pub fn encode_agent_identity( - data: CodexAgentIdentityCredentialData, - ) -> Result { - Self::encode_complete(CodexCredentialData::AgentIdentity(data)) - } - pub fn encode_complete( data: CodexCredentialData, ) -> Result { @@ -200,21 +178,6 @@ impl CodexCredentialCodec { data.oauth_client_id, data.oauth_scope, ), - CodexCredentialData::AgentIdentity(data) => ( - CodexRuntimeAuthentication::AgentIdentity(Box::new( - CodexAgentIdentitySecret::from_pkcs8( - data.agent_runtime_id, - &data.agent_private_key, - data.task_id, - ) - .map_err(map_agent_identity_error)?, - )), - None, - data.installation_id, - data.cookies, - None, - None, - ), }; Ok(CodexRuntimeCredential { authentication, @@ -257,21 +220,6 @@ impl CodexCredentialCodec { (CodexCredentialData::OAuth(incoming), CodexCredentialData::OAuth(existing)) => { incoming.installation_id = existing.installation_id; } - ( - CodexCredentialData::AgentIdentity(incoming), - CodexCredentialData::AgentIdentity(existing), - ) => { - if incoming.agent_runtime_id != existing.agent_runtime_id { - return Err(CodexCredentialDataError::Invalid); - } - incoming.installation_id = existing.installation_id; - if incoming.task_id.is_none() - && incoming.agent_private_key == existing.agent_private_key - { - incoming.task_id = existing.task_id; - } - } - _ => return Err(CodexCredentialDataError::Invalid), } Self::encode_complete(incoming) } @@ -285,25 +233,6 @@ fn validate(data: &CodexCredentialData) -> Result<(), CodexCredentialDataError> } (&data.installation_id, &data.cookies) } - CodexCredentialData::AgentIdentity(data) => { - if data.schema_version != CODEX_CREDENTIAL_SCHEMA_VERSION - || !valid_identity(&data.agent_runtime_id) - || !valid_secret(&data.agent_private_key) - || data - .task_id - .as_deref() - .is_some_and(|value| !valid_identity(value)) - || CodexAgentIdentitySecret::from_pkcs8( - data.agent_runtime_id.clone(), - &data.agent_private_key, - data.task_id.clone(), - ) - .is_err() - { - return Err(CodexCredentialDataError::Invalid); - } - (&data.installation_id, &data.cookies) - } }; if !valid_installation_id(installation_id) || cookies.len() > MAX_COOKIES @@ -323,20 +252,8 @@ fn validate(data: &CodexCredentialData) -> Result<(), CodexCredentialDataError> Ok(()) } -const fn map_agent_identity_error(_: CodexAgentIdentityError) -> CodexCredentialDataError { - CodexCredentialDataError::Invalid -} - -fn valid_identity(value: &str) -> bool { - !value.is_empty() && value.len() <= 2_048 && !value.chars().any(char::is_control) -} - fn valid_installation_id(value: &str) -> bool { uuid::Uuid::parse_str(value) .ok() .is_some_and(|uuid| uuid.get_version_num() == 4) } - -fn valid_secret(value: &str) -> bool { - !value.is_empty() && value.len() <= MAX_TOKEN_BYTES && !value.chars().any(char::is_control) -} diff --git a/backend/crates/providers/openai/src/credential/selector.rs b/backend/crates/providers/openai/src/credential/selector.rs index a381abcca..cc16c98dd 100644 --- a/backend/crates/providers/openai/src/credential/selector.rs +++ b/backend/crates/providers/openai/src/credential/selector.rs @@ -22,7 +22,6 @@ use secrecy::ExposeSecret; use thiserror::Error; use url::Url; -use super::agent_identity::CodexAgentIdentityTaskService; use super::catalog::CodexCredentialCatalogService; use super::cookie::CodexCookiePolicy; use super::quota::CodexCredentialQuotaService; @@ -30,8 +29,7 @@ use super::refresh::refresh_recovery_deadline; use super::repository::{CodexCredentialRepository, CredentialRepositoryError}; use super::security::CodexRuntimeAuthentication; use super::types::{ - CODEX_AUTHENTICATION_KIND_AGENT_IDENTITY, CODEX_AUTHENTICATION_KIND_OAUTH, CodexCookie, - CodexCookieCaptureOutcome, RuntimeCodexCookie, + CODEX_AUTHENTICATION_KIND_OAUTH, CodexCookie, CodexCookieCaptureOutcome, RuntimeCodexCookie, }; const CLOUDFLARE_RECOVERY_STALE_AFTER: Duration = Duration::from_secs(60 * 60); @@ -120,7 +118,6 @@ pub struct CodexCredentialSelector { session_exclusions: Arc, catalog: Arc, quota: Arc, - agent_identity: Arc, cookie_policy: CodexCookiePolicy, risk_recovery: Mutex>, account_feedback: Arc, @@ -269,7 +266,6 @@ impl CodexCredentialSelector { session_exclusions: Arc, catalog: Arc, quota: Arc, - agent_identity: Arc, account_feedback: Arc, cookie_policy: CodexCookiePolicy, ) -> Self { @@ -281,7 +277,6 @@ impl CodexCredentialSelector { session_exclusions, catalog, quota, - agent_identity, cookie_policy, risk_recovery: Mutex::new(HashMap::new()), account_feedback, @@ -580,20 +575,7 @@ impl CodexCredentialSelector { account_switch = affinity_telemetry.account_switch, "OpenAI account selected" ); - let (account, runtime) = if account.authentication_kind() - == CODEX_AUTHENTICATION_KIND_AGENT_IDENTITY - { - let prepared = if allows_account_state_mutation { - self.agent_identity.prepare(&account).await - } else { - self.agent_identity.load_current(account.id()).await - } - .map_err(|_| CredentialSelectionError::InvalidCredential)?; - (prepared.account, prepared.credential) - } else { - let runtime = self.repository.load_runtime_credential(&account).await?; - (account, runtime) - }; + let runtime = self.repository.load_runtime_credential(&account).await?; let cookies = runtime .cookies .into_iter() diff --git a/backend/crates/providers/openai/src/credential/types.rs b/backend/crates/providers/openai/src/credential/types.rs index c310021a5..828953643 100644 --- a/backend/crates/providers/openai/src/credential/types.rs +++ b/backend/crates/providers/openai/src/credential/types.rs @@ -280,7 +280,6 @@ impl fmt::Debug for CodexCookie { } pub const CODEX_AUTHENTICATION_KIND_OAUTH: &str = "oauth"; -pub const CODEX_AUTHENTICATION_KIND_AGENT_IDENTITY: &str = "agent_identity"; /// Codex OAuth 对 `provider_credentials_json` 的完整明文 schema。 #[derive(Clone, Serialize, Deserialize)] @@ -323,49 +322,11 @@ impl fmt::Debug for CodexOAuthCredentialData { } } -/// Agent Identity 文档的固定认证模式。 -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub enum CodexAgentIdentityAuthMode { - #[serde(rename = "agentIdentity")] - AgentIdentity, -} - -/// OpenAI Agent Identity 对 `provider_credentials_json` 的完整明文 schema。 -#[derive(Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct CodexAgentIdentityCredentialData { - pub schema_version: u32, - pub auth_mode: CodexAgentIdentityAuthMode, - pub installation_id: String, - pub agent_runtime_id: String, - pub agent_private_key: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub task_id: Option, - #[serde(default)] - pub cookies: Vec, -} - -impl fmt::Debug for CodexAgentIdentityCredentialData { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter - .debug_struct("CodexAgentIdentityCredentialData") - .field("schema_version", &self.schema_version) - .field("auth_mode", &self.auth_mode) - .field("installation_id", &"") - .field("agent_runtime_id", &"") - .field("agent_private_key", &"") - .field("task_id", &self.task_id.as_ref().map(|_| "")) - .field("cookies", &self.cookies) - .finish() - } -} - -/// OpenAI Provider 支持的两种规范化凭据形态。 +/// OpenAI Provider 的规范化 OAuth 凭据形态。 #[derive(Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum CodexCredentialData { OAuth(CodexOAuthCredentialData), - AgentIdentity(CodexAgentIdentityCredentialData), } impl CodexCredentialData { @@ -373,7 +334,6 @@ impl CodexCredentialData { pub const fn authentication_kind(&self) -> &'static str { match self { Self::OAuth(_) => CODEX_AUTHENTICATION_KIND_OAUTH, - Self::AgentIdentity(_) => CODEX_AUTHENTICATION_KIND_AGENT_IDENTITY, } } @@ -381,14 +341,12 @@ impl CodexCredentialData { pub fn installation_id(&self) -> &str { match self { Self::OAuth(data) => &data.installation_id, - Self::AgentIdentity(data) => &data.installation_id, } } pub fn set_installation_id(&mut self, installation_id: String) { match self { Self::OAuth(data) => data.installation_id = installation_id, - Self::AgentIdentity(data) => data.installation_id = installation_id, } } @@ -396,14 +354,12 @@ impl CodexCredentialData { pub fn cookies(&self) -> &[CodexCookie] { match self { Self::OAuth(data) => &data.cookies, - Self::AgentIdentity(data) => &data.cookies, } } pub fn cookies_mut(&mut self) -> &mut Vec { match self { Self::OAuth(data) => &mut data.cookies, - Self::AgentIdentity(data) => &mut data.cookies, } } @@ -411,29 +367,12 @@ impl CodexCredentialData { pub fn oauth(&self) -> Option<&CodexOAuthCredentialData> { match self { Self::OAuth(data) => Some(data), - Self::AgentIdentity(_) => None, } } pub fn oauth_mut(&mut self) -> Option<&mut CodexOAuthCredentialData> { match self { Self::OAuth(data) => Some(data), - Self::AgentIdentity(_) => None, - } - } - - #[must_use] - pub fn agent_identity(&self) -> Option<&CodexAgentIdentityCredentialData> { - match self { - Self::OAuth(_) => None, - Self::AgentIdentity(data) => Some(data), - } - } - - pub fn agent_identity_mut(&mut self) -> Option<&mut CodexAgentIdentityCredentialData> { - match self { - Self::OAuth(_) => None, - Self::AgentIdentity(data) => Some(data), } } @@ -448,7 +387,6 @@ impl fmt::Debug for CodexCredentialData { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::OAuth(data) => data.fmt(formatter), - Self::AgentIdentity(data) => data.fmt(formatter), } } } diff --git a/backend/crates/providers/openai/src/lib.rs b/backend/crates/providers/openai/src/lib.rs index fdd7952a2..09fa8d69c 100644 --- a/backend/crates/providers/openai/src/lib.rs +++ b/backend/crates/providers/openai/src/lib.rs @@ -16,10 +16,9 @@ use gateway_core::task::WorkerContribution; use crate::admin::{OpenAiAdminProvider, OpenAiAdminServices, OpenAiOAuthPendingStore}; use crate::credential::token_client::{AuthorizationCodeExchanger, TokenRefresher}; use crate::credential::{ - CodexAgentIdentityTaskService, CodexCookiePolicy, CodexCredentialAdmin, - CodexCredentialAdminService, CodexCredentialCatalogService, CodexCredentialQuotaService, - CodexCredentialRefreshService, CodexCredentialRepository, CodexCredentialSelector, - CodexOAuthAdmin, CodexOAuthAdminService, OfficialCodexAgentIdentityTaskRegistrar, + CodexCookiePolicy, CodexCredentialAdmin, CodexCredentialAdminService, + CodexCredentialCatalogService, CodexCredentialQuotaService, CodexCredentialRefreshService, + CodexCredentialRepository, CodexCredentialSelector, CodexOAuthAdmin, CodexOAuthAdminService, }; use crate::transport::profile::{ CodexArtifactProfileCache, CodexDesktopReleaseService, OfficialCodexDesktopReleaseTransport, @@ -97,20 +96,11 @@ pub async fn initialize( let websocket_pool = Arc::new(CodexWebSocketPool::with_config( config.websocket_pool_config(), )); - let agent_identity = Arc::new(CodexAgentIdentityTaskService::new( - repository.clone(), - Arc::new( - OfficialCodexAgentIdentityTaskRegistrar::new(http.clone(), profile.clone()) - .map_err(|_| OpenAiInitializeError::AgentIdentity)?, - ), - Arc::clone(&websocket_pool), - )); let catalog = Arc::new(CodexCredentialCatalogService::new( repository.clone(), profile.clone(), http.clone(), config.base_url().to_owned(), - Arc::clone(&agent_identity), ports.catalog_cache(), )); let quota = Arc::new(CodexCredentialQuotaService::new( @@ -118,7 +108,6 @@ pub async fn initialize( profile.clone(), http.clone(), config.base_url().to_owned(), - Arc::clone(&agent_identity), ports.cooldowns(), )); let selector = Arc::new(CodexCredentialSelector::new( @@ -129,7 +118,6 @@ pub async fn initialize( session_exclusions, Arc::clone(&catalog), Arc::clone("a), - Arc::clone(&agent_identity), Arc::clone(&account_feedback), CodexCookiePolicy::official().map_err(|_| OpenAiInitializeError::CookiePolicy)?, )); @@ -138,7 +126,6 @@ pub async fn initialize( selector, Arc::clone(&catalog), Arc::clone("a), - agent_identity, account_feedback, http, profile.clone(), @@ -246,8 +233,6 @@ pub enum OpenAiInitializeError { CookiePolicy, #[error("OpenAI token client could not initialize")] TokenClient, - #[error("OpenAI Agent Identity service could not initialize")] - AgentIdentity, #[error("OpenAI credential administration could not initialize")] CredentialAdmin, #[error("OpenAI credential refresh could not initialize")] diff --git a/backend/crates/providers/openai/src/provider.rs b/backend/crates/providers/openai/src/provider.rs index aad0a62f0..a629faf06 100644 --- a/backend/crates/providers/openai/src/provider.rs +++ b/backend/crates/providers/openai/src/provider.rs @@ -49,12 +49,12 @@ use serde_json::{Map, Value, json}; use url::Url; use crate::credential::{ - CodexAccountFailure, CodexAgentIdentityTaskService, CodexCredentialCatalogError, - CodexCredentialCatalogService, CodexCredentialLease, CodexCredentialQuotaService, - CodexCredentialRefreshOutcome, CodexCredentialRefreshService, CodexCredentialSelector, - CodexCyberPolicyScope, CodexQuotaRefreshPolicy, CredentialSelectionError, RuntimeCodexCookie, - SelectCodexCredential, SelectCodexProviderEndpointCredential, - derive_codex_cyber_policy_session_key, derive_codex_session_affinity_key, + CodexAccountFailure, CodexCredentialCatalogError, CodexCredentialCatalogService, + CodexCredentialLease, CodexCredentialQuotaService, CodexCredentialRefreshOutcome, + CodexCredentialRefreshService, CodexCredentialSelector, CodexCyberPolicyScope, + CodexQuotaRefreshPolicy, CredentialSelectionError, RuntimeCodexCookie, SelectCodexCredential, + SelectCodexProviderEndpointCredential, derive_codex_cyber_policy_session_key, + derive_codex_session_affinity_key, }; use crate::transport::canonical::{ CodexCanonicalDecoder, CodexCanonicalError, CodexCanonicalOutcome, @@ -121,7 +121,6 @@ pub struct CodexProvider { selector: Arc, catalog: Arc, quota: Arc, - agent_identity: Arc, account_feedback: Arc, client: CodexBackendClient, responses_url: Url, @@ -137,7 +136,6 @@ impl CodexProvider { selector: Arc, catalog: Arc, quota: Arc, - agent_identity: Arc, account_feedback: Arc, http: Client, profile: CodexWireProfileState, @@ -157,7 +155,6 @@ impl CodexProvider { selector, catalog, quota, - agent_identity, account_feedback, client, responses_url, @@ -417,7 +414,6 @@ impl Provider for CodexProvider { selector: Arc::clone(&self.selector), quota: Arc::clone(&self.quota), catalog: Arc::clone(&self.catalog), - agent_identity: Arc::clone(&self.agent_identity), lease: Arc::clone(&lease), output_started_at, session_affinity_key, @@ -489,7 +485,6 @@ impl CodexProvider { context, selector: Arc::clone(&self.selector), quota: Arc::clone(&self.quota), - agent_identity: Arc::clone(&self.agent_identity), lease: Arc::clone(&lease), output_started_at: Instant::now(), }); @@ -512,7 +507,6 @@ struct ColdResponse { selector: Arc, quota: Arc, catalog: Arc, - agent_identity: Arc, lease: Arc, output_started_at: Instant, session_affinity_key: Option, @@ -528,7 +522,6 @@ struct ColdJsonResponse { context: AttemptContext, selector: Arc, quota: Arc, - agent_identity: Arc, lease: Arc, output_started_at: Instant, } @@ -641,7 +634,6 @@ enum CodexHandshakeAttemptError { Client(CodexClientError), Cancelled, Timeout, - AgentIdentityRecovery, } async fn create_response_attempt( @@ -678,9 +670,6 @@ fn map_handshake_attempt_error(error: CodexHandshakeAttemptError) -> MappedProvi ProviderErrorKind::Timeout, UpstreamSendState::Ambiguous, )), - CodexHandshakeAttemptError::AgentIdentityRecovery => MappedProviderFailure::plain( - provider_error(ProviderErrorKind::Unavailable, UpstreamSendState::Sent), - ), } } @@ -728,12 +717,12 @@ fn cold_json_response_stream(request: ColdJsonResponse) -> EventStream { cyber_policy_scope: None, allows_account_state_mutation, }; - let mut active_account = request.lease.account().clone(); + let active_account = request.lease.account().clone(); let cookie_header = build_cookie_header(request.lease.cookies())?; - let mut authorization = request + let authorization = request .lease .authentication() - .authorization_header(chrono::Utc::now()) + .authorization_header() .map_err(|_| { provider_error( ProviderErrorKind::Unauthorized, @@ -745,7 +734,7 @@ fn cold_json_response_stream(request: ColdJsonResponse) -> EventStream { request.lease.escape_reason(), request.lease.account_switch(), ); - let mut response = create_json_attempt( + let response = create_json_attempt( &request, &active_account, request.lease.installation_id(), @@ -754,44 +743,6 @@ fn cold_json_response_stream(request: ColdJsonResponse) -> EventStream { account_selection, ) .await; - if allows_account_state_mutation - && let Err(CodexHandshakeAttemptError::Client(error)) = &response - { - match request - .agent_identity - .recover_after_rejected_task( - active_account.id(), - request.lease.authentication(), - error, - ) - .await - { - Ok(Some(recovered)) => { - authorization = recovered - .credential - .authentication - .authorization_header(chrono::Utc::now()) - .map_err(|_| { - provider_error( - ProviderErrorKind::Unauthorized, - UpstreamSendState::NotSent, - ) - })?; - active_account = recovered.account; - response = create_json_attempt( - &request, - &active_account, - &recovered.credential.installation_id, - &authorization, - cookie_header.as_ref(), - account_selection, - ) - .await; - } - Ok(None) => {} - Err(_) => response = Err(CodexHandshakeAttemptError::AgentIdentityRecovery), - } - } let response = match response.map_err(map_handshake_attempt_error) { Ok(response) => response, Err(mut failure) => { @@ -867,7 +818,6 @@ fn cold_response_stream(response: ColdResponse) -> EventStream { selector, quota, catalog, - agent_identity, lease, output_started_at, session_affinity_key, @@ -886,9 +836,9 @@ fn cold_response_stream(response: ColdResponse) -> EventStream { }; let mut active_account = lease.account().clone(); let cookie_header = build_cookie_header(lease.cookies())?; - let mut authorization = lease + let authorization = lease .authentication() - .authorization_header(chrono::Utc::now()) + .authorization_header() .map_err(|_| { provider_error( ProviderErrorKind::Unauthorized, @@ -902,7 +852,7 @@ fn cold_response_stream(response: ColdResponse) -> EventStream { lease.escape_reason(), lease.account_switch(), ); - let mut response = create_response_attempt( + let response = create_response_attempt( &client, &request, codex_request_context( @@ -919,51 +869,6 @@ fn cold_response_stream(response: ColdResponse) -> EventStream { &cancellation, ) .await; - if allows_account_state_mutation - && let Err(CodexHandshakeAttemptError::Client(error)) = &response - { - match agent_identity - .recover_after_rejected_task( - active_account.id(), - lease.authentication(), - error, - ) - .await - { - Ok(Some(recovered)) => { - authorization = recovered - .credential - .authentication - .authorization_header(chrono::Utc::now()) - .map_err(|_| { - provider_error( - ProviderErrorKind::Unauthorized, - UpstreamSendState::NotSent, - ) - })?; - active_account = recovered.account; - response = create_response_attempt( - &client, - &request, - codex_request_context( - &request, - &request_id, - &active_account, - &recovered.credential.installation_id, - &authorization, - cookie_header.as_ref(), - account_selection, - ), - active_account.id().as_str(), - context.deadline(), - &cancellation, - ) - .await; - } - Ok(None) => {} - Err(_) => response = Err(CodexHandshakeAttemptError::AgentIdentityRecovery), - } - } let response = response.map_err(map_handshake_attempt_error); let response = match response { Ok(response) => response, diff --git a/backend/crates/providers/openai/tests/credential/agent_identity.rs b/backend/crates/providers/openai/tests/credential/agent_identity.rs deleted file mode 100644 index 3140b7d29..000000000 --- a/backend/crates/providers/openai/tests/credential/agent_identity.rs +++ /dev/null @@ -1,256 +0,0 @@ -use std::collections::VecDeque; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex}; - -use async_trait::async_trait; -use base64::Engine as _; -use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}; -use chrono::{TimeZone, Utc}; -use ed25519_dalek::pkcs8::EncodePrivateKey as _; -use ed25519_dalek::{Signature, SigningKey, Verifier as _}; -use gateway_core::engine::credential::{ - CredentialRevision, CredentialState, NewProviderAccount, ProviderAccount, ProviderAccountId, - ProviderAccountStore, QuotaState, -}; -use gateway_core::routing::ProviderKind; -use provider_openai::OpenAiConfig; -use provider_openai::credential::{ - CODEX_AUTHENTICATION_KIND_AGENT_IDENTITY, CodexAgentIdentityAuthMode, - CodexAgentIdentityCredentialData, CodexAgentIdentityError, CodexAgentIdentitySecret, - CodexAgentIdentityTaskRegistrar, CodexAgentIdentityTaskService, CodexCredentialCodec, - CodexRuntimeAuthentication, is_agent_identity_task_invalid_response, -}; -use provider_openai::transport::CodexWebSocketPool; -use provider_openai::transport::headers::build_codex_profile_headers; -use reqwest::StatusCode; -use reqwest::header::USER_AGENT; -use secrecy::ExposeSecret as _; -use serde_json::Value; - -use crate::support::MemoryAccountStore; - -fn signing_fixture(seed: u8) -> (SigningKey, String) { - let signing_key = SigningKey::from_bytes(&[seed; 32]); - let der = signing_key.to_pkcs8_der().expect("encode test PKCS#8 key"); - (signing_key, STANDARD.encode(der.as_bytes())) -} - -fn agent_account( - account_id: &str, - runtime_id: &str, - private_key: &str, - task_id: Option<&str>, -) -> NewProviderAccount { - let account_id = ProviderAccountId::new(account_id.to_owned()).expect("account id"); - let provider = ProviderKind::new("openai").expect("provider"); - let credential = - CodexCredentialCodec::encode_agent_identity(CodexAgentIdentityCredentialData { - schema_version: 1, - auth_mode: CodexAgentIdentityAuthMode::AgentIdentity, - installation_id: "00000000-0000-4000-8000-000000000001".to_owned(), - agent_runtime_id: runtime_id.to_owned(), - agent_private_key: private_key.to_owned(), - task_id: task_id.map(str::to_owned), - cookies: Vec::new(), - }) - .expect("encode Agent Identity credential"); - let account = ProviderAccount::new( - account_id, - provider, - "Agent Identity test".to_owned(), - Some("agent-user".to_owned()), - CODEX_AUTHENTICATION_KIND_AGENT_IDENTITY.to_owned(), - CredentialRevision::new(1).expect("revision"), - None, - ) - .with_profile( - Some("agent@example.com".to_owned()), - Some("agent-account".to_owned()), - Some("pro".to_owned()), - ) - .with_account_facts( - true, - CredentialState::Ready, - QuotaState::unknown(), - None, - None, - ) - .with_refresh_schedule(false, None); - NewProviderAccount { - account, - credential, - } -} - -#[test] -fn agent_identity_secret_builds_a_verifiable_assertion_without_leaking_material() { - let (signing_key, private_key) = signing_fixture(7); - let secret = CodexAgentIdentitySecret::from_pkcs8( - "runtime-test".to_owned(), - &private_key, - Some("task-test".to_owned()), - ) - .expect("parse Agent Identity secret"); - let now = Utc - .with_ymd_and_hms(2026, 7, 23, 1, 2, 3) - .single() - .expect("timestamp"); - let header = secret - .authorization_header(now) - .expect("build AgentAssertion"); - let encoded = header - .expose_secret() - .strip_prefix("AgentAssertion ") - .expect("AgentAssertion scheme"); - let envelope: Value = serde_json::from_slice( - &URL_SAFE_NO_PAD - .decode(encoded) - .expect("decode assertion envelope"), - ) - .expect("parse assertion envelope"); - assert_eq!(envelope["agent_runtime_id"], "runtime-test"); - assert_eq!(envelope["task_id"], "task-test"); - assert_eq!(envelope["timestamp"], "2026-07-23T01:02:03Z"); - let signature = Signature::from_slice( - &STANDARD - .decode(envelope["signature"].as_str().expect("signature")) - .expect("decode signature"), - ) - .expect("signature shape"); - signing_key - .verifying_key() - .verify(b"runtime-test:task-test:2026-07-23T01:02:03Z", &signature) - .expect("verify AgentAssertion"); - let debug = format!("{secret:?}"); - assert!(!debug.contains(&private_key)); - assert!(!debug.contains("runtime-test")); - assert!(!debug.contains("task-test")); -} - -#[test] -fn agent_identity_task_error_classifier_requires_unauthorized_task_error() { - assert!(is_agent_identity_task_invalid_response( - StatusCode::UNAUTHORIZED, - r#"{"error":{"code":"task_not_found"}}"#, - )); - assert!(is_agent_identity_task_invalid_response( - StatusCode::UNAUTHORIZED, - "task expired", - )); - assert!(!is_agent_identity_task_invalid_response( - StatusCode::FORBIDDEN, - r#"{"error":{"code":"task_not_found"}}"#, - )); - assert!(!is_agent_identity_task_invalid_response( - StatusCode::UNAUTHORIZED, - "invalid access token", - )); -} - -#[test] -fn agent_identity_task_registration_should_use_the_shared_desktop_profile_headers() { - let profile = OpenAiConfig::default().wire_profile_state().snapshot(); - let headers = build_codex_profile_headers(&profile).expect("Desktop profile headers"); - - assert_eq!(headers.len(), 3); - assert_eq!(headers["originator"], "Codex Desktop"); - assert_eq!(headers["x-openai-internal-codex-residency"], "us"); - assert_eq!( - headers[USER_AGENT], - "Codex Desktop/0.147.0-alpha.6.6 (Mac OS 15.7.1; arm64) unknown (Codex Desktop; 26.803.81509)" - ); -} - -struct SequenceTaskRegistrar { - task_ids: Mutex>, - calls: AtomicUsize, -} - -#[async_trait] -impl CodexAgentIdentityTaskRegistrar for SequenceTaskRegistrar { - async fn register( - &self, - _credential: &CodexAgentIdentitySecret, - ) -> Result { - self.calls.fetch_add(1, Ordering::SeqCst); - self.task_ids - .lock() - .expect("task registrar lock") - .pop_front() - .ok_or(CodexAgentIdentityError::TaskRegistrationUnavailable) - } -} - -fn task_service( - store: &Arc, - registrar: Arc, -) -> CodexAgentIdentityTaskService { - CodexAgentIdentityTaskService::new( - store.repository(), - registrar, - Arc::new(CodexWebSocketPool::default()), - ) -} - -#[tokio::test] -async fn task_registration_and_recovery_are_revision_fenced() { - let (signing_key, private_key) = signing_fixture(8); - let runtime_id = format!( - "runtime-{}", - hex::encode(signing_key.verifying_key().as_bytes()) - ); - let store = Arc::new(MemoryAccountStore::default()); - store - .create_account(agent_account( - "acct_agent_task", - &runtime_id, - &private_key, - None, - )) - .await - .expect("seed Agent Identity account"); - let registrar = Arc::new(SequenceTaskRegistrar { - task_ids: Mutex::new(VecDeque::from([ - "task-first".to_owned(), - "task-recovered".to_owned(), - ])), - calls: AtomicUsize::new(0), - }); - let service = task_service(&store, Arc::clone(®istrar)); - let current = store.account("acct_agent_task").expect("current account"); - let prepared = service.prepare(¤t).await.expect("register task"); - assert_eq!(prepared.account.revision().get(), 2); - let CodexRuntimeAuthentication::AgentIdentity(secret) = prepared.credential.authentication - else { - panic!("expected Agent Identity runtime credential"); - }; - assert_eq!(secret.task_id(), Some("task-first")); - assert_eq!(registrar.calls.load(Ordering::SeqCst), 1); - - let current = store.account("acct_agent_task").expect("updated account"); - let prepared = service - .prepare(¤t) - .await - .expect("reuse registered task"); - assert_eq!(prepared.account.revision().get(), 2); - assert_eq!(registrar.calls.load(Ordering::SeqCst), 1); - - let recovered = service - .recover(prepared.account.id(), "task-first") - .await - .expect("rejected task must register a replacement"); - assert_eq!(recovered.account.revision().get(), 3); - assert_eq!(registrar.calls.load(Ordering::SeqCst), 2); - let CodexRuntimeAuthentication::AgentIdentity(secret) = recovered.credential.authentication - else { - panic!("expected Agent Identity runtime credential"); - }; - assert_eq!(secret.task_id(), Some("task-recovered")); - - let fenced = service - .recover(recovered.account.id(), "task-first") - .await - .expect("stale task recovery must keep the newer task"); - assert_eq!(fenced.account.revision().get(), 3); - assert_eq!(registrar.calls.load(Ordering::SeqCst), 2); -} diff --git a/backend/crates/providers/openai/tests/credential/catalog.rs b/backend/crates/providers/openai/tests/credential/catalog.rs index 880ebe59a..c78e2b401 100644 --- a/backend/crates/providers/openai/tests/credential/catalog.rs +++ b/backend/crates/providers/openai/tests/credential/catalog.rs @@ -13,7 +13,7 @@ use provider_openai::transport::profile::{CodexWireProfile, CodexWireProfileStat use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, Request, Respond, ResponseTemplate}; -use crate::support::{MemoryAccountStore, agent_identity_service, catalog_cache, profile, secret}; +use crate::support::{MemoryAccountStore, catalog_cache, profile, secret}; const OFFICIAL_FIXTURE: &[u8] = include_bytes!("../transport/fixtures/official_models_snapshot.json"); @@ -84,7 +84,6 @@ fn service_with_catalog_cache( .build() .expect("client"), base_url, - agent_identity_service(store), catalog_cache, ) } diff --git a/backend/crates/providers/openai/tests/credential/contract.rs b/backend/crates/providers/openai/tests/credential/contract.rs index 461a19dc2..e8126ba62 100644 --- a/backend/crates/providers/openai/tests/credential/contract.rs +++ b/backend/crates/providers/openai/tests/credential/contract.rs @@ -37,7 +37,7 @@ use url::Url; use crate::support::{ MemoryAccountStore, MemoryCooldownPort, MemorySessionAffinity, MemorySessionExclusions, - TestLeaseCoordinator, account_policy, agent_identity_service, catalog_cache, profile, secret, + TestLeaseCoordinator, account_policy, catalog_cache, profile, secret, }; fn create_account(store: &Arc, id: &str, token: &str) { @@ -162,13 +162,11 @@ fn selector_with_runtime( verified_at: chrono::Utc::now(), }); let http = reqwest::Client::builder().build().expect("HTTP client"); - let agent_identity = agent_identity_service(store); let catalog = Arc::new(CodexCredentialCatalogService::new( store.repository(), profile.clone(), http.clone(), OFFICIAL_CODEX_BASE_URL.to_owned(), - Arc::clone(&agent_identity), catalog_cache(), )); let quota = Arc::new(CodexCredentialQuotaService::new( @@ -176,7 +174,6 @@ fn selector_with_runtime( profile, http, OFFICIAL_CODEX_BASE_URL.to_owned(), - Arc::clone(&agent_identity), cooldowns, )); CodexCredentialSelector::new( @@ -187,7 +184,6 @@ fn selector_with_runtime( Arc::new(MemorySessionExclusions::default()), catalog, quota, - agent_identity, account_feedback, CodexCookiePolicy::official().expect("official cookie policy"), ) diff --git a/backend/crates/providers/openai/tests/credential/mod.rs b/backend/crates/providers/openai/tests/credential/mod.rs index 98045b462..008cf5bad 100644 --- a/backend/crates/providers/openai/tests/credential/mod.rs +++ b/backend/crates/providers/openai/tests/credential/mod.rs @@ -1,5 +1,4 @@ mod admin; -mod agent_identity; mod catalog; mod contract; mod cookie; diff --git a/backend/crates/providers/openai/tests/credential/quota/mod.rs b/backend/crates/providers/openai/tests/credential/quota/mod.rs index ebeca8371..0060a70e1 100644 --- a/backend/crates/providers/openai/tests/credential/quota/mod.rs +++ b/backend/crates/providers/openai/tests/credential/quota/mod.rs @@ -62,7 +62,6 @@ fn quota_service_with_base_url( wire_profile(), http, base_url, - crate::support::agent_identity_service(store), Arc::new(crate::support::MemoryCooldownPort::new()), ) } diff --git a/backend/crates/providers/openai/tests/provider/contract.rs b/backend/crates/providers/openai/tests/provider/contract.rs index 78e5d6aff..290cb9710 100644 --- a/backend/crates/providers/openai/tests/provider/contract.rs +++ b/backend/crates/providers/openai/tests/provider/contract.rs @@ -53,8 +53,7 @@ use wiremock::{Mock, MockServer, ResponseTemplate}; use crate::support::{ MemoryAccountStore, MemoryCooldownPort, MemorySessionAffinity, MemorySessionExclusions, - TestLeaseCoordinator, account_policy, agent_identity_service_with_pool, catalog_cache, profile, - secret, + TestLeaseCoordinator, account_policy, catalog_cache, profile, secret, }; use crate::transport::accept_codex_test_websocket; @@ -146,13 +145,11 @@ fn provider_and_quota_with_affinity_and_base_url_and_leases( .build() .expect("client"); let websocket_pool = Arc::new(CodexWebSocketPool::default()); - let agent_identity = agent_identity_service_with_pool(store, Arc::clone(&websocket_pool)); let catalog = Arc::new(CodexCredentialCatalogService::new( store.repository(), profile.clone(), http.clone(), base_url.clone(), - Arc::clone(&agent_identity), catalog_cache(), )); let quota = Arc::new(CodexCredentialQuotaService::new( @@ -160,7 +157,6 @@ fn provider_and_quota_with_affinity_and_base_url_and_leases( profile.clone(), http.clone(), base_url.clone(), - Arc::clone(&agent_identity), Arc::new(MemoryCooldownPort::new()), )); let account_feedback = Arc::new(AccountFeedbackStats::default()); @@ -172,7 +168,6 @@ fn provider_and_quota_with_affinity_and_base_url_and_leases( Arc::new(MemorySessionExclusions::default()), Arc::clone(&catalog), Arc::clone("a), - Arc::clone(&agent_identity), Arc::clone(&account_feedback), CodexCookiePolicy::official().expect("cookie policy"), )); @@ -181,7 +176,6 @@ fn provider_and_quota_with_affinity_and_base_url_and_leases( selector, catalog, Arc::clone("a), - agent_identity, account_feedback, http, profile, diff --git a/backend/crates/providers/openai/tests/support.rs b/backend/crates/providers/openai/tests/support.rs index 38522def5..2dd1d36dd 100644 --- a/backend/crates/providers/openai/tests/support.rs +++ b/backend/crates/providers/openai/tests/support.rs @@ -27,11 +27,9 @@ use gateway_core::provider_ports::{ }; use gateway_core::routing::ProviderKind; use provider_openai::credential::{ - CodexAccountProfile, CodexAgentIdentityTaskService, CodexCredentialAdmin, - CodexCredentialRepository, CodexOAuthSecret, ImportCodexOAuthCredential, - OfficialCodexAgentIdentityTaskRegistrar, + CodexAccountProfile, CodexCredentialAdmin, CodexCredentialRepository, CodexOAuthSecret, + ImportCodexOAuthCredential, }; -use provider_openai::transport::CodexWebSocketPool; use secrecy::SecretString; #[derive(Clone)] @@ -510,32 +508,6 @@ fn rebuild_account(current: &ProviderAccount, rebuild: AccountRebuild) -> Provid .with_refresh_schedule(rebuild.has_refresh_token, rebuild.next_refresh_at) } -pub(crate) fn agent_identity_service( - store: &Arc, -) -> Arc { - agent_identity_service_with_pool(store, Arc::new(CodexWebSocketPool::default())) -} - -pub(crate) fn agent_identity_service_with_pool( - store: &Arc, - websocket_pool: Arc, -) -> Arc { - let client = reqwest::Client::builder() - .no_proxy() - .build() - .expect("agent task client"); - let registrar = OfficialCodexAgentIdentityTaskRegistrar::new( - client, - provider_openai::OpenAiConfig::default().wire_profile_state(), - ) - .expect("agent task registrar"); - Arc::new(CodexAgentIdentityTaskService::new( - store.repository(), - Arc::new(registrar), - websocket_pool, - )) -} - #[derive(Default)] pub(crate) struct TestLeaseCoordinator { pub(crate) requests: Mutex>, diff --git a/docs/api.md b/docs/api.md index b40aa3c1f..9d1f9a29b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -131,7 +131,7 @@ Grok wire 与 Responses wire 之间的协议转换层,转换只在 xAI Provide 导入的 `data` 必须是 JSON object,Admin API 请求上限为 64 MiB;Provider 可以收紧限制, 当前 xAI 导入上限为 16 MiB。内部 schema 由目标 Provider 独占解释: -- OpenAI 接受单账号 OAuth 文档、`accounts` 数组(最多 200 项)、CPR 账号 bundle 和 Agent Identity; +- OpenAI 接受单账号 OAuth 文档、`accounts` 数组(最多 200 项)和 CPR 账号 bundle; - OpenAI OAuth token 字段只识别 `accessToken`、`refreshToken`、`idToken`,可以嵌套在账号 object 内; 每项至少包含 AT 或 RT。RT-only 会在导入时换取 AT,AT-only 不具备自动续期能力; - xAI 从单账号 object 或 `accounts` 数组中提取 OAuth token;包装中的代理、并发、优先级等字段不参与认证; @@ -160,25 +160,7 @@ RT-only 使用同一形状,只提交 `refreshToken`。不得把真实 token 重新授权以及普通 credential refresh/rotation 均保留已有分组。分组关系只通过账号编辑维护。 账号列表的每个 item 返回轻量 `groups: [{ id, name, enabled }]`。 -OpenAI 的 CPR 导出保持 OAuth 账号的既有字段;Agent Identity 账号则输出 -`authMode: "agentIdentity"`、`agentRuntimeId`、`agentPrivateKey` 和可选的 -`taskId`,不伪造 OAuth token 或过期时间。一个 `sourceFormat: "cpr"` 文档可以包含两种 -认证类型,导入时按每个账号的认证模式分别解析。 - -```json -{ - "sourceFormat": "cpr", - "accounts": [{ - "id": "acct_...", - "accountId": "...", - "userId": "...", - "authMode": "agentIdentity", - "agentRuntimeId": "...", - "agentPrivateKey": "...", - "taskId": "..." - }] -} -``` +OpenAI 的 CPR 导出保持 OAuth 账号的既有 token 与过期时间字段。 OpenAI rotation 请求字段为: diff --git a/docs/architecture.md b/docs/architecture.md index cecdd7a09..5b4183fe7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -59,7 +59,7 @@ flowchart LR | `gateway-api` | HTTP/WS/SSE 解码与交付、Admin wire、静态管理端;不直接访问 Store 或具体 Provider | | `gateway-store` | PostgreSQL、Redis、S3/R2、`pg_dump` 适配器;不拥有业务策略 | | `gateway-host` | 配置加载、日志、HTTP 生命周期、Worker 监督和系统更新 | -| `providers/openai` | OpenAI OAuth/Agent Identity、账号选择、目录、额度、Responses/Images transport | +| `providers/openai` | OpenAI OAuth、账号选择、目录、额度、Responses/Images transport | | `providers/xai` | xAI OAuth session、账号选择、目录、额度和 Grok/Responses 转换 | | `frontend` | Vue 管理端,仅通过 Admin API 读写状态 | @@ -189,7 +189,7 @@ PostgreSQL 周期对账才是正确性基础。 credential 与 quota 是两组独立事实:credential refresh 不等于 quota refresh,额度接口的 401/403 也不能单独证明 refresh token 永久失效。 -- OpenAI 支持 OAuth、AT/RT、OAuth JSON 与 Agent Identity;RT-only 导入先换取 AT,AT-only 导入没有 +- OpenAI 支持 OAuth、AT/RT 与 OAuth JSON;RT-only 导入先换取 AT,AT-only 导入没有 自动续期能力。OAuth 身份只从官方 JWT claims 投影,不信任导入文档顶层身份字段。 - xAI 使用 OAuth session;API Key 不是受支持的账号 credential。 - 新账号导入和首次 OAuth 在 credential 提交后尽力读取一次额度;失败只留下观测,不回滚账号事务。 diff --git a/frontend/src/components/ProviderIconGroup.vue b/frontend/src/components/ProviderIconGroup.vue index 2ba7abd22..72dce492f 100644 --- a/frontend/src/components/ProviderIconGroup.vue +++ b/frontend/src/components/ProviderIconGroup.vue @@ -1,5 +1,5 @@