diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index ef7e175b103..760b27a53b0 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -140,6 +140,7 @@ use codex_app_server_protocol::UserSavedConfig; use codex_app_server_protocol::build_turns_from_event_msgs; use codex_backend_client::Client as BackendClient; use codex_chatgpt::connectors; +use codex_cloud_requirements::cloud_requirements_loader; use codex_core::AuthManager; use codex_core::CodexAuth; use codex_core::CodexThread; @@ -162,6 +163,7 @@ use codex_core::config::edit::ConfigEditsBuilder; use codex_core::config::types::McpServerTransportConfig; use codex_core::config_loader::CloudRequirementsLoader; use codex_core::default_client::get_codex_user_agent; +use codex_core::default_client::set_default_client_residency_requirement; use codex_core::error::CodexErr; use codex_core::exec::ExecParams; use codex_core::exec_env::create_env; @@ -218,6 +220,7 @@ use std::io::Error as IoError; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; +use std::sync::RwLock; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; use std::time::Duration; @@ -277,7 +280,7 @@ pub(crate) struct CodexMessageProcessor { codex_linux_sandbox_exe: Option, config: Arc, cli_overrides: Vec<(String, TomlValue)>, - cloud_requirements: CloudRequirementsLoader, + cloud_requirements: Arc>, conversation_listeners: HashMap>, listener_thread_ids_by_subscription: HashMap, active_login: Arc>>, @@ -349,7 +352,7 @@ impl CodexMessageProcessor { codex_linux_sandbox_exe, config, cli_overrides, - cloud_requirements, + cloud_requirements: Arc::new(RwLock::new(cloud_requirements)), conversation_listeners: HashMap::new(), listener_thread_ids_by_subscription: HashMap::new(), active_login: Arc::new(Mutex::new(None)), @@ -362,9 +365,10 @@ impl CodexMessageProcessor { } async fn load_latest_config(&self) -> Result { + let cloud_requirements = self.current_cloud_requirements(); codex_core::config::ConfigBuilder::default() .cli_overrides(self.cli_overrides.clone()) - .cloud_requirements(self.cloud_requirements.clone()) + .cloud_requirements(cloud_requirements) .build() .await .map_err(|err| JSONRPCErrorError { @@ -374,6 +378,13 @@ impl CodexMessageProcessor { }) } + fn current_cloud_requirements(&self) -> CloudRequirementsLoader { + self.cloud_requirements + .read() + .map(|guard| guard.clone()) + .unwrap_or_default() + } + fn review_request_from_target( target: ApiReviewTarget, ) -> Result<(ReviewRequest, String), JSONRPCErrorError> { @@ -834,6 +845,9 @@ impl CodexMessageProcessor { let outgoing_clone = self.outgoing.clone(); let active_login = self.active_login.clone(); let auth_manager = self.auth_manager.clone(); + let cloud_requirements = self.cloud_requirements.clone(); + let chatgpt_base_url = self.config.chatgpt_base_url.clone(); + let cli_overrides = self.cli_overrides.clone(); let auth_url = server.auth_url.clone(); tokio::spawn(async move { let (success, error_msg) = match tokio::time::timeout( @@ -863,6 +877,16 @@ impl CodexMessageProcessor { if success { auth_manager.reload(); + replace_cloud_requirements_loader( + cloud_requirements.as_ref(), + auth_manager.clone(), + chatgpt_base_url, + ); + sync_default_client_residency_requirement( + &cli_overrides, + cloud_requirements.as_ref(), + ) + .await; // Notify clients with the actual current auth mode. let current_auth_method = auth_manager @@ -927,6 +951,9 @@ impl CodexMessageProcessor { let outgoing_clone = self.outgoing.clone(); let active_login = self.active_login.clone(); let auth_manager = self.auth_manager.clone(); + let cloud_requirements = self.cloud_requirements.clone(); + let chatgpt_base_url = self.config.chatgpt_base_url.clone(); + let cli_overrides = self.cli_overrides.clone(); let auth_url = server.auth_url.clone(); tokio::spawn(async move { let (success, error_msg) = match tokio::time::timeout( @@ -956,6 +983,16 @@ impl CodexMessageProcessor { if success { auth_manager.reload(); + replace_cloud_requirements_loader( + cloud_requirements.as_ref(), + auth_manager.clone(), + chatgpt_base_url, + ); + sync_default_client_residency_requirement( + &cli_overrides, + cloud_requirements.as_ref(), + ) + .await; // Notify clients with the actual current auth mode. let current_auth_method = auth_manager @@ -1123,6 +1160,16 @@ impl CodexMessageProcessor { return; } self.auth_manager.reload(); + replace_cloud_requirements_loader( + self.cloud_requirements.as_ref(), + self.auth_manager.clone(), + self.config.chatgpt_base_url.clone(), + ); + sync_default_client_residency_requirement( + &self.cli_overrides, + self.cloud_requirements.as_ref(), + ) + .await; self.outgoing .send_response(request_id, LoginAccountResponse::ChatgptAuthTokens {}) @@ -1561,11 +1608,12 @@ impl CodexMessageProcessor { ); } + let cloud_requirements = self.current_cloud_requirements(); let config = match derive_config_from_params( &self.cli_overrides, Some(request_overrides), typesafe_overrides, - &self.cloud_requirements, + &cloud_requirements, ) .await { @@ -1647,11 +1695,12 @@ impl CodexMessageProcessor { ); typesafe_overrides.ephemeral = ephemeral; + let cloud_requirements = self.current_cloud_requirements(); let config = match derive_config_from_params( &self.cli_overrides, config, typesafe_overrides, - &self.cloud_requirements, + &cloud_requirements, ) .await { @@ -2440,12 +2489,13 @@ impl CodexMessageProcessor { ); // Derive a Config using the same logic as new conversation, honoring overrides if provided. + let cloud_requirements = self.current_cloud_requirements(); let config = match derive_config_for_cwd( &self.cli_overrides, request_overrides, typesafe_overrides, history_cwd, - &self.cloud_requirements, + &cloud_requirements, ) .await { @@ -2633,12 +2683,13 @@ impl CodexMessageProcessor { None, ); // Derive a Config using the same logic as new conversation, honoring overrides if provided. + let cloud_requirements = self.current_cloud_requirements(); let config = match derive_config_for_cwd( &self.cli_overrides, request_overrides, typesafe_overrides, history_cwd, - &self.cloud_requirements, + &cloud_requirements, ) .await { @@ -3438,12 +3489,13 @@ impl CodexMessageProcessor { ), }; + let cloud_requirements = self.current_cloud_requirements(); let config = match derive_config_for_cwd( &self.cli_overrides, request_overrides, typesafe_overrides, history_cwd, - &self.cloud_requirements, + &cloud_requirements, ) .await { @@ -3627,12 +3679,13 @@ impl CodexMessageProcessor { ), }; + let cloud_requirements = self.current_cloud_requirements(); let config = match derive_config_for_cwd( &self.cli_overrides, request_overrides, typesafe_overrides, history_cwd, - &self.cloud_requirements, + &cloud_requirements, ) .await { @@ -4944,6 +4997,41 @@ fn validate_dynamic_tools( Ok(()) } +fn replace_cloud_requirements_loader( + cloud_requirements: &RwLock, + auth_manager: Arc, + chatgpt_base_url: String, +) { + let loader = cloud_requirements_loader(auth_manager, chatgpt_base_url); + if let Ok(mut guard) = cloud_requirements.write() { + *guard = loader; + } else { + warn!("failed to update cloud requirements loader"); + } +} + +async fn sync_default_client_residency_requirement( + cli_overrides: &[(String, TomlValue)], + cloud_requirements: &RwLock, +) { + let loader = cloud_requirements + .read() + .map(|guard| guard.clone()) + .unwrap_or_default(); + match codex_core::config::ConfigBuilder::default() + .cli_overrides(cli_overrides.to_vec()) + .cloud_requirements(loader) + .build() + .await + { + Ok(config) => set_default_client_residency_requirement(config.enforce_residency.value()), + Err(err) => warn!( + error = %err, + "failed to sync default client residency requirement after auth refresh" + ), + } +} + /// Derive the effective [`Config`] by layering three override sources. /// /// Precedence (lowest to highest): diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 2dbc1fcaf98..95e26fe675f 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -413,7 +413,7 @@ async fn run_ratatui_app( initial_config: Config, overrides: ConfigOverrides, cli_kv_overrides: Vec<(String, toml::Value)>, - cloud_requirements: CloudRequirementsLoader, + mut cloud_requirements: CloudRequirementsLoader, feedback: codex_feedback::CodexFeedback, ) -> color_eyre::Result { color_eyre::install()?; @@ -470,9 +470,10 @@ async fn run_ratatui_app( should_show_onboarding(login_status, &initial_config, should_show_trust_screen_flag); let config = if should_show_onboarding { + let show_login_screen = should_show_login_screen(login_status, &initial_config); let onboarding_result = run_onboarding_app( OnboardingScreenArgs { - show_login_screen: should_show_login_screen(login_status, &initial_config), + show_login_screen, show_trust_screen: should_show_trust_screen_flag, login_status, auth_manager: auth_manager.clone(), @@ -493,9 +494,19 @@ async fn run_ratatui_app( exit_reason: ExitReason::UserRequested, }); } - // If the user made an explicit trust decision, reload config so current - // process state reflects what was persisted to config.toml. - if onboarding_result.directory_trust_decision.is_some() { + // If this onboarding run included the login step, always refresh cloud requirements and + // rebuild config. This avoids missing newly available cloud requirements due to login + // status detection edge cases. + if show_login_screen { + cloud_requirements = cloud_requirements_loader( + auth_manager.clone(), + initial_config.chatgpt_base_url.clone(), + ); + } + + // If the user made an explicit trust decision, or we showed the login flow, reload config + // so current process state reflects persisted trust/auth changes. + if onboarding_result.directory_trust_decision.is_some() || show_login_screen { load_config_or_exit( cli_kv_overrides.clone(), overrides.clone(), @@ -668,6 +679,7 @@ async fn run_ratatui_app( } _ => config, }; + set_default_client_residency_requirement(config.enforce_residency.value()); let active_profile = config.active_profile.clone(); let should_show_trust_screen = should_show_trust_screen(&config);