From 074c237d6b2f061e02273af5b8ba2d7869020e10 Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Fri, 29 May 2026 16:04:19 -0700 Subject: [PATCH 1/5] Ensure cloud_requirements persist after /new, /clear, /side, etc. --- codex-rs/core/src/config/mod.rs | 13 +++- codex-rs/tui/src/app.rs | 5 ++ codex-rs/tui/src/app/config_persistence.rs | 60 ++++++++++++++++++- codex-rs/tui/src/app/test_support.rs | 1 + codex-rs/tui/src/app/tests.rs | 2 + ...external_agent_config_migration_startup.rs | 3 + codex-rs/tui/src/lib.rs | 1 + 7 files changed, 81 insertions(+), 4 deletions(-) diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index e319f3b39623..72af8fad9ca7 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1168,8 +1168,17 @@ impl ConfigBuilder { } pub async fn build(self) -> std::io::Result { - // Keep the large config-loading future off small runtime thread stacks. - Box::pin(self.build_inner()).await + // Keep the large config-loading future off the caller's stack. This is + // particularly important for the TUI, where config can be reloaded from + // a deep event-dispatch stack while the Tokio worker threads have a + // larger configured stack. + match tokio::spawn(async move { Box::pin(self.build_inner()).await }).await { + Ok(result) => result, + Err(err) if err.is_panic() => std::panic::resume_unwind(err.into_panic()), + Err(err) => Err(std::io::Error::other(format!( + "config build task failed: {err}" + ))), + } } async fn build_inner(self) -> std::io::Result { diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index aae2cba95468..4e0068b5d52d 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -130,6 +130,7 @@ use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnError as AppServerTurnError; use codex_app_server_protocol::TurnStatus; use codex_app_server_protocol::WriteStatus; +use codex_config::CloudRequirementsLoader; use codex_config::ConfigLayerStackOrdering; use codex_config::LoaderOverrides; use codex_config::types::ApprovalsReviewer; @@ -490,6 +491,7 @@ pub(crate) struct App { cli_kv_overrides: Vec<(String, TomlValue)>, harness_overrides: ConfigOverrides, loader_overrides: LoaderOverrides, + cloud_requirements: CloudRequirementsLoader, runtime_approval_policy_override: Option, runtime_permission_profile_override: Option, @@ -718,6 +720,7 @@ impl App { cli_kv_overrides: Vec<(String, TomlValue)>, harness_overrides: ConfigOverrides, loader_overrides: LoaderOverrides, + cloud_requirements: CloudRequirementsLoader, initial_prompt: Option, initial_images: Vec, session_selection: SessionSelection, @@ -752,6 +755,7 @@ impl App { &mut config, &cli_kv_overrides, &harness_overrides, + &cloud_requirements, entered_trust_nux, ) .await?; @@ -1001,6 +1005,7 @@ See the Codex keymap documentation for supported actions and examples." cli_kv_overrides, harness_overrides, loader_overrides, + cloud_requirements, runtime_approval_policy_override: None, runtime_permission_profile_override: None, file_search, diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index 319830b82a03..45b19b967631 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -34,7 +34,8 @@ impl App { .codex_home(self.config.codex_home.to_path_buf()) .cli_overrides(self.cli_kv_overrides.clone()) .harness_overrides(overrides) - .loader_overrides(self.loader_overrides.clone()); + .loader_overrides(self.loader_overrides.clone()) + .cloud_requirements(self.cloud_requirements.clone()); build_config_on_runtime_worker( builder, format!("Failed to rebuild config for cwd {cwd_display}"), @@ -55,7 +56,8 @@ impl App { .codex_home(self.config.codex_home.to_path_buf()) .cli_overrides(self.cli_kv_overrides.clone()) .harness_overrides(overrides) - .loader_overrides(self.loader_overrides.clone()); + .loader_overrides(self.loader_overrides.clone()) + .cloud_requirements(self.cloud_requirements.clone()); build_config_on_runtime_worker( builder, format!("Failed to rebuild config for permission profile {profile_id}"), @@ -1123,6 +1125,60 @@ mod tests { Ok(()) } + // Regression coverage for `/new` and `/clear`: cloud requirements + // must survive the config refresh that runs before thread transitions. + #[tokio::test] + async fn refresh_in_memory_config_from_disk_keeps_cloud_requirements_for_thread_transitions() + -> Result<()> { + let mut app = make_test_app().await; + let codex_home = tempdir()?; + let required_policy = AskForApproval::Never; + let cloud_requirements = CloudRequirementsLoader::new(async move { + Ok(Some(codex_config::ConfigRequirementsToml { + allowed_approval_policies: Some(vec![required_policy]), + ..Default::default() + })) + }); + + let config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .loader_overrides(LoaderOverrides::without_managed_config_for_tests()) + .cloud_requirements(cloud_requirements.clone()) + .build() + .await?; + app.config = config; + app.cloud_requirements = cloud_requirements; + + let assert_cloud_requirements = |app: &App| { + let config = app.fresh_session_config(); + assert_eq!( + config + .config_layer_stack + .requirements_toml() + .allowed_approval_policies + .clone(), + Some(vec![required_policy]) + ); + assert_eq!(config.permissions.approval_policy.value(), required_policy); + }; + + assert_cloud_requirements(&app); + + // These are the config-refresh paths used by `/new`, `/clear`, `/fork`, + // side conversations, and leaving the session picker. + for action in [ + "starting a new thread", + "forking the thread", + "starting a side conversation", + "closing the session picker", + ] { + app.refresh_in_memory_config_from_disk_best_effort(action) + .await; + assert_cloud_requirements(&app); + } + Ok(()) + } + #[tokio::test] async fn refresh_in_memory_config_from_disk_best_effort_keeps_current_config_on_error() -> Result<()> { diff --git a/codex-rs/tui/src/app/test_support.rs b/codex-rs/tui/src/app/test_support.rs index 9bbe0c60b478..e5f85ee4283d 100644 --- a/codex-rs/tui/src/app/test_support.rs +++ b/codex-rs/tui/src/app/test_support.rs @@ -25,6 +25,7 @@ pub(super) async fn make_test_app() -> App { cli_kv_overrides: Vec::new(), harness_overrides: ConfigOverrides::default(), loader_overrides: LoaderOverrides::without_managed_config_for_tests(), + cloud_requirements: CloudRequirementsLoader::default(), runtime_approval_policy_override: None, runtime_permission_profile_override: None, file_search, diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index b18726004e01..24334537173c 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -3774,6 +3774,7 @@ async fn make_test_app() -> App { cli_kv_overrides: Vec::new(), harness_overrides: ConfigOverrides::default(), loader_overrides: LoaderOverrides::without_managed_config_for_tests(), + cloud_requirements: CloudRequirementsLoader::default(), runtime_approval_policy_override: None, runtime_permission_profile_override: None, file_search, @@ -3837,6 +3838,7 @@ async fn make_test_app_with_channels() -> ( cli_kv_overrides: Vec::new(), harness_overrides: ConfigOverrides::default(), loader_overrides: LoaderOverrides::without_managed_config_for_tests(), + cloud_requirements: CloudRequirementsLoader::default(), runtime_approval_policy_override: None, runtime_permission_profile_override: None, file_search, diff --git a/codex-rs/tui/src/external_agent_config_migration_startup.rs b/codex-rs/tui/src/external_agent_config_migration_startup.rs index ce184303c273..49a2fb11ed3c 100644 --- a/codex-rs/tui/src/external_agent_config_migration_startup.rs +++ b/codex-rs/tui/src/external_agent_config_migration_startup.rs @@ -9,6 +9,7 @@ use crate::legacy_core::config::edit::ConfigEditsBuilder; use crate::tui; use codex_app_server_protocol::ExternalAgentConfigDetectParams; use codex_app_server_protocol::ExternalAgentConfigMigrationItem; +use codex_config::CloudRequirementsLoader; use codex_features::Feature; use color_eyre::eyre::Result; use color_eyre::eyre::WrapErr; @@ -248,6 +249,7 @@ pub(crate) async fn handle_external_agent_config_migration_prompt_if_needed( config: &mut Config, cli_kv_overrides: &[(String, TomlValue)], harness_overrides: &ConfigOverrides, + cloud_requirements: &CloudRequirementsLoader, entered_trust_nux: bool, ) -> Result { if !should_show_external_agent_config_migration_prompt(config, entered_trust_nux) { @@ -321,6 +323,7 @@ pub(crate) async fn handle_external_agent_config_migration_prompt_if_needed( .codex_home(config.codex_home.to_path_buf()) .cli_overrides(cli_kv_overrides.to_vec()) .harness_overrides(harness_overrides.clone()) + .cloud_requirements(cloud_requirements.clone()) .build() .await .wrap_err("Failed to reload config after external agent migration")?; diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 5e6b6cf81ca8..d853fa27a526 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -1848,6 +1848,7 @@ async fn run_ratatui_app( cli_kv_overrides.clone(), overrides.clone(), loader_overrides.clone(), + cloud_requirements, prompt, images, session_selection, From eb22256674028348ce7cb5ff6ed3b5365c11091e Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Fri, 29 May 2026 17:15:25 -0700 Subject: [PATCH 2/5] fix type mismatch in new test --- codex-rs/tui/src/app/config_persistence.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index 45b19b967631..0c347a43ed28 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -1132,7 +1132,7 @@ mod tests { -> Result<()> { let mut app = make_test_app().await; let codex_home = tempdir()?; - let required_policy = AskForApproval::Never; + let required_policy = codex_protocol::protocol::AskForApproval::Never; let cloud_requirements = CloudRequirementsLoader::new(async move { Ok(Some(codex_config::ConfigRequirementsToml { allowed_approval_policies: Some(vec![required_policy]), From dfcc2221c8b46a2390a982d48ef1426aadcc4a2b Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Fri, 29 May 2026 17:19:24 -0700 Subject: [PATCH 3/5] Make cloud requirements reload regression fail on refresh errors --- codex-rs/tui/src/app/config_persistence.rs | 32 ++++++++++++++-------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index 0c347a43ed28..e486c3f40280 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -1148,6 +1148,16 @@ mod tests { .await?; app.config = config; app.cloud_requirements = cloud_requirements; + let app_id = "unit_test_cloud_requirements_reload_marker"; + std::fs::write( + codex_home.path().join("config.toml"), + format!( + r#" +[apps.{app_id}] +enabled = false +"# + ), + )?; let assert_cloud_requirements = |app: &App| { let config = app.fresh_session_config(); @@ -1163,19 +1173,17 @@ mod tests { }; assert_cloud_requirements(&app); + assert_eq!(app_enabled_in_effective_config(&app.config, app_id), None); - // These are the config-refresh paths used by `/new`, `/clear`, `/fork`, - // side conversations, and leaving the session picker. - for action in [ - "starting a new thread", - "forking the thread", - "starting a side conversation", - "closing the session picker", - ] { - app.refresh_in_memory_config_from_disk_best_effort(action) - .await; - assert_cloud_requirements(&app); - } + // This is the fallible reload that the best-effort `/new`, `/clear`, + // `/fork`, side-conversation, and session-picker paths wrap. + app.refresh_in_memory_config_from_disk().await?; + + assert_eq!( + app_enabled_in_effective_config(&app.config, app_id), + Some(false) + ); + assert_cloud_requirements(&app); Ok(()) } From c25d9987b95c0f55aeac3fc963a4d4087bf173d2 Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Mon, 8 Jun 2026 10:48:21 -0700 Subject: [PATCH 4/5] Use cloud config bundle for TUI reloads --- codex-rs/tui/src/app.rs | 10 +++++----- codex-rs/tui/src/app/config_persistence.rs | 18 ++++++++---------- codex-rs/tui/src/app/test_support.rs | 2 +- codex-rs/tui/src/app/tests.rs | 4 ++-- .../external_agent_config_migration_startup.rs | 6 +++--- codex-rs/tui/src/lib.rs | 2 +- 6 files changed, 20 insertions(+), 22 deletions(-) diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 4e0068b5d52d..e50a46e4ffdb 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -130,7 +130,7 @@ use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnError as AppServerTurnError; use codex_app_server_protocol::TurnStatus; use codex_app_server_protocol::WriteStatus; -use codex_config::CloudRequirementsLoader; +use codex_config::CloudConfigBundleLoader; use codex_config::ConfigLayerStackOrdering; use codex_config::LoaderOverrides; use codex_config::types::ApprovalsReviewer; @@ -491,7 +491,7 @@ pub(crate) struct App { cli_kv_overrides: Vec<(String, TomlValue)>, harness_overrides: ConfigOverrides, loader_overrides: LoaderOverrides, - cloud_requirements: CloudRequirementsLoader, + cloud_config_bundle: CloudConfigBundleLoader, runtime_approval_policy_override: Option, runtime_permission_profile_override: Option, @@ -720,7 +720,7 @@ impl App { cli_kv_overrides: Vec<(String, TomlValue)>, harness_overrides: ConfigOverrides, loader_overrides: LoaderOverrides, - cloud_requirements: CloudRequirementsLoader, + cloud_config_bundle: CloudConfigBundleLoader, initial_prompt: Option, initial_images: Vec, session_selection: SessionSelection, @@ -755,7 +755,7 @@ impl App { &mut config, &cli_kv_overrides, &harness_overrides, - &cloud_requirements, + &cloud_config_bundle, entered_trust_nux, ) .await?; @@ -1005,7 +1005,7 @@ See the Codex keymap documentation for supported actions and examples." cli_kv_overrides, harness_overrides, loader_overrides, - cloud_requirements, + cloud_config_bundle, runtime_approval_policy_override: None, runtime_permission_profile_override: None, file_search, diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index e486c3f40280..7d1f6047e056 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -35,7 +35,7 @@ impl App { .cli_overrides(self.cli_kv_overrides.clone()) .harness_overrides(overrides) .loader_overrides(self.loader_overrides.clone()) - .cloud_requirements(self.cloud_requirements.clone()); + .cloud_config_bundle(self.cloud_config_bundle.clone()); build_config_on_runtime_worker( builder, format!("Failed to rebuild config for cwd {cwd_display}"), @@ -57,7 +57,7 @@ impl App { .cli_overrides(self.cli_kv_overrides.clone()) .harness_overrides(overrides) .loader_overrides(self.loader_overrides.clone()) - .cloud_requirements(self.cloud_requirements.clone()); + .cloud_config_bundle(self.cloud_config_bundle.clone()); build_config_on_runtime_worker( builder, format!("Failed to rebuild config for permission profile {profile_id}"), @@ -1133,21 +1133,19 @@ mod tests { let mut app = make_test_app().await; let codex_home = tempdir()?; let required_policy = codex_protocol::protocol::AskForApproval::Never; - let cloud_requirements = CloudRequirementsLoader::new(async move { - Ok(Some(codex_config::ConfigRequirementsToml { - allowed_approval_policies: Some(vec![required_policy]), - ..Default::default() - })) - }); + let cloud_config_bundle = + codex_config::test_support::CloudConfigBundleFixture::loader_with_enterprise_requirement( + r#"allowed_approval_policies = ["never"]"#, + ); let config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) .loader_overrides(LoaderOverrides::without_managed_config_for_tests()) - .cloud_requirements(cloud_requirements.clone()) + .cloud_config_bundle(cloud_config_bundle.clone()) .build() .await?; app.config = config; - app.cloud_requirements = cloud_requirements; + app.cloud_config_bundle = cloud_config_bundle; let app_id = "unit_test_cloud_requirements_reload_marker"; std::fs::write( codex_home.path().join("config.toml"), diff --git a/codex-rs/tui/src/app/test_support.rs b/codex-rs/tui/src/app/test_support.rs index e5f85ee4283d..bc75a1247864 100644 --- a/codex-rs/tui/src/app/test_support.rs +++ b/codex-rs/tui/src/app/test_support.rs @@ -25,7 +25,7 @@ pub(super) async fn make_test_app() -> App { cli_kv_overrides: Vec::new(), harness_overrides: ConfigOverrides::default(), loader_overrides: LoaderOverrides::without_managed_config_for_tests(), - cloud_requirements: CloudRequirementsLoader::default(), + cloud_config_bundle: CloudConfigBundleLoader::default(), runtime_approval_policy_override: None, runtime_permission_profile_override: None, file_search, diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 24334537173c..6faff9163cb9 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -3774,7 +3774,7 @@ async fn make_test_app() -> App { cli_kv_overrides: Vec::new(), harness_overrides: ConfigOverrides::default(), loader_overrides: LoaderOverrides::without_managed_config_for_tests(), - cloud_requirements: CloudRequirementsLoader::default(), + cloud_config_bundle: CloudConfigBundleLoader::default(), runtime_approval_policy_override: None, runtime_permission_profile_override: None, file_search, @@ -3838,7 +3838,7 @@ async fn make_test_app_with_channels() -> ( cli_kv_overrides: Vec::new(), harness_overrides: ConfigOverrides::default(), loader_overrides: LoaderOverrides::without_managed_config_for_tests(), - cloud_requirements: CloudRequirementsLoader::default(), + cloud_config_bundle: CloudConfigBundleLoader::default(), runtime_approval_policy_override: None, runtime_permission_profile_override: None, file_search, diff --git a/codex-rs/tui/src/external_agent_config_migration_startup.rs b/codex-rs/tui/src/external_agent_config_migration_startup.rs index 49a2fb11ed3c..b2afbe771862 100644 --- a/codex-rs/tui/src/external_agent_config_migration_startup.rs +++ b/codex-rs/tui/src/external_agent_config_migration_startup.rs @@ -9,7 +9,7 @@ use crate::legacy_core::config::edit::ConfigEditsBuilder; use crate::tui; use codex_app_server_protocol::ExternalAgentConfigDetectParams; use codex_app_server_protocol::ExternalAgentConfigMigrationItem; -use codex_config::CloudRequirementsLoader; +use codex_config::CloudConfigBundleLoader; use codex_features::Feature; use color_eyre::eyre::Result; use color_eyre::eyre::WrapErr; @@ -249,7 +249,7 @@ pub(crate) async fn handle_external_agent_config_migration_prompt_if_needed( config: &mut Config, cli_kv_overrides: &[(String, TomlValue)], harness_overrides: &ConfigOverrides, - cloud_requirements: &CloudRequirementsLoader, + cloud_config_bundle: &CloudConfigBundleLoader, entered_trust_nux: bool, ) -> Result { if !should_show_external_agent_config_migration_prompt(config, entered_trust_nux) { @@ -323,7 +323,7 @@ pub(crate) async fn handle_external_agent_config_migration_prompt_if_needed( .codex_home(config.codex_home.to_path_buf()) .cli_overrides(cli_kv_overrides.to_vec()) .harness_overrides(harness_overrides.clone()) - .cloud_requirements(cloud_requirements.clone()) + .cloud_config_bundle(cloud_config_bundle.clone()) .build() .await .wrap_err("Failed to reload config after external agent migration")?; diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index d853fa27a526..17f9a483bf38 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -1848,7 +1848,7 @@ async fn run_ratatui_app( cli_kv_overrides.clone(), overrides.clone(), loader_overrides.clone(), - cloud_requirements, + cloud_config_bundle, prompt, images, session_selection, From 95e0f15865134edec8b6cc97f9983631b2607d0f Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Mon, 8 Jun 2026 17:57:05 -0700 Subject: [PATCH 5/5] Restore config build cancellation semantics --- codex-rs/core/src/config/mod.rs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index dd3e3caabd7a..fd74fcf2d251 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1169,17 +1169,8 @@ impl ConfigBuilder { } pub async fn build(self) -> std::io::Result { - // Keep the large config-loading future off the caller's stack. This is - // particularly important for the TUI, where config can be reloaded from - // a deep event-dispatch stack while the Tokio worker threads have a - // larger configured stack. - match tokio::spawn(async move { Box::pin(self.build_inner()).await }).await { - Ok(result) => result, - Err(err) if err.is_panic() => std::panic::resume_unwind(err.into_panic()), - Err(err) => Err(std::io::Error::other(format!( - "config build task failed: {err}" - ))), - } + // Keep the large config-loading future off small runtime thread stacks. + Box::pin(self.build_inner()).await } async fn build_inner(self) -> std::io::Result {