diff --git a/codex-rs/app-server/src/config_manager.rs b/codex-rs/app-server/src/config_manager.rs index 658f7f52bc..83b1f04f67 100644 --- a/codex-rs/app-server/src/config_manager.rs +++ b/codex-rs/app-server/src/config_manager.rs @@ -1,5 +1,5 @@ use codex_arg0::Arg0DispatchPaths; -use codex_cloud_config::chatgpt_config_bundle_loader; +use codex_cloud_config::managed_config_bundle_loader; use codex_config::CloudConfigBundleLoader; use codex_config::ConfigLayerStack; use codex_config::LoaderOverrides; @@ -196,23 +196,24 @@ impl ConfigManager { auth_manager: Arc, config: &Config, ) { - let chatgpt_base_url = config.chatgpt_base_url.clone(); - let http_client_factory = config.http_client_factory(); - let endpoint = codex_backend_client::Client::new( - chatgpt_base_url.clone(), - http_client_factory.clone(), - ) - .config_bundle_url(); - let http_client_factory = http_client_factory.with_network_policy( - self.local_network_policy - .policy() - .restrict_to_endpoints(endpoint.parse().into_iter().collect()), - ); - let loader = chatgpt_config_bundle_loader( + // Keep config-server settings and their refresh worker when only + // ChatGPT/API-key credentials change. + if self + .current_managed_config_loader() + .config_server() + .is_some() + { + self.revoke_mcp_ema(); + return; + } + let loader = managed_config_bundle_loader( auth_manager, - chatgpt_base_url, + config.chatgpt_base_url.clone(), self.codex_home.clone(), - http_client_factory, + config + .http_client_factory() + .with_network_policy(self.local_network_policy.policy()), + config.config_layer_stack.requirements_toml(), ); if let Ok(mut guard) = self.cloud_config_bundle.write() { guard.retire_ema_policy(); @@ -225,8 +226,10 @@ impl ConfigManager { pub(crate) fn clear_chatgpt_config_loader(&self) { if let Ok(mut guard) = self.cloud_config_bundle.write() { - guard.retire_ema_policy(); - *guard = CloudConfigBundleLoader::default(); + if guard.config_server().is_none() { + guard.retire_ema_policy(); + *guard = CloudConfigBundleLoader::default(); + } } else { warn!("failed to clear cloud config bundle loader"); } diff --git a/codex-rs/app-server/tests/suite/v2/config_server_tests.rs b/codex-rs/app-server/tests/suite/v2/config_server_tests.rs new file mode 100644 index 0000000000..f6ca0981e8 --- /dev/null +++ b/codex-rs/app-server/tests/suite/v2/config_server_tests.rs @@ -0,0 +1,241 @@ +//! Exercises required HTTPS policy through real app-server startup and session RPCs. + +use anyhow::Result; +use app_test_support::TestAppServer; +use codex_app_server_protocol::LoginAccountResponse; +use codex_app_server_protocol::LogoutAccountResponse; +use codex_app_server_protocol::ThreadResumeParams; +use codex_app_server_protocol::ThreadResumeResponse; +use codex_app_server_protocol::ThreadStartParams; +use codex_app_server_protocol::TurnStartParams; +use codex_app_server_protocol::TurnStatus; +use codex_app_server_protocol::UserInput; +use codex_config::test_support::CloudConfigBundleFixture; +use core_test_support::responses; +use core_test_support::skip_if_no_network; +use pretty_assertions::assert_eq; +use std::path::Path; +use std::path::PathBuf; +use std::sync::Arc; +use tempfile::TempDir; +use tempfile::tempdir; +use tokio::net::TcpListener; +use tokio::net::TcpStream; +use tokio_rustls::TlsAcceptor; +use tokio_rustls::rustls; +use tokio_util::task::AbortOnDropHandle; +use wiremock::Mock; +use wiremock::MockServer; +use wiremock::ResponseTemplate; +use wiremock::matchers::method; +use wiremock::matchers::path; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn config_server_settings_are_fetched_and_applied_at_startup() -> Result<()> { + skip_if_no_network!(Ok(())); + let setup = ConfigServerSetup::new().await?; + let mut app = setup.start_app().await?; + assert_eq!( + setup.config_server.received_requests().await.unwrap().len(), + 1 + ); + let started = app + .start_thread(ThreadStartParams { + model_provider: Some("openai".into()), + ..Default::default() + }) + .await?; + assert_eq!(started.model_provider, "company"); + setup + .assert_turn_uses_managed_instructions(&mut app, &started.thread.id) + .await?; + app.shutdown_gracefully().await?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn config_server_settings_survive_openai_login_and_logout() -> Result<()> { + skip_if_no_network!(Ok(())); + let setup = ConfigServerSetup::new().await?; + let mut app = setup.start_app().await?; + let login = app + .send_login_account_api_key_request("test-api-key") + .await?; + let _: LoginAccountResponse = app.read_response(login).await?; + let logout = app.send_logout_account_request().await?; + let _: LogoutAccountResponse = app.read_response(logout).await?; + let started = app.start_thread(ThreadStartParams::default()).await?; + setup + .assert_turn_uses_managed_instructions(&mut app, &started.thread.id) + .await?; + app.shutdown_gracefully().await?; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn config_server_settings_are_cached_across_restart_and_resume() -> Result<()> { + skip_if_no_network!(Ok(())); + let setup = ConfigServerSetup::new().await?; + let mut app = setup.start_app().await?; + let started = app.start_thread(ThreadStartParams::default()).await?; + setup + .assert_turn_uses_managed_instructions(&mut app, &started.thread.id) + .await?; + app.shutdown_gracefully().await?; + + let mut app = setup.start_app().await?; + let resumed = app + .send_thread_resume_request(ThreadResumeParams { + thread_id: started.thread.id, + ..Default::default() + }) + .await?; + let resumed: ThreadResumeResponse = app.read_response(resumed).await?; + setup + .assert_turn_uses_managed_instructions(&mut app, &resumed.thread.id) + .await?; + assert_eq!( + setup.config_server.received_requests().await.unwrap().len(), + 1 + ); + app.shutdown_gracefully().await?; + Ok(()) +} + +struct ConfigServerSetup { + home: TempDir, + config_server: MockServer, + inference: MockServer, + ca_path: PathBuf, + _tls: AbortOnDropHandle<()>, +} + +impl ConfigServerSetup { + async fn new() -> Result { + let home = tempdir()?; + let config_server = MockServer::start().await; + let inference = responses::start_mock_server().await; + let requirements = format!( + r#"additional_developer_instructions = 'Company requirements' +model_provider = 'company' + +[model_providers.company] +name = 'Company' +base_url = {:?} +env_key = 'CONFIG_SERVER_TEST_KEY' +wire_api = 'responses' +"#, + inference.uri() + ); + let bundle = CloudConfigBundleFixture::enterprise_config( + "developer_instructions = 'Company defaults'", + ) + .add_enterprise_requirement(requirements) + .into_bundle(); + Mock::given(method("GET")) + .and(path("/team/codex/config/bundle")) + .respond_with(ResponseTemplate::new(200).set_body_json(bundle)) + .mount(&config_server) + .await; + + let (origin, ca_path, tls) = https_mock_server(home.path(), &config_server).await?; + let base_url = format!("{origin}/team/"); + std::fs::write( + home.path().join("requirements.toml"), + format!("[config_server]\nbase_url = {base_url:?}\n"), + )?; + std::fs::write( + home.path().join("config.toml"), + "model = 'gpt-5.5'\napproval_policy = 'never'\nsandbox_mode = 'read-only'\n[features]\napps = false\n", + )?; + Ok(Self { + home, + config_server, + inference, + ca_path, + _tls: tls, + }) + } + + async fn start_app(&self) -> Result { + let ca_path = self.ca_path.to_string_lossy(); + let env = [ + ("CODEX_CA_CERTIFICATE", Some(ca_path.as_ref())), + ("CONFIG_SERVER_TEST_KEY", Some("test-key")), + ("NO_PROXY", Some("127.0.0.1,localhost")), + ]; + TestAppServer::builder() + .with_codex_home(self.home.path()) + .with_args(&["--strict-config"]) + .with_env_overrides(&env) + .build_initialized() + .await + } + + async fn assert_turn_uses_managed_instructions( + &self, + app: &mut TestAppServer, + thread_id: &str, + ) -> Result<()> { + let recorded = responses::mount_sse_once( + &self.inference, + responses::sse(vec![ + responses::ev_assistant_message("reply", "Policy applied"), + responses::ev_completed("response"), + ]), + ) + .await; + let completed = app + .start_turn_and_wait_for_completion(TurnStartParams { + thread_id: thread_id.into(), + input: vec![UserInput::Text { + text: "Hello".into(), + text_elements: vec![], + }], + ..Default::default() + }) + .await?; + assert_eq!(completed.turn.status, TurnStatus::Completed); + let instructions = recorded + .single_request() + .message_input_texts("developer") + .join("\n"); + assert!(instructions.contains("Company defaults")); + assert!(instructions.contains("Company requirements")); + Ok(()) + } +} + +async fn https_mock_server( + home: &Path, + policy: &MockServer, +) -> Result<(String, PathBuf, AbortOnDropHandle<()>)> { + let cert = rcgen::generate_simple_self_signed(vec!["localhost".into(), "127.0.0.1".into()])?; + let ca_path = home.join("policy-ca.pem"); + std::fs::write(&ca_path, cert.cert.pem())?; + let tls = rustls::ServerConfig::builder_with_provider(Arc::new( + rustls::crypto::aws_lc_rs::default_provider(), + )) + .with_safe_default_protocol_versions()? + .with_no_client_auth() + .with_single_cert( + vec![cert.cert.der().clone()], + rustls::pki_types::PrivatePkcs8KeyDer::from(cert.signing_key.serialize_der()).into(), + )?; + let acceptor = TlsAcceptor::from(Arc::new(tls)); + let listener = TcpListener::bind("127.0.0.1:0").await?; + let origin_url = format!("https://{}", listener.local_addr()?); + let origin = *policy.address(); + let tls = AbortOnDropHandle::new(tokio::spawn(async move { + while let Ok((stream, _)) = listener.accept().await { + let acceptor = acceptor.clone(); + tokio::spawn(async move { + let mut stream = acceptor.accept(stream).await?; + let mut upstream = TcpStream::connect(origin).await?; + tokio::io::copy_bidirectional(&mut stream, &mut upstream).await?; + Ok::<_, std::io::Error>(()) + }); + } + })); + Ok((origin_url, ca_path, tls)) +} diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index 3276e86680..aded29d0d9 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -38,6 +38,8 @@ mod config_requirements_exec; mod config_requirements_in_app_browser; mod config_requirements_login; mod config_rpc; +#[path = "config_server_tests.rs"] +mod config_server; #[cfg(unix)] #[path = "connection_handling_stdio_tests.rs"] mod connection_handling_stdio; diff --git a/codex-rs/cli/src/cloud_config.rs b/codex-rs/cli/src/cloud_config.rs index c67b34d621..8a0f1f374b 100644 --- a/codex-rs/cli/src/cloud_config.rs +++ b/codex-rs/cli/src/cloud_config.rs @@ -1,7 +1,7 @@ use anyhow::Context; use anyhow::Result; use codex_app_server::in_process::EmbeddedNetworkPolicy; -use codex_cloud_config::chatgpt_config_bundle_loader_for_storage; +use codex_cloud_config::managed_config_bundle_loader_for_storage; use codex_config::CloudConfigBundleLoader; use codex_config::ConfigLoadOptions; use codex_core::config::Config; @@ -27,6 +27,7 @@ pub async fn load_config( .build() .await .context("failed to load configuration")?; + config.config_layer_stack.ensure_config_server_applied()?; EmbeddedNetworkPolicy::default().activate(&mut config); Ok(config) } @@ -58,11 +59,12 @@ pub async fn config_builder( ) .await .context("failed to load bootstrap configuration")?; - let cloud_config_bundle = chatgpt_config_bundle_loader_for_storage( + let cloud_config_bundle = managed_config_bundle_loader_for_storage( network_policy.bind_bootstrap_auth( bootstrap_auth_config(codex_home.as_path(), &bootstrap_config) .context("failed to resolve cloud configuration authentication")?, ), + bootstrap_config.config_layer_stack.requirements_toml(), /*enable_codex_api_key_env*/ false, ) .await diff --git a/codex-rs/cli/src/doctor.rs b/codex-rs/cli/src/doctor.rs index 7b80c10e67..e7814dc56d 100644 --- a/codex-rs/cli/src/doctor.rs +++ b/codex-rs/cli/src/doctor.rs @@ -628,7 +628,7 @@ async fn load_config( ..config_overrides_from_interactive(interactive, arg0_paths) }; - crate::cloud_config::config_builder( + let config = crate::cloud_config::config_builder( &root_config_overrides, LoaderOverrides::default(), overrides, @@ -636,7 +636,9 @@ async fn load_config( .await? .build() .await - .context("failed to load Codex config") + .context("failed to load Codex config")?; + config.config_layer_stack.ensure_config_server_applied()?; + Ok(config) } fn config_overrides_from_interactive( diff --git a/codex-rs/cli/src/lib.rs b/codex-rs/cli/src/lib.rs index f0d447bb29..cc91f5b82d 100644 --- a/codex-rs/cli/src/lib.rs +++ b/codex-rs/cli/src/lib.rs @@ -18,7 +18,7 @@ pub use login::read_api_key_from_stdin; pub use login::run_login_status; pub use login::run_login_with_access_token; pub use login::run_login_with_api_key; -pub use login::run_login_with_chatgpt; +pub use login::run_login_with_browser; pub use login::run_login_with_device_code; pub use login::run_login_with_device_code_fallback_to_browser; pub use login::run_logout; diff --git a/codex-rs/cli/src/login.rs b/codex-rs/cli/src/login.rs index 1e194f67cc..a471e8cd60 100644 --- a/codex-rs/cli/src/login.rs +++ b/codex-rs/cli/src/login.rs @@ -172,6 +172,27 @@ pub async fn login_with_chatgpt( server.block_until_done().await } +/// Signs into the config server if needed, then reloads configuration. +async fn sign_in_to_config_server_and_reload_config( + config: &mut Config, + cli_config_overrides: &CliConfigOverrides, +) -> std::io::Result<()> { + let requirements = config.config_layer_stack.requirements_toml(); + let network = codex_app_server::in_process::EmbeddedNetworkPolicy::load( + &codex_config::LoaderOverrides::default(), + ) + .await; + let bootstrap_auth = network.bind_bootstrap_auth(config.auth_config()); + let auth_route_config = bootstrap_auth.auth_route_config.clone(); + codex_cloud_config::ensure_config_server_login(bootstrap_auth, requirements).await?; + *config = crate::cloud_config::load_config(cli_config_overrides, Default::default()) + .await + .map_err(|error| std::io::Error::other(format!("{error:#}")))?; + network.activate(config); + config.application_auth_route_config = Some(auth_route_config); + Ok(()) +} + fn finish_login(result: std::io::Result<()>) -> ! { match result { Ok(_) => { @@ -187,17 +208,28 @@ fn finish_login(result: std::io::Result<()>) -> ! { async fn run_interactive_login( cli_config_overrides: CliConfigOverrides, - login: impl FnOnce(Config) -> F, + chatgpt_login: impl FnOnce(Config) -> F, ) -> ! where F: std::future::Future>, { - let config = load_config_or_exit(cli_config_overrides).await; + let mut config = load_config_or_exit(cli_config_overrides.clone()).await; + if !config + .config_layer_stack + .requirements_toml() + .requires_remote_config() + { + let _login_log_guard = init_login_file_logging(&config); + finish_login(chatgpt_login(config).await); + } + + let result = + sign_in_to_config_server_and_reload_config(&mut config, &cli_config_overrides).await; let _login_log_guard = init_login_file_logging(&config); - finish_login(login(config).await) + finish_login(result) } -pub async fn run_login_with_chatgpt(cli_config_overrides: CliConfigOverrides) -> ! { +pub async fn run_login_with_browser(cli_config_overrides: CliConfigOverrides) -> ! { run_interactive_login(cli_config_overrides, |config| async move { tracing::info!("starting browser login flow"); @@ -226,7 +258,7 @@ pub async fn run_login_with_api_key( cli_config_overrides: CliConfigOverrides, api_key: String, ) -> ! { - let config = load_config_or_exit(cli_config_overrides).await; + let config = load_managed_login_config_or_exit(cli_config_overrides).await; let _login_log_guard = init_login_file_logging(&config); tracing::info!("starting api key login flow"); @@ -259,7 +291,7 @@ pub async fn run_login_with_access_token( cli_config_overrides: CliConfigOverrides, access_token: String, ) -> ! { - let config = load_config_or_exit(cli_config_overrides).await; + let config = load_managed_login_config_or_exit(cli_config_overrides).await; let _login_log_guard = init_login_file_logging(&config); tracing::info!("starting access token login flow"); @@ -521,9 +553,28 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! { } } +async fn logout_config_server(config: &Config) -> std::io::Result { + match codex_cloud_config::managed_config_auth_manager( + config.auth_config(), + config.config_layer_stack.requirements_toml(), + )? { + Some(manager) => manager.logout().await, + None => Ok(false), + } +} + pub async fn run_logout(cli_config_overrides: CliConfigOverrides) -> ! { let config = load_config_or_exit(cli_config_overrides).await; let auth_route_config = config.auth_route_config(); + let config_server_logout = if config + .config_layer_stack + .requirements_toml() + .requires_remote_config() + { + logout_config_server(&config).await + } else { + Ok(false) + }; // Cleanup is independent of the current feature gate. Retain the grant lock // through account removal so another process cannot commit a late login. @@ -590,7 +641,14 @@ pub async fn run_logout(cli_config_overrides: CliConfigOverrides) -> ! { false }; - if logged_out || cleared_bedrock_config { + let config_server_logged_out = match config_server_logout { + Ok(removed) => removed, + Err(error) => { + eprintln!("Error logging out: {error}"); + std::process::exit(1); + } + }; + if logged_out || config_server_logged_out || cleared_bedrock_config { eprintln!("Successfully logged out"); } else { eprintln!("Not logged in"); @@ -598,6 +656,20 @@ pub async fn run_logout(cli_config_overrides: CliConfigOverrides) -> ! { std::process::exit(0); } +async fn load_managed_login_config_or_exit(cli_config_overrides: CliConfigOverrides) -> Config { + let mut config = load_config_or_exit(cli_config_overrides.clone()).await; + if config + .config_layer_stack + .requirements_toml() + .requires_remote_config() + && let Err(error) = + sign_in_to_config_server_and_reload_config(&mut config, &cli_config_overrides).await + { + finish_login(Err(error)); + } + config +} + async fn load_config_or_exit(cli_config_overrides: CliConfigOverrides) -> Config { let cli_overrides = match cli_config_overrides.parse_overrides() { Ok(v) => v, diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index f8b752f8db..8b39fa155c 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -16,7 +16,7 @@ use codex_cli::read_api_key_from_stdin; use codex_cli::run_login_status; use codex_cli::run_login_with_access_token; use codex_cli::run_login_with_api_key; -use codex_cli::run_login_with_chatgpt; +use codex_cli::run_login_with_browser; use codex_cli::run_login_with_device_code; use codex_cli::run_logout; use codex_cloud_tasks::Cli as CloudTasksCli; @@ -1699,7 +1699,7 @@ async fn cli_main( let access_token = read_access_token_from_stdin(); run_login_with_access_token(login_cli.config_overrides, access_token).await; } else { - run_login_with_chatgpt(login_cli.config_overrides).await; + run_login_with_browser(login_cli.config_overrides).await; } } } diff --git a/codex-rs/cloud-config/src/bundle_loader.rs b/codex-rs/cloud-config/src/bundle_loader.rs index 0ca022897b..6c79a49c54 100644 --- a/codex-rs/cloud-config/src/bundle_loader.rs +++ b/codex-rs/cloud-config/src/bundle_loader.rs @@ -25,6 +25,27 @@ pub fn managed_config_auth_manager( crate::config_server_auth_manager(service_config, auth.codex_home, &factory) } +/// Existing grants are left to the loader's normal cache and refresh handling. +pub async fn ensure_config_server_login( + auth: AuthConfig, + requirements: &ConfigRequirementsToml, +) -> std::io::Result<()> { + let Some(manager) = managed_config_auth_manager(auth, requirements)? else { + return Ok(()); + }; + if let Err(error) = manager.authorization_id().await { + if error + .get_ref() + .and_then(|source| source.downcast_ref::()) + != Some(&codex_login::OAuthSessionError::LoginRequired) + { + return Err(error); + } + manager.login().await?; + } + Ok(()) +} + pub fn managed_config_bundle_loader( auth_manager: Arc, chatgpt_base_url: String, diff --git a/codex-rs/cloud-config/src/lib.rs b/codex-rs/cloud-config/src/lib.rs index 41c72cbb8b..be5edd07ec 100644 --- a/codex-rs/cloud-config/src/lib.rs +++ b/codex-rs/cloud-config/src/lib.rs @@ -16,6 +16,7 @@ mod validation; pub use bundle_loader::chatgpt_config_bundle_loader; pub use bundle_loader::chatgpt_config_bundle_loader_for_storage; pub use bundle_loader::chatgpt_config_bundle_loader_for_storage_without_cache; +pub use bundle_loader::ensure_config_server_login; pub use bundle_loader::managed_config_auth_manager; pub use bundle_loader::managed_config_bundle_loader; pub use bundle_loader::managed_config_bundle_loader_for_storage; diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index a8761fdfaf..054e5a1569 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -58,7 +58,7 @@ use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::TurnStartedNotification; use codex_arg0::Arg0DispatchPaths; -use codex_cloud_config::chatgpt_config_bundle_loader_for_storage; +use codex_cloud_config::managed_config_bundle_loader_for_storage; use codex_config::CloudConfigBundleLoader; use codex_config::ConfigLoadError; use codex_config::ConfigLoadOptions; @@ -407,9 +407,10 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result CloudConfigBundleLoader::default(), ) .await; - let gate_cloud_config = chatgpt_config_bundle_loader_for_storage( + let gate_cloud_config = managed_config_bundle_loader_for_storage( embedded_network_policy .bind_bootstrap_auth(bootstrap_auth_config(&codex_home, &gate_bootstrap)?), + gate_bootstrap.config_layer_stack.requirements_toml(), /*enable_codex_api_key_env*/ false, ) .await?; @@ -507,8 +508,9 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result // API keys cannot fetch workspace-managed configuration. Preserve the // existing ChatGPT bootstrap identity even when model requests allow // CODEX_API_KEY. - let cloud_config_bundle = chatgpt_config_bundle_loader_for_storage( + let cloud_config_bundle = managed_config_bundle_loader_for_storage( bootstrap_auth_config, + bootstrap_config.config_layer_stack.requirements_toml(), /*enable_codex_api_key_env*/ false, ) .await?; diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 113117caf4..d16dac6572 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -47,7 +47,7 @@ use codex_app_server_protocol::ThreadListCwdFilter; use codex_app_server_protocol::ThreadListParams; use codex_app_server_protocol::ThreadSortKey as AppServerThreadSortKey; use codex_app_server_protocol::ThreadSourceKind; -use codex_cloud_config::chatgpt_config_bundle_loader_for_storage; +use codex_cloud_config::managed_config_bundle_loader_for_storage; use codex_config::CloudConfigBundleLoader; use codex_config::ConfigLoadError; use codex_config::LoaderOverrides; @@ -1098,11 +1098,12 @@ async fn cloud_config_bundle_for_app_server_target( codex_home: &Path, embedded_network_policy: &codex_app_server_client::EmbeddedNetworkPolicy, ) -> std::io::Result { - chatgpt_config_bundle_loader_for_storage( + managed_config_bundle_loader_for_storage( embedded_network_policy .bind_bootstrap_auth(app_server_target.auth_config_for_cloud_loader( bootstrap_auth_config(codex_home, bootstrap_config)?, )), + bootstrap_config.config_layer_stack.requirements_toml(), /*enable_codex_api_key_env*/ false, ) .await @@ -1415,8 +1416,9 @@ async fn run_ratatui_app( // bundle and rebuild config. This avoids missing newly available cloud-managed // policy due to login status detection edge cases. if show_login_screen && !uses_remote_workspace && !workload_identity_selected { - cloud_config_bundle = chatgpt_config_bundle_loader_for_storage( + cloud_config_bundle = managed_config_bundle_loader_for_storage( embedded_network_policy.bind_bootstrap_auth(initial_config.auth_config()), + initial_config.config_layer_stack.requirements_toml(), /*enable_codex_api_key_env*/ false, ) .await?; diff --git a/codex-rs/tui/src/startup_orchestration.rs b/codex-rs/tui/src/startup_orchestration.rs index 27081eb8b0..50045199e3 100644 --- a/codex-rs/tui/src/startup_orchestration.rs +++ b/codex-rs/tui/src/startup_orchestration.rs @@ -305,6 +305,19 @@ pub(super) async fn run_main_inner( err } })?; + if std::io::stdin().is_terminal() && std::io::stdout().is_terminal() { + codex_cloud_config::ensure_config_server_login( + embedded_network_policy.bind_bootstrap_auth(bootstrap_auth_config( + &codex_home, + &presentation.bootstrap_config, + )?), + presentation + .bootstrap_config + .config_layer_stack + .requirements_toml(), + ) + .await?; + } // Keep normal terminal signals available until local configuration is ready. Once raw // mode begins, StartupDraft immediately takes ownership of input. let (initialized_terminal, terminal_restore_guard) = tokio::task::spawn_blocking(|| { @@ -508,10 +521,11 @@ pub(super) async fn run_main_inner( cloud_config_bundle } else { startup_draft - .run_until(chatgpt_config_bundle_loader_for_storage( + .run_until(managed_config_bundle_loader_for_storage( embedded_network_policy.bind_bootstrap_auth( app_server_target.auth_config_for_cloud_loader(config.auth_config()), ), + config.config_layer_stack.requirements_toml(), /*enable_codex_api_key_env*/ false, )) .await??