diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index fcfb23eeba..4e851379ed 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -840,8 +840,6 @@ "dylint_linting_5.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert_cmd\",\"req\":\"^2.0\"},{\"name\":\"cargo_metadata\",\"req\":\"^0.23\"},{\"features\":[\"config\"],\"name\":\"dylint_internal\",\"req\":\"=5.0.0\"},{\"name\":\"paste\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rustc_version\",\"req\":\"^0.4\"},{\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.23\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"},{\"name\":\"toml\",\"req\":\"^0.9\"},{\"kind\":\"build\",\"name\":\"toml\",\"req\":\"^0.9\"}],\"features\":{\"constituent\":[]}}", "dylint_testing_5.0.0": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"cargo_metadata\",\"req\":\"^0.23\"},{\"name\":\"compiletest_rs\",\"req\":\"^0.11\"},{\"name\":\"dylint\",\"req\":\"=5.0.0\"},{\"name\":\"dylint_internal\",\"req\":\"=5.0.0\"},{\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"name\":\"once_cell\",\"req\":\"^1.21\"},{\"name\":\"regex\",\"req\":\"^1.11\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"tempfile\",\"req\":\"^3.23\"}],\"features\":{\"default\":[],\"deny_warnings\":[]}}", "dyn-clone_1.0.20": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.66\"}],\"features\":{}}", - "ed25519-dalek_2.2.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"blake2\",\"req\":\"^0.10\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"default_features\":false,\"features\":[\"digest\"],\"name\":\"curve25519-dalek\",\"req\":\"^4\"},{\"default_features\":false,\"features\":[\"digest\",\"rand_core\"],\"kind\":\"dev\",\"name\":\"curve25519-dalek\",\"req\":\"^4\"},{\"default_features\":false,\"name\":\"ed25519\",\"req\":\">=2.2, <2.3\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"merlin\",\"optional\":true,\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6.4\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6.4\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"sha2\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"sha3\",\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"signature\",\"optional\":true,\"req\":\">=2.0, <2.3\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.3.0\"},{\"kind\":\"dev\",\"name\":\"toml\",\"req\":\"^0.7\"},{\"default_features\":false,\"features\":[\"static_secrets\"],\"kind\":\"dev\",\"name\":\"x25519-dalek\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.5\"}],\"features\":{\"alloc\":[\"curve25519-dalek/alloc\",\"ed25519/alloc\",\"serde?/alloc\",\"zeroize/alloc\"],\"asm\":[\"sha2/asm\"],\"batch\":[\"alloc\",\"merlin\",\"rand_core\"],\"default\":[\"fast\",\"std\",\"zeroize\"],\"digest\":[\"signature/digest\"],\"fast\":[\"curve25519-dalek/precomputed-tables\"],\"hazmat\":[],\"legacy_compatibility\":[\"curve25519-dalek/legacy_compatibility\"],\"pem\":[\"alloc\",\"ed25519/pem\",\"pkcs8\"],\"pkcs8\":[\"ed25519/pkcs8\"],\"rand_core\":[\"dep:rand_core\"],\"serde\":[\"dep:serde\",\"ed25519/serde\"],\"std\":[\"alloc\",\"ed25519/std\",\"serde?/std\",\"sha2/std\"],\"zeroize\":[\"dep:zeroize\",\"curve25519-dalek/zeroize\"]}}", - "ed25519_2.2.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"features\":[\"rand_core\"],\"kind\":\"dev\",\"name\":\"ed25519-dalek\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"name\":\"pkcs8\",\"optional\":true,\"req\":\"^0.10\"},{\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"default_features\":false,\"features\":[\"signature\"],\"kind\":\"dev\",\"name\":\"ring-compat\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"serde_bytes\",\"optional\":true,\"req\":\"^0.11\"},{\"default_features\":false,\"name\":\"signature\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"pkcs8?/alloc\"],\"default\":[\"std\"],\"pem\":[\"alloc\",\"pkcs8/pem\"],\"serde_bytes\":[\"serde\",\"dep:serde_bytes\"],\"std\":[\"pkcs8?/std\",\"signature/std\"]}}", "either_1.15.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"alloc\",\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.95\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[],\"use_std\":[\"std\"]}}", "ena_0.14.3": "{\"dependencies\":[{\"name\":\"dogged\",\"optional\":true,\"req\":\"^0.2.0\"},{\"name\":\"log\",\"req\":\"^0.4\"}],\"features\":{\"bench\":[],\"persistent\":[\"dogged\"]}}", "encode_unicode_1.0.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"ascii\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.0\",\"target\":\"cfg(unix)\"},{\"features\":[\"https-native\"],\"kind\":\"dev\",\"name\":\"minreq\",\"req\":\"^2.6\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 50d19a40a7..0a90957908 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1912,7 +1912,6 @@ dependencies = [ "codex-api", "codex-app-server-protocol", "codex-apply-patch", - "codex-arg0", "codex-async-utils", "codex-code-mode", "codex-config", @@ -1967,7 +1966,6 @@ dependencies = [ "ctor 0.6.3", "dirs", "dunce", - "ed25519-dalek", "env-flags", "eventsource-stream", "futures", @@ -3742,7 +3740,6 @@ dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", - "digest", "fiat-crypto", "rustc_version", "subtle", @@ -4219,7 +4216,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4323,30 +4320,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8", - "signature", -] - -[[package]] -name = "ed25519-dalek" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" -dependencies = [ - "curve25519-dalek", - "ed25519", - "serde", - "sha2", - "subtle", - "zeroize", -] - [[package]] name = "either" version = "1.15.0" @@ -6845,7 +6818,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -12028,7 +12001,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index eb6cd1df17..92db5ef4ef 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -234,7 +234,6 @@ diffy = "0.4.2" dirs = "6" dotenvy = "0.15.7" dunce = "1.0.4" -ed25519-dalek = { version = "2.2.0", features = ["pkcs8"] } encoding_rs = "0.8.35" env-flags = "0.1.1" env_logger = "0.11.9" diff --git a/codex-rs/app-server/src/transport/remote_control/tests.rs b/codex-rs/app-server/src/transport/remote_control/tests.rs index d702a99c2c..eb49116167 100644 --- a/codex-rs/app-server/src/transport/remote_control/tests.rs +++ b/codex-rs/app-server/src/transport/remote_control/tests.rs @@ -97,7 +97,6 @@ fn remote_control_auth_dot_json(account_id: Option<&str>) -> AuthDotJson { account_id: account_id.map(str::to_string), }), last_refresh: Some(chrono::Utc::now()), - agent_identity: None, } } diff --git a/codex-rs/app-server/src/transport/remote_control/websocket.rs b/codex-rs/app-server/src/transport/remote_control/websocket.rs index 58df3035aa..5ebbb12eb7 100644 --- a/codex-rs/app-server/src/transport/remote_control/websocket.rs +++ b/codex-rs/app-server/src/transport/remote_control/websocket.rs @@ -999,7 +999,6 @@ mod tests { account_id: Some("account_id".to_string()), }), last_refresh: Some(Utc::now()), - agent_identity: None, } } diff --git a/codex-rs/app-server/tests/common/auth_fixtures.rs b/codex-rs/app-server/tests/common/auth_fixtures.rs index 86f0fb456d..99334f0770 100644 --- a/codex-rs/app-server/tests/common/auth_fixtures.rs +++ b/codex-rs/app-server/tests/common/auth_fixtures.rs @@ -163,7 +163,6 @@ pub fn write_chatgpt_auth( openai_api_key: None, tokens: Some(tokens), last_refresh, - agent_identity: None, }; save_auth(codex_home, &auth, cli_auth_credentials_store_mode).context("write auth.json") diff --git a/codex-rs/app-server/tests/suite/v2/app_list.rs b/codex-rs/app-server/tests/suite/v2/app_list.rs index 78a915d178..7aa8034528 100644 --- a/codex-rs/app-server/tests/suite/v2/app_list.rs +++ b/codex-rs/app-server/tests/suite/v2/app_list.rs @@ -119,7 +119,6 @@ async fn list_apps_returns_empty_with_api_key_auth() -> Result<()> { openai_api_key: Some("test-api-key".to_string()), tokens: None, last_refresh: None, - agent_identity: None, }, AuthCredentialsStoreMode::File, )?; diff --git a/codex-rs/core/BUILD.bazel b/codex-rs/core/BUILD.bazel index dd52bce43d..df5f4da1fa 100644 --- a/codex-rs/core/BUILD.bazel +++ b/codex-rs/core/BUILD.bazel @@ -57,7 +57,6 @@ codex_rust_crate( "//codex-rs/linux-sandbox:codex-linux-sandbox", "//codex-rs/rmcp-client:test_stdio_server", "//codex-rs/rmcp-client:test_streamable_http_server", - "//codex-rs/responses-api-proxy:codex-responses-api-proxy", "//codex-rs/cli:codex", ], ) diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index ab87ded32c..61d5d3ff49 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -42,7 +42,6 @@ codex-login = { workspace = true } codex-mcp = { workspace = true } codex-model-provider-info = { workspace = true } codex-models-manager = { workspace = true } -ed25519-dalek = { workspace = true } codex-shell-command = { workspace = true } codex-execpolicy = { workspace = true } codex-git-utils = { workspace = true } @@ -147,7 +146,6 @@ codex-shell-escalation = { workspace = true } [dev-dependencies] assert_cmd = { workspace = true } assert_matches = { workspace = true } -codex-arg0 = { workspace = true } codex-otel = { workspace = true } codex-test-binary-support = { workspace = true } codex-utils-cargo-bin = { workspace = true } diff --git a/codex-rs/core/src/agent_identity.rs b/codex-rs/core/src/agent_identity.rs deleted file mode 100644 index 7e86ba22ae..0000000000 --- a/codex-rs/core/src/agent_identity.rs +++ /dev/null @@ -1,771 +0,0 @@ -use std::sync::Arc; -use std::time::Duration; - -use anyhow::Context; -use anyhow::Result; -use base64::Engine as _; -use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; -use base64::engine::general_purpose::URL_SAFE_NO_PAD; -use chrono::SecondsFormat; -use chrono::Utc; -use codex_features::Feature; -use codex_login::AgentIdentityAuthRecord; -use codex_login::AuthManager; -use codex_login::CodexAuth; -use codex_login::default_client::create_client; -use codex_protocol::protocol::SessionSource; -use ed25519_dalek::SigningKey; -use ed25519_dalek::VerifyingKey; -use ed25519_dalek::pkcs8::DecodePrivateKey; -use ed25519_dalek::pkcs8::EncodePrivateKey; -use rand::TryRngCore; -use rand::rngs::OsRng; -use serde::Deserialize; -use serde::Serialize; -use tokio::sync::Semaphore; -use tracing::debug; -use tracing::info; -use tracing::warn; - -use crate::config::Config; - -const AGENT_REGISTRATION_TIMEOUT: Duration = Duration::from_secs(15); -const AGENT_IDENTITY_BISCUIT_TIMEOUT: Duration = Duration::from_secs(15); - -#[derive(Clone)] -pub(crate) struct AgentIdentityManager { - auth_manager: Arc, - chatgpt_base_url: String, - feature_enabled: bool, - abom: AgentBillOfMaterials, - ensure_lock: Arc, -} - -impl std::fmt::Debug for AgentIdentityManager { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("AgentIdentityManager") - .field("chatgpt_base_url", &self.chatgpt_base_url) - .field("feature_enabled", &self.feature_enabled) - .field("abom", &self.abom) - .finish_non_exhaustive() - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub(crate) struct StoredAgentIdentity { - pub(crate) binding_id: String, - pub(crate) chatgpt_account_id: String, - pub(crate) chatgpt_user_id: Option, - pub(crate) agent_runtime_id: String, - pub(crate) private_key_pkcs8_base64: String, - pub(crate) public_key_ssh: String, - pub(crate) registered_at: String, - pub(crate) abom: AgentBillOfMaterials, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub(crate) struct AgentBillOfMaterials { - pub(crate) agent_version: String, - pub(crate) agent_harness_id: String, - pub(crate) running_location: String, -} - -#[derive(Debug, Serialize)] -struct RegisterAgentRequest { - abom: AgentBillOfMaterials, - agent_public_key: String, - capabilities: Vec, -} - -#[derive(Debug, Deserialize)] -struct RegisterAgentResponse { - agent_runtime_id: String, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct AgentIdentityBinding { - binding_id: String, - chatgpt_account_id: String, - chatgpt_user_id: Option, - access_token: String, -} - -struct GeneratedAgentKeyMaterial { - private_key_pkcs8_base64: String, - public_key_ssh: String, -} - -impl AgentIdentityManager { - pub(crate) fn new( - config: &Config, - auth_manager: Arc, - session_source: SessionSource, - ) -> Self { - Self { - auth_manager, - chatgpt_base_url: config.chatgpt_base_url.clone(), - feature_enabled: config.features.enabled(Feature::UseAgentIdentity), - abom: build_abom(session_source), - ensure_lock: Arc::new(Semaphore::new(/*permits*/ 1)), - } - } - - pub(crate) fn is_enabled(&self) -> bool { - self.feature_enabled - } - - pub(crate) async fn ensure_registered_identity(&self) -> Result> { - if !self.feature_enabled { - return Ok(None); - } - - let Some(auth) = self.auth_manager.auth().await else { - debug!("skipping agent identity registration because no auth is available"); - return Ok(None); - }; - - let Some(binding) = - AgentIdentityBinding::from_auth(&auth, self.auth_manager.forced_chatgpt_workspace_id()) - else { - debug!("skipping agent identity registration because ChatGPT auth is unavailable"); - return Ok(None); - }; - - let _guard = self - .ensure_lock - .acquire() - .await - .map_err(|_| anyhow::anyhow!("agent identity ensure semaphore closed"))?; - - if let Some(stored_identity) = self.load_stored_identity(&auth, &binding)? { - info!( - agent_runtime_id = %stored_identity.agent_runtime_id, - binding_id = %binding.binding_id, - "reusing stored agent identity" - ); - return Ok(Some(stored_identity)); - } - - let stored_identity = self.register_agent_identity(&binding).await?; - self.store_identity(&auth, &stored_identity)?; - Ok(Some(stored_identity)) - } - - async fn register_agent_identity( - &self, - binding: &AgentIdentityBinding, - ) -> Result { - let key_material = generate_agent_key_material()?; - let request_body = RegisterAgentRequest { - abom: self.abom.clone(), - agent_public_key: key_material.public_key_ssh.clone(), - capabilities: Vec::new(), - }; - - let url = agent_registration_url(&self.chatgpt_base_url); - let human_biscuit = self.mint_human_biscuit(binding, "POST", &url).await?; - let client = create_client(); - let response = client - .post(&url) - .header("X-OpenAI-Authorization", human_biscuit) - .json(&request_body) - .timeout(AGENT_REGISTRATION_TIMEOUT) - .send() - .await - .with_context(|| { - format!("failed to send agent identity registration request to {url}") - })?; - - if response.status().is_success() { - let response_body = response - .json::() - .await - .with_context(|| format!("failed to parse agent identity response from {url}"))?; - let stored_identity = StoredAgentIdentity { - binding_id: binding.binding_id.clone(), - chatgpt_account_id: binding.chatgpt_account_id.clone(), - chatgpt_user_id: binding.chatgpt_user_id.clone(), - agent_runtime_id: response_body.agent_runtime_id, - private_key_pkcs8_base64: key_material.private_key_pkcs8_base64, - public_key_ssh: key_material.public_key_ssh, - registered_at: Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true), - abom: self.abom.clone(), - }; - info!( - agent_runtime_id = %stored_identity.agent_runtime_id, - binding_id = %binding.binding_id, - "registered agent identity" - ); - return Ok(stored_identity); - } - - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - anyhow::bail!("agent identity registration failed with status {status} from {url}: {body}") - } - - async fn mint_human_biscuit( - &self, - binding: &AgentIdentityBinding, - target_method: &str, - target_url: &str, - ) -> Result { - let url = agent_identity_biscuit_url(&self.chatgpt_base_url); - let request_id = agent_identity_request_id()?; - let client = create_client(); - let response = client - .get(&url) - .bearer_auth(&binding.access_token) - .header("X-Request-Id", request_id.clone()) - .header("X-Original-Method", target_method) - .header("X-Original-Url", target_url) - .timeout(AGENT_IDENTITY_BISCUIT_TIMEOUT) - .send() - .await - .with_context(|| format!("failed to send agent identity biscuit request to {url}"))?; - - if response.status().is_success() { - let human_biscuit = response - .headers() - .get("x-openai-authorization") - .context("agent identity biscuit response did not include x-openai-authorization")? - .to_str() - .context("agent identity biscuit response header was not valid UTF-8")? - .to_string(); - info!( - request_id = %request_id, - "minted human biscuit for agent identity registration" - ); - return Ok(human_biscuit); - } - - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - anyhow::bail!( - "agent identity biscuit minting failed with status {status} from {url}: {body}" - ) - } - - fn load_stored_identity( - &self, - auth: &CodexAuth, - binding: &AgentIdentityBinding, - ) -> Result> { - let Some(record) = auth.get_agent_identity(&binding.chatgpt_account_id) else { - return Ok(None); - }; - - let stored_identity = - match StoredAgentIdentity::from_auth_record(binding, record, self.abom.clone()) { - Ok(stored_identity) => stored_identity, - Err(error) => { - warn!( - binding_id = %binding.binding_id, - error = %error, - "stored agent identity is invalid; deleting cached value" - ); - auth.remove_agent_identity()?; - return Ok(None); - } - }; - - if !stored_identity.matches_binding(binding) { - warn!( - binding_id = %binding.binding_id, - "stored agent identity binding no longer matches current auth; deleting cached value" - ); - auth.remove_agent_identity()?; - return Ok(None); - } - - if let Err(error) = stored_identity.validate_key_material() { - warn!( - agent_runtime_id = %stored_identity.agent_runtime_id, - binding_id = %binding.binding_id, - error = %error, - "stored agent identity key material is invalid; deleting cached value" - ); - auth.remove_agent_identity()?; - return Ok(None); - } - - Ok(Some(stored_identity)) - } - - fn store_identity( - &self, - auth: &CodexAuth, - stored_identity: &StoredAgentIdentity, - ) -> Result<()> { - auth.set_agent_identity(stored_identity.to_auth_record())?; - Ok(()) - } - - #[cfg(test)] - fn new_for_tests( - auth_manager: Arc, - feature_enabled: bool, - chatgpt_base_url: String, - session_source: SessionSource, - ) -> Self { - Self { - auth_manager, - chatgpt_base_url, - feature_enabled, - abom: build_abom(session_source), - ensure_lock: Arc::new(Semaphore::new(/*permits*/ 1)), - } - } -} - -impl StoredAgentIdentity { - fn from_auth_record( - binding: &AgentIdentityBinding, - record: AgentIdentityAuthRecord, - abom: AgentBillOfMaterials, - ) -> Result { - if record.workspace_id != binding.chatgpt_account_id { - anyhow::bail!( - "stored agent identity workspace {:?} does not match current workspace {:?}", - record.workspace_id, - binding.chatgpt_account_id - ); - } - let signing_key = signing_key_from_private_key_pkcs8_base64(&record.agent_private_key)?; - Ok(Self { - binding_id: binding.binding_id.clone(), - chatgpt_account_id: binding.chatgpt_account_id.clone(), - chatgpt_user_id: record.chatgpt_user_id, - agent_runtime_id: record.agent_runtime_id, - private_key_pkcs8_base64: record.agent_private_key, - public_key_ssh: encode_ssh_ed25519_public_key(&signing_key.verifying_key()), - registered_at: record.registered_at, - abom, - }) - } - - fn to_auth_record(&self) -> AgentIdentityAuthRecord { - AgentIdentityAuthRecord { - workspace_id: self.chatgpt_account_id.clone(), - chatgpt_user_id: self.chatgpt_user_id.clone(), - agent_runtime_id: self.agent_runtime_id.clone(), - agent_private_key: self.private_key_pkcs8_base64.clone(), - registered_at: self.registered_at.clone(), - } - } - - fn matches_binding(&self, binding: &AgentIdentityBinding) -> bool { - self.binding_id == binding.binding_id - && self.chatgpt_account_id == binding.chatgpt_account_id - && match binding.chatgpt_user_id.as_deref() { - Some(chatgpt_user_id) => self.chatgpt_user_id.as_deref() == Some(chatgpt_user_id), - None => true, - } - } - - fn validate_key_material(&self) -> Result<()> { - let signing_key = self.signing_key()?; - let derived_public_key = encode_ssh_ed25519_public_key(&signing_key.verifying_key()); - anyhow::ensure!( - self.public_key_ssh == derived_public_key, - "stored public key does not match the private key" - ); - Ok(()) - } - - pub(crate) fn signing_key(&self) -> Result { - signing_key_from_private_key_pkcs8_base64(&self.private_key_pkcs8_base64) - } -} - -impl AgentIdentityBinding { - fn from_auth(auth: &CodexAuth, forced_workspace_id: Option) -> Option { - if !auth.is_chatgpt_auth() { - return None; - } - - let token_data = auth.get_token_data().ok()?; - let resolved_account_id = - forced_workspace_id - .filter(|value| !value.is_empty()) - .or(token_data - .account_id - .clone() - .filter(|value| !value.is_empty()))?; - - Some(Self { - binding_id: format!("chatgpt-account-{resolved_account_id}"), - chatgpt_account_id: resolved_account_id, - chatgpt_user_id: token_data - .id_token - .chatgpt_user_id - .filter(|value| !value.is_empty()), - access_token: token_data.access_token, - }) - } -} - -fn build_abom(session_source: SessionSource) -> AgentBillOfMaterials { - AgentBillOfMaterials { - agent_version: env!("CARGO_PKG_VERSION").to_string(), - agent_harness_id: match &session_source { - SessionSource::VSCode => "codex-app".to_string(), - SessionSource::Cli - | SessionSource::Exec - | SessionSource::Mcp - | SessionSource::Custom(_) - | SessionSource::SubAgent(_) - | SessionSource::Unknown => "codex-cli".to_string(), - }, - running_location: format!("{}-{}", session_source, std::env::consts::OS), - } -} - -fn generate_agent_key_material() -> Result { - let mut secret_key_bytes = [0u8; 32]; - OsRng - .try_fill_bytes(&mut secret_key_bytes) - .context("failed to generate agent identity private key bytes")?; - let signing_key = SigningKey::from_bytes(&secret_key_bytes); - let private_key_pkcs8 = signing_key - .to_pkcs8_der() - .context("failed to encode agent identity private key as PKCS#8")?; - - Ok(GeneratedAgentKeyMaterial { - private_key_pkcs8_base64: BASE64_STANDARD.encode(private_key_pkcs8.as_bytes()), - public_key_ssh: encode_ssh_ed25519_public_key(&signing_key.verifying_key()), - }) -} - -fn encode_ssh_ed25519_public_key(verifying_key: &VerifyingKey) -> String { - let mut blob = Vec::with_capacity(4 + 11 + 4 + 32); - append_ssh_string(&mut blob, b"ssh-ed25519"); - append_ssh_string(&mut blob, verifying_key.as_bytes()); - format!("ssh-ed25519 {}", BASE64_STANDARD.encode(blob)) -} - -fn append_ssh_string(buf: &mut Vec, value: &[u8]) { - buf.extend_from_slice(&(value.len() as u32).to_be_bytes()); - buf.extend_from_slice(value); -} - -fn agent_registration_url(chatgpt_base_url: &str) -> String { - let trimmed = chatgpt_base_url.trim_end_matches('/'); - format!("{trimmed}/v1/agent/register") -} - -fn signing_key_from_private_key_pkcs8_base64(private_key_pkcs8_base64: &str) -> Result { - let private_key = BASE64_STANDARD - .decode(private_key_pkcs8_base64) - .context("stored agent identity private key is not valid base64")?; - SigningKey::from_pkcs8_der(&private_key) - .context("stored agent identity private key is not valid PKCS#8") -} - -fn agent_identity_biscuit_url(chatgpt_base_url: &str) -> String { - let trimmed = chatgpt_base_url.trim_end_matches('/'); - format!("{trimmed}/authenticate_app_v2") -} - -fn agent_identity_request_id() -> Result { - let mut request_id_bytes = [0u8; 16]; - OsRng - .try_fill_bytes(&mut request_id_bytes) - .context("failed to generate agent identity request id")?; - Ok(format!( - "codex-agent-identity-{}", - URL_SAFE_NO_PAD.encode(request_id_bytes) - )) -} - -#[cfg(test)] -mod tests { - use super::*; - - use base64::engine::general_purpose::URL_SAFE_NO_PAD; - use codex_app_server_protocol::AuthMode as ApiAuthMode; - use codex_login::AuthCredentialsStoreMode; - use codex_login::AuthDotJson; - use codex_login::save_auth; - use codex_login::token_data::IdTokenInfo; - use codex_login::token_data::TokenData; - use pretty_assertions::assert_eq; - use wiremock::Mock; - use wiremock::MockServer; - use wiremock::ResponseTemplate; - use wiremock::matchers::header; - use wiremock::matchers::method; - use wiremock::matchers::path; - - #[tokio::test] - async fn ensure_registered_identity_skips_when_feature_is_disabled() { - let auth_manager = - AuthManager::from_auth_for_testing(make_chatgpt_auth("account-123", Some("user-123"))); - let manager = AgentIdentityManager::new_for_tests( - auth_manager, - /*feature_enabled*/ false, - "https://chatgpt.com/backend-api/".to_string(), - SessionSource::Cli, - ); - - assert_eq!(manager.ensure_registered_identity().await.unwrap(), None); - } - - #[tokio::test] - async fn ensure_registered_identity_skips_for_api_key_auth() { - let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test-key")); - let manager = AgentIdentityManager::new_for_tests( - auth_manager, - /*feature_enabled*/ true, - "https://chatgpt.com/backend-api/".to_string(), - SessionSource::Cli, - ); - - assert_eq!(manager.ensure_registered_identity().await.unwrap(), None); - } - - #[tokio::test] - async fn ensure_registered_identity_registers_and_reuses_cached_identity() { - let server = MockServer::start().await; - let chatgpt_base_url = server.uri(); - mount_human_biscuit(&server, &chatgpt_base_url).await; - Mock::given(method("POST")) - .and(path("/v1/agent/register")) - .and(header("x-openai-authorization", "human-biscuit")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "agent_runtime_id": "agent_123", - }))) - .expect(1) - .mount(&server) - .await; - - let auth_manager = - AuthManager::from_auth_for_testing(make_chatgpt_auth("account-123", Some("user-123"))); - let manager = AgentIdentityManager::new_for_tests( - auth_manager, - /*feature_enabled*/ true, - chatgpt_base_url, - SessionSource::Cli, - ); - - let first = manager - .ensure_registered_identity() - .await - .unwrap() - .expect("identity should be registered"); - let second = manager - .ensure_registered_identity() - .await - .unwrap() - .expect("identity should be reused"); - - assert_eq!(first.agent_runtime_id, "agent_123"); - assert_eq!(first, second); - assert_eq!(first.abom.agent_harness_id, "codex-cli"); - assert_eq!(first.chatgpt_account_id, "account-123"); - assert_eq!(first.chatgpt_user_id.as_deref(), Some("user-123")); - } - - #[tokio::test] - async fn ensure_registered_identity_deletes_invalid_cached_identity_and_reregisters() { - let server = MockServer::start().await; - let chatgpt_base_url = server.uri(); - mount_human_biscuit(&server, &chatgpt_base_url).await; - Mock::given(method("POST")) - .and(path("/v1/agent/register")) - .and(header("x-openai-authorization", "human-biscuit")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "agent_runtime_id": "agent_456", - }))) - .expect(1) - .mount(&server) - .await; - - let auth = make_chatgpt_auth("account-123", Some("user-123")); - let auth_manager = AuthManager::from_auth_for_testing(auth.clone()); - let manager = AgentIdentityManager::new_for_tests( - auth_manager, - /*feature_enabled*/ true, - chatgpt_base_url, - SessionSource::Cli, - ); - - let binding = - AgentIdentityBinding::from_auth(&auth, /*forced_workspace_id*/ None).expect("binding"); - auth.set_agent_identity(AgentIdentityAuthRecord { - workspace_id: "account-123".to_string(), - chatgpt_user_id: Some("user-123".to_string()), - agent_runtime_id: "agent_invalid".to_string(), - agent_private_key: "not-valid-base64".to_string(), - registered_at: "2026-01-01T00:00:00Z".to_string(), - }) - .expect("seed invalid identity"); - - let stored = manager - .ensure_registered_identity() - .await - .unwrap() - .expect("identity should be registered"); - - assert_eq!(stored.agent_runtime_id, "agent_456"); - let persisted = auth - .get_agent_identity(&binding.chatgpt_account_id) - .expect("stored identity"); - assert_eq!(persisted.agent_runtime_id, "agent_456"); - } - - #[tokio::test] - async fn ensure_registered_identity_deletes_different_user_identity_and_reregisters() { - let server = MockServer::start().await; - let chatgpt_base_url = server.uri(); - mount_human_biscuit(&server, &chatgpt_base_url).await; - Mock::given(method("POST")) - .and(path("/v1/agent/register")) - .and(header("x-openai-authorization", "human-biscuit")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "agent_runtime_id": "agent_new", - }))) - .expect(1) - .mount(&server) - .await; - - let auth = make_chatgpt_auth("account-123", Some("user-new")); - let stale_key = generate_agent_key_material().expect("key material"); - auth.set_agent_identity(AgentIdentityAuthRecord { - workspace_id: "account-123".to_string(), - chatgpt_user_id: Some("user-old".to_string()), - agent_runtime_id: "agent_old".to_string(), - agent_private_key: stale_key.private_key_pkcs8_base64, - registered_at: "2026-01-01T00:00:00Z".to_string(), - }) - .expect("seed stale identity"); - - let auth_manager = AuthManager::from_auth_for_testing(auth.clone()); - let manager = AgentIdentityManager::new_for_tests( - auth_manager, - /*feature_enabled*/ true, - chatgpt_base_url, - SessionSource::Cli, - ); - - let stored = manager - .ensure_registered_identity() - .await - .unwrap() - .expect("identity should be registered"); - - assert_eq!(stored.agent_runtime_id, "agent_new"); - assert_eq!(stored.chatgpt_user_id.as_deref(), Some("user-new")); - let persisted = auth - .get_agent_identity("account-123") - .expect("stored identity"); - assert_eq!(persisted.agent_runtime_id, "agent_new"); - assert_eq!(persisted.chatgpt_user_id.as_deref(), Some("user-new")); - } - - #[tokio::test] - async fn ensure_registered_identity_uses_chatgpt_base_url() { - let server = MockServer::start().await; - let chatgpt_base_url = format!("{}/backend-api", server.uri()); - mount_human_biscuit(&server, &chatgpt_base_url).await; - Mock::given(method("POST")) - .and(path("/backend-api/v1/agent/register")) - .and(header("x-openai-authorization", "human-biscuit")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "agent_runtime_id": "agent_canonical", - }))) - .expect(1) - .mount(&server) - .await; - - let auth_manager = - AuthManager::from_auth_for_testing(make_chatgpt_auth("account-123", Some("user-123"))); - let manager = AgentIdentityManager::new_for_tests( - auth_manager, - /*feature_enabled*/ true, - chatgpt_base_url, - SessionSource::Cli, - ); - - let stored = manager - .ensure_registered_identity() - .await - .unwrap() - .expect("identity should be registered"); - assert_eq!(stored.agent_runtime_id, "agent_canonical"); - } - - async fn mount_human_biscuit(server: &MockServer, chatgpt_base_url: &str) { - let biscuit_url = agent_identity_biscuit_url(chatgpt_base_url); - let biscuit_path = reqwest::Url::parse(&biscuit_url) - .expect("biscuit URL parses") - .path() - .to_string(); - let target_url = agent_registration_url(chatgpt_base_url); - Mock::given(method("GET")) - .and(path(biscuit_path)) - .and(header("authorization", "Bearer access-token-account-123")) - .and(header("x-original-method", "POST")) - .and(header("x-original-url", target_url)) - .respond_with( - ResponseTemplate::new(200).insert_header("x-openai-authorization", "human-biscuit"), - ) - .expect(1) - .mount(server) - .await; - } - - #[test] - fn encode_ssh_ed25519_public_key_matches_expected_wire_shape() { - let key_material = generate_agent_key_material().expect("key material"); - let (_, encoded_blob) = key_material - .public_key_ssh - .split_once(' ') - .expect("public key contains scheme"); - let decoded = BASE64_STANDARD.decode(encoded_blob).expect("base64"); - - assert_eq!(&decoded[..4], 11u32.to_be_bytes().as_slice()); - assert_eq!(&decoded[4..15], b"ssh-ed25519"); - assert_eq!(&decoded[15..19], 32u32.to_be_bytes().as_slice()); - assert_eq!(decoded.len(), 51); - } - - fn make_chatgpt_auth(account_id: &str, user_id: Option<&str>) -> CodexAuth { - let tempdir = tempfile::tempdir().expect("tempdir"); - let auth_json = AuthDotJson { - auth_mode: Some(ApiAuthMode::Chatgpt), - openai_api_key: None, - tokens: Some(TokenData { - id_token: IdTokenInfo { - email: None, - chatgpt_plan_type: None, - chatgpt_user_id: user_id.map(ToOwned::to_owned), - chatgpt_account_id: Some(account_id.to_string()), - chatgpt_account_is_fedramp: false, - raw_jwt: fake_id_token(account_id, user_id), - }, - access_token: format!("access-token-{account_id}"), - refresh_token: "refresh-token".to_string(), - account_id: Some(account_id.to_string()), - }), - last_refresh: Some(Utc::now()), - agent_identity: None, - }; - save_auth(tempdir.path(), &auth_json, AuthCredentialsStoreMode::File).expect("save auth"); - CodexAuth::from_auth_storage(tempdir.path(), AuthCredentialsStoreMode::File) - .expect("load auth") - .expect("auth") - } - - fn fake_id_token(account_id: &str, user_id: Option<&str>) -> String { - let header = URL_SAFE_NO_PAD.encode(r#"{"alg":"none","typ":"JWT"}"#); - let payload = serde_json::json!({ - "https://api.openai.com/auth": { - "chatgpt_user_id": user_id, - "chatgpt_account_id": account_id, - } - }); - let payload = URL_SAFE_NO_PAD.encode(payload.to_string()); - format!("{header}.{payload}.signature") - } -} diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 1809789a8e..14ee38e6b7 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -5,7 +5,6 @@ // the TUI or the tracing stack). #![deny(clippy::print_stdout, clippy::print_stderr)] -mod agent_identity; mod apply_patch; mod apps; mod arc_monitor; diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 1aeec3a9ab..8c7f7174d2 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -128,7 +128,6 @@ use std::path::Path; use std::time::Duration; use tokio::sync::Semaphore; use tokio::time::sleep; -use tokio::time::timeout; use tracing_opentelemetry::OpenTelemetrySpanExt; use codex_protocol::mcp::CallToolResult as McpCallToolResult; @@ -3072,11 +3071,6 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { }), rollout: Mutex::new(None), user_shell: Arc::new(default_user_shell()), - agent_identity_manager: Arc::new(crate::agent_identity::AgentIdentityManager::new( - config.as_ref(), - Arc::clone(&auth_manager), - session_configuration.session_source.clone(), - )), shell_snapshot_tx: watch::channel(None).0, show_raw_agent_reasoning: config.show_raw_agent_reasoning, exec_policy, @@ -4035,11 +4029,6 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( }), rollout: Mutex::new(None), user_shell: Arc::new(default_user_shell()), - agent_identity_manager: Arc::new(crate::agent_identity::AgentIdentityManager::new( - config.as_ref(), - Arc::clone(&auth_manager), - session_configuration.session_source.clone(), - )), shell_snapshot_tx: watch::channel(None).0, show_raw_agent_reasoning: config.show_raw_agent_reasoning, exec_policy, @@ -4147,42 +4136,6 @@ pub(crate) async fn make_session_and_context_with_rx() -> ( make_session_and_context_with_dynamic_tools_and_rx(Vec::new()).await } -#[tokio::test] -async fn fail_agent_identity_registration_emits_error_and_shutdown() { - let (session, _turn_context, rx_event) = make_session_and_context_with_rx().await; - - session - .fail_agent_identity_registration(anyhow::anyhow!("registration exploded")) - .await; - - let error_event = timeout(Duration::from_secs(1), rx_event.recv()) - .await - .expect("error event should arrive") - .expect("error event should be readable"); - match error_event.msg { - EventMsg::Error(ErrorEvent { - message, - codex_error_info, - }) => { - assert_eq!( - message, - "Agent identity registration failed. Codex cannot continue while `features.use_agent_identity` is enabled: registration exploded".to_string() - ); - assert_eq!(codex_error_info, Some(CodexErrorInfo::Other)); - } - other => panic!("expected error event, got {other:?}"), - } - - let shutdown_event = timeout(Duration::from_secs(1), rx_event.recv()) - .await - .expect("shutdown event should arrive") - .expect("shutdown event should be readable"); - match shutdown_event.msg { - EventMsg::ShutdownComplete => {} - other => panic!("expected shutdown event, got {other:?}"), - } -} - #[tokio::test] async fn refresh_mcp_servers_is_deferred_until_next_turn() { let (session, turn_context) = make_session_and_context().await; diff --git a/codex-rs/core/src/state/service.rs b/codex-rs/core/src/state/service.rs index 5db38f7b72..aae02d61bd 100644 --- a/codex-rs/core/src/state/service.rs +++ b/codex-rs/core/src/state/service.rs @@ -4,7 +4,6 @@ use std::sync::Arc; use crate::RolloutRecorder; use crate::SkillsManager; use crate::agent::AgentControl; -use crate::agent_identity::AgentIdentityManager; use crate::client::ModelClient; use crate::config::StartedNetworkProxy; use crate::exec_policy::ExecPolicyManager; @@ -43,7 +42,6 @@ pub(crate) struct SessionServices { pub(crate) hooks: Hooks, pub(crate) rollout: Mutex>, pub(crate) user_shell: Arc, - pub(crate) agent_identity_manager: Arc, pub(crate) shell_snapshot_tx: watch::Sender>>, pub(crate) show_raw_agent_reasoning: bool, pub(crate) exec_policy: Arc, diff --git a/codex-rs/login/src/auth/auth_tests.rs b/codex-rs/login/src/auth/auth_tests.rs index fed4973070..c281509b3f 100644 --- a/codex-rs/login/src/auth/auth_tests.rs +++ b/codex-rs/login/src/auth/auth_tests.rs @@ -16,8 +16,6 @@ use serde_json::json; use std::sync::Arc; use tempfile::TempDir; use tempfile::tempdir; -use tokio::time::Duration; -use tokio::time::timeout; #[tokio::test] async fn refresh_without_id_token() { @@ -138,7 +136,6 @@ async fn pro_account_with_no_api_key_uses_chatgpt_auth() { account_id: None, }), last_refresh: Some(last_refresh), - agent_identity: None, }, auth_dot_json ); @@ -176,7 +173,6 @@ fn logout_removes_auth_file() -> Result<(), std::io::Error> { openai_api_key: Some("sk-test-key".to_string()), tokens: None, last_refresh: None, - agent_identity: None, }; super::save_auth(dir.path(), &auth_dot_json, AuthCredentialsStoreMode::File)?; let auth_file = get_auth_file(dir.path()); @@ -186,78 +182,6 @@ fn logout_removes_auth_file() -> Result<(), std::io::Error> { Ok(()) } -#[test] -fn chatgpt_auth_persists_agent_identity_for_workspace() { - let codex_home = tempdir().unwrap(); - write_auth_file( - AuthFileParams { - openai_api_key: None, - chatgpt_plan_type: Some("pro".to_string()), - chatgpt_account_id: Some("account-123".to_string()), - }, - codex_home.path(), - ) - .expect("failed to write auth file"); - let auth = super::load_auth( - codex_home.path(), - /*enable_codex_api_key_env*/ false, - AuthCredentialsStoreMode::File, - ) - .expect("load auth") - .expect("auth available"); - let record = AgentIdentityAuthRecord { - workspace_id: "account-123".to_string(), - chatgpt_user_id: Some("user-123".to_string()), - agent_runtime_id: "agent_123".to_string(), - agent_private_key: "pkcs8-base64".to_string(), - registered_at: "2026-04-13T12:00:00Z".to_string(), - }; - - auth.set_agent_identity(record.clone()) - .expect("set agent identity"); - - assert_eq!(auth.get_agent_identity("account-123"), Some(record.clone())); - assert_eq!(auth.get_agent_identity("other-account"), None); - let storage = FileAuthStorage::new(codex_home.path().to_path_buf()); - let persisted = storage - .load() - .expect("load auth") - .expect("auth should exist"); - assert_eq!(persisted.agent_identity, Some(record)); - - assert!(auth.remove_agent_identity().expect("remove agent identity")); - assert_eq!(auth.get_agent_identity("account-123"), None); -} - -#[test] -fn dummy_chatgpt_auth_does_not_create_cwd_auth_json_when_identity_is_set() { - let cwd_auth = std::env::current_dir() - .expect("current dir") - .join("auth.json"); - let had_auth_json_before_test = cwd_auth.exists(); - let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); - let record = AgentIdentityAuthRecord { - workspace_id: "account_id".to_string(), - chatgpt_user_id: None, - agent_runtime_id: "agent_123".to_string(), - agent_private_key: "pkcs8-base64".to_string(), - registered_at: "2026-04-13T12:00:00Z".to_string(), - background_task_id: None, - }; - - auth.set_agent_identity(record.clone()) - .expect("set agent identity"); - - assert_eq!(auth.get_agent_identity("account_id"), Some(record)); - if !had_auth_json_before_test { - assert!( - !cwd_auth.exists(), - "dummy ChatGPT auth must not write auth.json in the test process cwd" - ); - } -} - -#[test] fn unauthorized_recovery_reports_mode_and_step_names() { let dir = tempdir().unwrap(); let manager = AuthManager::shared( @@ -550,67 +474,6 @@ exit 1 } } -#[tokio::test] -async fn auth_manager_notifies_when_auth_state_changes() { - let dir = tempdir().unwrap(); - let manager = AuthManager::shared( - dir.path().to_path_buf(), - /*enable_codex_api_key_env*/ false, - AuthCredentialsStoreMode::File, - ); - let mut auth_state_rx = manager.subscribe_auth_state(); - - save_auth( - dir.path(), - &AuthDotJson { - auth_mode: Some(ApiAuthMode::ApiKey), - openai_api_key: Some("sk-test-key".to_string()), - tokens: None, - last_refresh: None, - agent_identity: None, - }, - AuthCredentialsStoreMode::File, - ) - .expect("save auth"); - - assert!( - manager.reload(), - "reload should report a changed auth state" - ); - timeout(Duration::from_secs(1), auth_state_rx.changed()) - .await - .expect("auth change notification should arrive") - .expect("auth state watch should remain open"); - - save_auth( - dir.path(), - &AuthDotJson { - auth_mode: Some(ApiAuthMode::ApiKey), - openai_api_key: Some("sk-updated-key".to_string()), - tokens: None, - last_refresh: None, - agent_identity: None, - }, - AuthCredentialsStoreMode::File, - ) - .expect("save updated auth"); - - assert!( - !manager.reload(), - "reload remains mode-stable even when the underlying credentials change" - ); - timeout(Duration::from_secs(1), auth_state_rx.changed()) - .await - .expect("auth reload notification should still arrive") - .expect("auth state watch should remain open"); - - manager.set_forced_chatgpt_workspace_id(Some("workspace-123".to_string())); - timeout(Duration::from_secs(1), auth_state_rx.changed()) - .await - .expect("workspace change notification should arrive") - .expect("auth state watch should remain open"); -} - struct AuthFileParams { openai_api_key: Option, chatgpt_plan_type: Option, diff --git a/codex-rs/login/src/auth/manager.rs b/codex-rs/login/src/auth/manager.rs index c7928fa462..29aa7fe350 100644 --- a/codex-rs/login/src/auth/manager.rs +++ b/codex-rs/login/src/auth/manager.rs @@ -15,7 +15,6 @@ use std::sync::RwLock; use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering; use tokio::sync::Mutex as AsyncMutex; -use tokio::sync::watch; use codex_app_server_protocol::AuthMode; use codex_app_server_protocol::AuthMode as ApiAuthMode; @@ -24,7 +23,6 @@ use codex_protocol::config_types::ModelProviderAuthInfo; use super::external_bearer::BearerTokenRefresher; use super::revoke::revoke_auth_tokens; -pub use crate::auth::storage::AgentIdentityAuthRecord; pub use crate::auth::storage::AuthDotJson; use crate::auth::storage::AuthStorageBackend; use crate::auth::storage::create_auth_storage; @@ -65,7 +63,6 @@ pub struct ChatgptAuth { #[derive(Debug, Clone)] pub struct ChatgptAuthTokens { state: ChatgptAuthState, - storage: Arc, } #[derive(Debug, Clone)] @@ -213,13 +210,14 @@ impl CodexAuth { client, }; - let storage = create_auth_storage(codex_home.to_path_buf(), storage_mode); match auth_mode { - ApiAuthMode::Chatgpt => Ok(Self::Chatgpt(ChatgptAuth { state, storage })), - ApiAuthMode::ChatgptAuthTokens => Ok(Self::ChatgptAuthTokens(ChatgptAuthTokens { - state, - storage, - })), + ApiAuthMode::Chatgpt => { + let storage = create_auth_storage(codex_home.to_path_buf(), storage_mode); + Ok(Self::Chatgpt(ChatgptAuth { state, storage })) + } + ApiAuthMode::ChatgptAuthTokens => { + Ok(Self::ChatgptAuthTokens(ChatgptAuthTokens { state })) + } ApiAuthMode::ApiKey => unreachable!("api key mode is handled above"), } } @@ -366,52 +364,6 @@ impl CodexAuth { self.get_current_auth_json().and_then(|t| t.tokens) } - pub fn get_agent_identity(&self, workspace_id: &str) -> Option { - self.get_current_auth_json() - .and_then(|auth| auth.agent_identity) - .filter(|identity| identity.workspace_id == workspace_id) - } - - pub fn set_agent_identity(&self, record: AgentIdentityAuthRecord) -> std::io::Result<()> { - let (state, storage) = match self { - Self::Chatgpt(auth) => (&auth.state, &auth.storage), - Self::ChatgptAuthTokens(auth) => (&auth.state, &auth.storage), - Self::ApiKey(_) => return Ok(()), - }; - let mut guard = state - .auth_dot_json - .lock() - .map_err(|_| std::io::Error::other("failed to lock auth state"))?; - let mut auth = guard - .clone() - .ok_or_else(|| std::io::Error::other("auth data is not available"))?; - auth.agent_identity = Some(record); - storage.save(&auth)?; - *guard = Some(auth); - Ok(()) - } - - pub fn remove_agent_identity(&self) -> std::io::Result { - let (state, storage) = match self { - Self::Chatgpt(auth) => (&auth.state, &auth.storage), - Self::ChatgptAuthTokens(auth) => (&auth.state, &auth.storage), - Self::ApiKey(_) => return Ok(false), - }; - let mut guard = state - .auth_dot_json - .lock() - .map_err(|_| std::io::Error::other("failed to lock auth state"))?; - let Some(mut auth) = guard.clone() else { - return Ok(false); - }; - let removed = auth.agent_identity.take().is_some(); - if removed { - storage.save(&auth)?; - *guard = Some(auth); - } - Ok(removed) - } - /// Consider this private to integration tests. pub fn create_dummy_chatgpt_auth_for_testing() -> Self { let auth_dot_json = AuthDotJson { @@ -424,7 +376,6 @@ impl CodexAuth { account_id: Some("account_id".to_string()), }), last_refresh: Some(Utc::now()), - agent_identity: None, }; let client = create_client(); @@ -517,7 +468,6 @@ pub fn login_with_api_key( openai_api_key: Some(api_key.to_string()), tokens: None, last_refresh: None, - agent_identity: None, }; save_auth(codex_home, &auth_dot_json, auth_credentials_store_mode) } @@ -891,7 +841,6 @@ impl AuthDotJson { openai_api_key: None, tokens: Some(tokens), last_refresh: Some(Utc::now()), - agent_identity: None, }) } @@ -1187,7 +1136,6 @@ pub struct AuthManager { forced_chatgpt_workspace_id: RwLock>, refresh_lock: AsyncMutex<()>, external_auth: RwLock>>, - auth_state_tx: watch::Sender<()>, } /// Configuration view required to construct a shared [`AuthManager`]. @@ -1236,7 +1184,6 @@ impl AuthManager { enable_codex_api_key_env: bool, auth_credentials_store_mode: AuthCredentialsStoreMode, ) -> Self { - let (auth_state_tx, _) = watch::channel(()); let managed_auth = load_auth( &codex_home, enable_codex_api_key_env, @@ -1255,13 +1202,11 @@ impl AuthManager { forced_chatgpt_workspace_id: RwLock::new(None), refresh_lock: AsyncMutex::new(()), external_auth: RwLock::new(None), - auth_state_tx, } } /// Create an AuthManager with a specific CodexAuth, for testing only. pub fn from_auth_for_testing(auth: CodexAuth) -> Arc { - let (auth_state_tx, _) = watch::channel(()); let cached = CachedAuth { auth: Some(auth), permanent_refresh_failure: None, @@ -1275,13 +1220,11 @@ impl AuthManager { forced_chatgpt_workspace_id: RwLock::new(None), refresh_lock: AsyncMutex::new(()), external_auth: RwLock::new(None), - auth_state_tx, }) } /// Create an AuthManager with a specific CodexAuth and codex home, for testing only. pub fn from_auth_for_testing_with_home(auth: CodexAuth, codex_home: PathBuf) -> Arc { - let (auth_state_tx, _) = watch::channel(()); let cached = CachedAuth { auth: Some(auth), permanent_refresh_failure: None, @@ -1294,12 +1237,10 @@ impl AuthManager { forced_chatgpt_workspace_id: RwLock::new(None), refresh_lock: AsyncMutex::new(()), external_auth: RwLock::new(None), - auth_state_tx, }) } pub fn external_bearer_only(config: ModelProviderAuthInfo) -> Arc { - let (auth_state_tx, _) = watch::channel(()); Arc::new(Self { codex_home: PathBuf::from("non-existent"), inner: RwLock::new(CachedAuth { @@ -1313,7 +1254,6 @@ impl AuthManager { external_auth: RwLock::new(Some( Arc::new(BearerTokenRefresher::new(config)) as Arc )), - auth_state_tx, }) } @@ -1453,7 +1393,6 @@ impl AuthManager { } tracing::info!("Reloaded auth, changed: {changed}"); guard.auth = new_auth; - self.auth_state_tx.send_replace(()); changed } else { false @@ -1463,23 +1402,18 @@ impl AuthManager { pub fn set_external_auth(&self, external_auth: Arc) { if let Ok(mut guard) = self.external_auth.write() { *guard = Some(external_auth); - self.auth_state_tx.send_replace(()); } } pub fn clear_external_auth(&self) { if let Ok(mut guard) = self.external_auth.write() { *guard = None; - self.auth_state_tx.send_replace(()); } } pub fn set_forced_chatgpt_workspace_id(&self, workspace_id: Option) { - if let Ok(mut guard) = self.forced_chatgpt_workspace_id.write() - && *guard != workspace_id - { + if let Ok(mut guard) = self.forced_chatgpt_workspace_id.write() { *guard = workspace_id; - self.auth_state_tx.send_replace(()); } } @@ -1490,10 +1424,6 @@ impl AuthManager { .and_then(|guard| guard.clone()) } - pub fn subscribe_auth_state(&self) -> watch::Receiver<()> { - self.auth_state_tx.subscribe() - } - pub fn has_external_auth(&self) -> bool { self.external_auth().is_some() } @@ -1760,14 +1690,8 @@ impl AuthManager { ), ))); } - let mut auth_dot_json = + let auth_dot_json = AuthDotJson::from_external_tokens(&refreshed).map_err(RefreshTokenError::Transient)?; - if let Some(previous_auth) = self - .auth_cached() - .and_then(|auth| auth.get_current_auth_json()) - { - auth_dot_json.agent_identity = previous_auth.agent_identity; - } save_auth( &self.codex_home, &auth_dot_json, diff --git a/codex-rs/login/src/auth/storage.rs b/codex-rs/login/src/auth/storage.rs index f0d7a3169c..97e801415c 100644 --- a/codex-rs/login/src/auth/storage.rs +++ b/codex-rs/login/src/auth/storage.rs @@ -39,19 +39,6 @@ pub struct AuthDotJson { #[serde(default, skip_serializing_if = "Option::is_none")] pub last_refresh: Option>, - - #[serde(default, skip_serializing_if = "Option::is_none")] - pub agent_identity: Option, -} - -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] -pub struct AgentIdentityAuthRecord { - pub workspace_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub chatgpt_user_id: Option, - pub agent_runtime_id: String, - pub agent_private_key: String, - pub registered_at: String, } pub(super) fn get_auth_file(codex_home: &Path) -> PathBuf { diff --git a/codex-rs/login/src/auth/storage_tests.rs b/codex-rs/login/src/auth/storage_tests.rs index 2e1cc85029..4bf72c11b9 100644 --- a/codex-rs/login/src/auth/storage_tests.rs +++ b/codex-rs/login/src/auth/storage_tests.rs @@ -18,7 +18,6 @@ async fn file_storage_load_returns_auth_dot_json() -> anyhow::Result<()> { openai_api_key: Some("test-key".to_string()), tokens: None, last_refresh: Some(Utc::now()), - agent_identity: None, }; storage @@ -39,7 +38,6 @@ async fn file_storage_save_persists_auth_dot_json() -> anyhow::Result<()> { openai_api_key: Some("test-key".to_string()), tokens: None, last_refresh: Some(Utc::now()), - agent_identity: None, }; let file = get_auth_file(codex_home.path()); @@ -54,30 +52,6 @@ async fn file_storage_save_persists_auth_dot_json() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] -async fn file_storage_persists_agent_identity() -> anyhow::Result<()> { - let codex_home = tempdir()?; - let storage = FileAuthStorage::new(codex_home.path().to_path_buf()); - let auth_dot_json = AuthDotJson { - auth_mode: Some(AuthMode::Chatgpt), - openai_api_key: None, - tokens: None, - last_refresh: Some(Utc::now()), - agent_identity: Some(AgentIdentityAuthRecord { - workspace_id: "account-123".to_string(), - chatgpt_user_id: Some("user-123".to_string()), - agent_runtime_id: "agent_123".to_string(), - agent_private_key: "pkcs8-base64".to_string(), - registered_at: "2026-04-13T12:00:00Z".to_string(), - }), - }; - - storage.save(&auth_dot_json)?; - - assert_eq!(storage.load()?, Some(auth_dot_json)); - Ok(()) -} - #[test] fn file_storage_delete_removes_auth_file() -> anyhow::Result<()> { let dir = tempdir()?; @@ -86,7 +60,6 @@ fn file_storage_delete_removes_auth_file() -> anyhow::Result<()> { openai_api_key: Some("sk-test-key".to_string()), tokens: None, last_refresh: None, - agent_identity: None, }; let storage = create_auth_storage(dir.path().to_path_buf(), AuthCredentialsStoreMode::File); storage.save(&auth_dot_json)?; @@ -110,7 +83,6 @@ fn ephemeral_storage_save_load_delete_is_in_memory_only() -> anyhow::Result<()> openai_api_key: Some("sk-ephemeral".to_string()), tokens: None, last_refresh: Some(Utc::now()), - agent_identity: None, }; storage.save(&auth_dot_json)?; @@ -209,7 +181,6 @@ fn auth_with_prefix(prefix: &str) -> AuthDotJson { account_id: Some(format!("{prefix}-account-id")), }), last_refresh: None, - agent_identity: None, } } @@ -226,7 +197,6 @@ fn keyring_auth_storage_load_returns_deserialized_auth() -> anyhow::Result<()> { openai_api_key: Some("sk-test".to_string()), tokens: None, last_refresh: None, - agent_identity: None, }; seed_keyring_with_auth( &mock_keyring, @@ -269,7 +239,6 @@ fn keyring_auth_storage_save_persists_and_removes_fallback_file() -> anyhow::Res account_id: Some("account".to_string()), }), last_refresh: Some(Utc::now()), - agent_identity: None, }; storage.save(&auth)?; diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index d819b0946d..d69a77a97d 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -17,7 +17,6 @@ pub use server::ServerOptions; pub use server::ShutdownHandle; pub use server::run_login_server; -pub use auth::AgentIdentityAuthRecord; pub use auth::AuthConfig; pub use auth::AuthDotJson; pub use auth::AuthManager; diff --git a/codex-rs/login/src/server.rs b/codex-rs/login/src/server.rs index 0c7b810184..169a8a3091 100644 --- a/codex-rs/login/src/server.rs +++ b/codex-rs/login/src/server.rs @@ -781,7 +781,6 @@ pub(crate) async fn persist_tokens_async( openai_api_key: api_key, tokens: Some(tokens), last_refresh: Some(Utc::now()), - agent_identity: None, }; save_auth(&codex_home, &auth, auth_credentials_store_mode) }) diff --git a/codex-rs/login/tests/suite/auth_refresh.rs b/codex-rs/login/tests/suite/auth_refresh.rs index d754c9589d..bf9e03bc26 100644 --- a/codex-rs/login/tests/suite/auth_refresh.rs +++ b/codex-rs/login/tests/suite/auth_refresh.rs @@ -54,7 +54,6 @@ async fn refresh_token_succeeds_updates_storage() -> Result<()> { openai_api_key: None, tokens: Some(initial_tokens.clone()), last_refresh: Some(initial_last_refresh), - agent_identity: None, }; ctx.write_auth(&initial_auth)?; @@ -118,7 +117,6 @@ async fn refresh_token_refreshes_when_auth_is_unchanged() -> Result<()> { openai_api_key: None, tokens: Some(initial_tokens.clone()), last_refresh: Some(initial_last_refresh), - agent_identity: None, }; ctx.write_auth(&initial_auth)?; @@ -173,7 +171,6 @@ async fn refresh_token_skips_refresh_when_auth_changed() -> Result<()> { openai_api_key: None, tokens: Some(initial_tokens), last_refresh: Some(initial_last_refresh), - agent_identity: None, }; ctx.write_auth(&initial_auth)?; @@ -183,7 +180,6 @@ async fn refresh_token_skips_refresh_when_auth_changed() -> Result<()> { openai_api_key: None, tokens: Some(disk_tokens.clone()), last_refresh: Some(initial_last_refresh), - agent_identity: None, }; save_auth( ctx.codex_home.path(), @@ -238,7 +234,6 @@ async fn refresh_token_errors_on_account_mismatch() -> Result<()> { openai_api_key: None, tokens: Some(initial_tokens.clone()), last_refresh: Some(initial_last_refresh), - agent_identity: None, }; ctx.write_auth(&initial_auth)?; @@ -249,7 +244,6 @@ async fn refresh_token_errors_on_account_mismatch() -> Result<()> { openai_api_key: None, tokens: Some(disk_tokens), last_refresh: Some(initial_last_refresh), - agent_identity: None, }; save_auth( ctx.codex_home.path(), @@ -308,7 +302,6 @@ async fn returns_fresh_tokens_as_is() -> Result<()> { openai_api_key: None, tokens: Some(initial_tokens.clone()), last_refresh: Some(stale_refresh), - agent_identity: None, }; ctx.write_auth(&initial_auth)?; @@ -356,7 +349,6 @@ async fn refreshes_token_when_access_token_is_expired() -> Result<()> { openai_api_key: None, tokens: Some(initial_tokens.clone()), last_refresh: Some(fresh_refresh), - agent_identity: None, }; ctx.write_auth(&initial_auth)?; @@ -406,7 +398,6 @@ async fn auth_reloads_disk_auth_when_cached_auth_is_stale() -> Result<()> { openai_api_key: None, tokens: Some(initial_tokens), last_refresh: Some(stale_refresh), - agent_identity: None, }; ctx.write_auth(&initial_auth)?; @@ -417,7 +408,6 @@ async fn auth_reloads_disk_auth_when_cached_auth_is_stale() -> Result<()> { openai_api_key: None, tokens: Some(disk_tokens.clone()), last_refresh: Some(fresh_refresh), - agent_identity: None, }; save_auth( ctx.codex_home.path(), @@ -469,7 +459,6 @@ async fn auth_reloads_disk_auth_without_calling_expired_refresh_token() -> Resul openai_api_key: None, tokens: Some(initial_tokens), last_refresh: Some(stale_refresh), - agent_identity: None, }; ctx.write_auth(&initial_auth)?; @@ -480,7 +469,6 @@ async fn auth_reloads_disk_auth_without_calling_expired_refresh_token() -> Resul openai_api_key: None, tokens: Some(disk_tokens.clone()), last_refresh: Some(fresh_refresh), - agent_identity: None, }; save_auth( ctx.codex_home.path(), @@ -530,7 +518,6 @@ async fn refresh_token_returns_permanent_error_for_expired_refresh_token() -> Re openai_api_key: None, tokens: Some(initial_tokens.clone()), last_refresh: Some(initial_last_refresh), - agent_identity: None, }; ctx.write_auth(&initial_auth)?; @@ -583,7 +570,6 @@ async fn refresh_token_does_not_retry_after_permanent_failure() -> Result<()> { openai_api_key: None, tokens: Some(initial_tokens.clone()), last_refresh: Some(initial_last_refresh), - agent_identity: None, }; ctx.write_auth(&initial_auth)?; @@ -650,7 +636,6 @@ async fn refresh_token_reloads_changed_auth_after_permanent_failure() -> Result< openai_api_key: None, tokens: Some(initial_tokens.clone()), last_refresh: Some(initial_last_refresh), - agent_identity: None, }; ctx.write_auth(&initial_auth)?; @@ -672,7 +657,6 @@ async fn refresh_token_reloads_changed_auth_after_permanent_failure() -> Result< openai_api_key: None, tokens: Some(disk_tokens.clone()), last_refresh: Some(fresh_refresh), - agent_identity: None, }; save_auth( ctx.codex_home.path(), @@ -731,7 +715,6 @@ async fn refresh_token_returns_transient_error_on_server_failure() -> Result<()> openai_api_key: None, tokens: Some(initial_tokens.clone()), last_refresh: Some(initial_last_refresh), - agent_identity: None, }; ctx.write_auth(&initial_auth)?; @@ -784,7 +767,6 @@ async fn unauthorized_recovery_reloads_then_refreshes_tokens() -> Result<()> { openai_api_key: None, tokens: Some(initial_tokens.clone()), last_refresh: Some(initial_last_refresh), - agent_identity: None, }; ctx.write_auth(&initial_auth)?; @@ -794,7 +776,6 @@ async fn unauthorized_recovery_reloads_then_refreshes_tokens() -> Result<()> { openai_api_key: None, tokens: Some(disk_tokens.clone()), last_refresh: Some(initial_last_refresh), - agent_identity: None, }; save_auth( ctx.codex_home.path(), @@ -878,7 +859,6 @@ async fn unauthorized_recovery_errors_on_account_mismatch() -> Result<()> { openai_api_key: None, tokens: Some(initial_tokens.clone()), last_refresh: Some(initial_last_refresh), - agent_identity: None, }; ctx.write_auth(&initial_auth)?; @@ -889,7 +869,6 @@ async fn unauthorized_recovery_errors_on_account_mismatch() -> Result<()> { openai_api_key: None, tokens: Some(disk_tokens), last_refresh: Some(initial_last_refresh), - agent_identity: None, }; save_auth( ctx.codex_home.path(), @@ -947,7 +926,6 @@ async fn unauthorized_recovery_requires_chatgpt_auth() -> Result<()> { openai_api_key: Some("sk-test".to_string()), tokens: None, last_refresh: None, - agent_identity: None, }; ctx.write_auth(&auth)?; diff --git a/codex-rs/tui/src/local_chatgpt_auth.rs b/codex-rs/tui/src/local_chatgpt_auth.rs index 1f84b289a7..e888c0387c 100644 --- a/codex-rs/tui/src/local_chatgpt_auth.rs +++ b/codex-rs/tui/src/local_chatgpt_auth.rs @@ -108,7 +108,6 @@ mod tests { account_id: Some("workspace-1".to_string()), }), last_refresh: Some(Utc::now()), - agent_identity: None, }; save_auth(codex_home, &auth, AuthCredentialsStoreMode::File) .expect("chatgpt auth should save"); @@ -155,7 +154,6 @@ mod tests { openai_api_key: Some("sk-test".to_string()), tokens: None, last_refresh: None, - agent_identity: None, }, AuthCredentialsStoreMode::File, )