From a27d3847b5c36d7953b41064bc138cad26756272 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 20 May 2026 17:17:59 -0700 Subject: [PATCH] [codex] Reject read-only fallback with approvals disabled (#23774) ## Why If a user configures `approval_policy = "never"` with `sandbox_mode = "danger-full-access"`, managed requirements can reject full access and force the existing permission fallback to read-only. That leaves Codex in a dead-end session: writes are blocked by the sandbox, while approvals are disabled so the session cannot ask to proceed. This PR rejects that constrained configuration during startup instead of letting the TUI enter a read-only session that cannot make progress. The rejection is attached to the requirement-constrained permission path in [`Config`](https://github.com/openai/codex/blob/39f0abc0a7c0ed0e348a6843e9f0c7b76e2400bc/codex-rs/core/src/config/mod.rs#L3301-L3318). ## What changed - Reject the `danger-full-access` to read-only managed-requirements fallback when the effective approval policy is `never`. - Explain in the startup config error why the fallback is invalid and how to fix it. - Add a regression test for the managed requirements path. --- .../app-server/tests/suite/v2/turn_start.rs | 8 ++- codex-rs/core/src/config/config_tests.rs | 67 +++++++++++++++++++ codex-rs/core/src/config/mod.rs | 11 +++ 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 66f704f83..236d57a0e 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -1004,10 +1004,14 @@ async fn turn_start_rejects_invalid_permission_selection_before_starting_turn() assert!( err.error .message - .contains("invalid thread settings override") + .contains("`approval_policy = \"never\"` cannot be used"), + "unexpected error message: {}", + err.error.message ); assert!( - err.error.message.contains("allowed set [ReadOnly]"), + err.error + .message + .contains("requirements do not allow `sandbox_mode = \"danger-full-access\"`"), "unexpected error message: {}", err.error.message ); diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 5fa284ad3..ccde74afe 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -9906,6 +9906,73 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s Ok(()) } +#[tokio::test] +async fn danger_full_access_with_never_is_rejected_when_requirements_force_read_only() +-> std::io::Result<()> { + let codex_home = TempDir::new()?; + std::fs::write( + codex_home.path().join(CONFIG_TOML_FILE), + r#"approval_policy = "never" +sandbox_mode = "danger-full-access" +"#, + )?; + + let err = ConfigBuilder::without_managed_config_for_tests() + .codex_home(codex_home.path().to_path_buf()) + .fallback_cwd(Some(codex_home.path().to_path_buf())) + .cloud_requirements(CloudRequirementsLoader::new(async { + Ok(Some(codex_config::ConfigRequirementsToml { + allowed_sandbox_modes: Some(vec![codex_config::SandboxModeRequirement::ReadOnly]), + ..Default::default() + })) + })) + .build() + .await + .expect_err("requirements-constrained yolo should require sandbox approval"); + + assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); + assert_eq!( + err.to_string(), + "`approval_policy = \"never\"` cannot be used because requirements do not allow `sandbox_mode = \"danger-full-access\"`; Codex would fall back to read-only permissions with approvals disabled. Choose an `approval_policy` based on what you need, such as `on-request`, or choose an allowed sandbox mode." + ); + Ok(()) +} + +#[tokio::test] +async fn named_full_access_profile_with_never_is_rejected_when_requirements_force_read_only() +-> std::io::Result<()> { + let codex_home = TempDir::new()?; + std::fs::write( + codex_home.path().join(CONFIG_TOML_FILE), + r#"approval_policy = "never" +default_permissions = "dev" + +[permissions.dev.filesystem] +":root" = "write" +"#, + )?; + + let err = ConfigBuilder::without_managed_config_for_tests() + .codex_home(codex_home.path().to_path_buf()) + .fallback_cwd(Some(codex_home.path().to_path_buf())) + .cloud_requirements(CloudRequirementsLoader::new(async { + Ok(Some(codex_config::ConfigRequirementsToml { + allowed_sandbox_modes: Some(vec![codex_config::SandboxModeRequirement::ReadOnly]), + ..Default::default() + })) + })) + .build() + .await + .expect_err("requirements-constrained full-access profile should require sandbox approval"); + + assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); + assert_eq!( + err.to_string(), + "`approval_policy = \"never\"` cannot be used because requirements do not allow `sandbox_mode = \"danger-full-access\"`; Codex would fall back to read-only permissions with approvals disabled. Choose an `approval_policy` based on what you need, such as `on-request`, or choose an allowed sandbox mode." + ); + Ok(()) +} + #[tokio::test] async fn permission_profile_override_falls_back_when_disallowed_by_requirements() -> std::io::Result<()> { diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 305ae3b75..9ff9434fb 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -3334,6 +3334,17 @@ impl Config { &mut constrained_permission_profile, &mut startup_warnings, )?; + if permission_profile_was_constrained + && sandbox_mode_requirement_for_permission_profile(&original_permission_profile) + == SandboxModeRequirement::DangerFullAccess + && constrained_permission_profile.get() == &PermissionProfile::read_only() + && constrained_approval_policy.value() == AskForApproval::Never + { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "`approval_policy = \"never\"` cannot be used because requirements do not allow `sandbox_mode = \"danger-full-access\"`; Codex would fall back to read-only permissions with approvals disabled. Choose an `approval_policy` based on what you need, such as `on-request`, or choose an allowed sandbox mode.", + )); + } if permission_profile_was_constrained { // The selected profile no longer describes the effective // permissions after requirements forced a fallback.